LeetCode刷题之排序算法

LeetCode刷题笔记

排序算法

稳定与不稳定

内排序和外排序

内排序:待排序的所有记录都放置在内存中。

外排序:由于记录的个数太多,不能同时放在内存中,整个排序过程需要在内外存之间多次交换数据才能进行。

//排序所有的顺序表结构
#define MAXSIZE 10000;
typedef struct{
    int r[MAXSIZE+1];
    int length;
}SqList;

//排序过程中用到的元素交换函数
void swap(SqList* L,int i,int j){
    int temp;
    temp = L->r[i];
    L->r[i] = L->r[j];
    L->r[j] = temp;
}

1.插入排序

1.插入排序

  • 基本思想

    将一个记录插入到一个已经排好序的有序表中,从而得到一个新的,记录数增一的有序表。

  • 代码实现

    void InsertSort(SqList *L)
    {
        int i, j;
        for (i = 2; i <= L->length; i++) {
            if (L->r[i] < L->r[i-1]) {
                L->r[0] = L->r[i];
                for(j = i-1; L->r[j] > L->r[0]; j--) {
                    L->r[j+1] = L->r[j];
                }
                L->r[j+1] = L->r[0];
            }
        }
    }
    
  • 算法复杂度和稳定性分析

    最好的情况是O(n),最坏的情况是O(n^2)。算法是稳定的。

2.希尔排序

  • 基本思想

    希尔排序的本质还是插入排序,但是其与直接插入排序不同的是它会将原序列分成几个小序列,然后再分别对其进行排序,然后逐渐增大子序列的长度,最后,当子序列长度为1时,排序结束。


    代码思路及图解链接 希尔排序


  • 代码实现

    void ShellSort(SqList *L) {
        int i, j, k = 0;
        int increment = L->length;//当inrecment == 1时,子序列即为全局序列
        do {
            increment = increment / 3 + 1;//选择序列增量,即每个子序列中的相邻元素序号在全局序列中序列差值为increment
            for (i = increment + 1; i <= L->length; i++) {//下面的步骤与直接插入排序的思路一致,几个子序列分别进行直接插入排序
                if(L->r[i] < L->[i-increment]) {
                    L->r[0] = L->r[i];//L->r[0]可用temp来代替
                    for (j = i - increment; j > 0 && L->r[0] < L->r[j]; j -= increment) {//对子序列进行直接插入操作
                        L->r[j+increment] = L-<r[j];
                       L->[j+increment] = L->r[0];
                    }
                }
            }
        } while(increment > 1);  
    }
    
  • 算法复杂度及稳定性分析

    最好的情况是O(n1.5),最坏的情况是O(n2)。算法是不稳定的。

2.交换排序

1.冒泡排序

  • 基本思想

    冒泡排序属于一种交换排序,基本思想:是两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。

  • 代码实现

    1.初级版本(从前往后比)

    void BubbleSort(SqList* L)
    {
        int i,j;
        for (int i = 1; i < L->length; i++)
        {
            for (int j = i+1; j <= L->length; j++)
            {
                if (L->r[i] > L->r[j]) swap(L,i,j);
            }
        }
    }
    

    2.改良版本(从后往前比)

    void BubbleSort(SqList *L)
    {
        int i,j;
        for (int i = 1; i < L->length; i++)
        {
            for (int j = L->length; j >= i; j--)
            {
                if (L->r[j] > L->r[j+1])
                {
                    swap(L, j, j+1);
                }
            }
        }
    }
    

    3.进阶版本(从后往前比,增加标志位)

    void BubbleSort(SqList *L)
    {
        int i,j;
        bool flag = true;
        for (int i = 1; i < L->length && flag; i++) {
            flag = false;
            for (int j = L->length; j >= i; j--) {
                swap(L, j, j+1);
                flag = true;
            }
        }
    }
    
  • 算法复杂度及稳定性分析

    最好的情况为O(n),最坏的情况是O(n^2)。算法是稳定的。

2.快速排序

  • 基本思想

    快速排序的基本思想是:通过一段排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

  • 代码实现

    1.基本的代码实现

    void QuickSort(SqList *L) {
        QSort(L, 1, L->length);
    } 
    
    void QSort(SqList *L, int low, int high) {
        int pivot;
        if (low < high) {
            pivot = Partition(L,low,high);//找出中间枢纽
            QSort(L, low, pivot-1);//对低子表进行递归排序
            QSort(L, pivot+1, high);//对高子表进行递归排序
        }
    }
    
    int Partition(SqList *L, int low, int high) {
        int piv;
        piv = L->r[low];
        while (low < high) {
            while (low < high && L->r[high] >= piv) {
                --high;
            }
            swap(L, low, high);
            while (low < high && L->r[low] <= piv) {
                ++low;
            }
            swap(L, low, high);
        }
        return low;
    }
    

    2.快速排序进行优化

    • 优化选取中间枢纽

      中间枢纽的选择快速排序中显得极为重要,选取一个合适的中间,可以大大减少排序过程中的交换。因此我们可以基于此对其进行优化。固定选取第一个关键字显得极不合理,我们采取的优化方法是三数取中,即选取三个数,对其进行排序,然后选取中间数作为中间枢纽。

      int piv;
      
      int m = low + (high - low) / 2;
      if (L->r[low] > L->r[high]) {
          swap(L, low ,high);
      }
      if (L->r[m] > L->r[high]) {
          swap(L, high, m);
      }
      if (L->r[m] > L->r[low]) {
          swap(L, m, low);
      }
      piv = L->r[low];
      
    • 优化不必要的交换

      我们发现中间枢纽的值在不停的交换,实际上它只要到达它最终的位置就行。因此我们可以对其进行优化,减少不必要的交换。

      int Partition(SqList *L, int low, int high) {
          int piv;
          //piv = L->r[low];
      
          int m = low + (high - low) / 2;
          if (L->r[low] > L->r[high]) {
              swap(L, low ,high);
          }
          if (L->r[m] > L->r[high]) {
              swap(L, high, m);
          }
          if (L->r[m] > L->r[low]) {
              swap(L, m, low);
          }
          piv = L->r[low];
      
          L->r[0] = piv;//将中间枢纽的值存储在L->r[0]中
          while (low < high) {
              while (low < high && L->r[high] >= piv) {
                  --high;
              }
              //swap(L, low, high);
              L->r[low] = L->r[high]
              while (low < high && L->r[low] <= piv) {
                  ++low;
              }
              //swap(L, low, high);
              L->r[high] = L->r[low];
          }
          L->r[low] = L->r[0]
          return low;
      }
      
    • 优化排序小数组时的排序方案

      对于数组元素比较少的数组,大可不必用快速排序,可以直接采用直接插入排序。

  • 算法复杂度及稳定性分析
    最好的情况下时间复杂度为O(nlogn),最坏的情况下是O(n^2)。算法是不稳定的。

3. 选择排序

1.简单选择排序

  • 基本思想

    通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1<=i<=n)个记录交换。

  • 代码实现

    void SelectSort(SqList *L)
    {
        int i, j, min;
        for (int i=1; i < L->length; i++) {
            min = i;
            for (int j = i + 1; j <= L->length; j++) {
                if (L->r[min] > L->r[j]) {
                    min = j;
                }
            }
            if(i != min) {
                swap(L, i, min);
            }
        }
    }
    
  • 算法复杂度和稳定性分析

    最好和最坏的情况都是O(n^2)。算法是稳定的。

2.堆排序

  • 基本思想

    堆的定义:堆是具有以下性质的二叉树:每个节点的值都大于或者等于其左右孩子的节点的值,称为大顶堆;每个节点的值都小于或者等于其左右孩子的值,称为小顶堆。

    堆排序算法思路:将待排序的序列构成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值)。然后将剩余n-1个元素重新构成一个堆,这样就可以得到n个元素中的次大值。如此反复进行,便能得到一个有序序列。

  • 代码实现

    //堆序列表进行堆排序
    void HeapSort(SqList *L) {
        int i;
        for (int i = L->length / 2; i > 0; i--) {//先将待排序序列变成大顶堆
            HeapAdjust(L, i, L->length);
        }
        for (i = L->length; i > 1; i--) {
            swap(L, 1, i);//交换最大元素与末尾元素的位置
            HeapAdjust(L, 1, i-1);//调整堆
        }
    }
    
    //堆调整函数
    void HeapAdjust(SqList *L,int s,int m) {
        int j,temp;
        temp = L->r[s];
        for (j = 2 * s;j < = m; j *= 2) {
            if (j <m && L->r[j] < L->r[j+1]) {//比较左右子节点的大小,取其中较大的节点
                ++j;
            }
            if (temp >= L->r[j]) {//若节点大于左右子节点,则堆已经完成
                break;
            }
            L->r[s] = L->r[j];//交换值
            s = j;
        }
        L->r[s] = temp;
    }
    
  • 算法复杂度及稳定性分析

    最好的和最坏的情况都是O(n*logn)。算法是不稳定的。

4.归并排序

  • 算法思路

    归并排序(Merging Sort)就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到n/2| (x表示不小于x的最小整数)个长度为2或1的有序子序列:再两两归并,...如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。

  • 代码实现

    1.递归实现归并排序

    //归并排序算法
    void MergeSort(SqList *L) {
        MSort(L->r, L->r, 1, L->length);
    }
    
    //归并排序
    void MSort(int SR[], int TR1[], int s, int t) {
        int m;
        int TR2[MAXSIZE + 1];
        if (s == t) {
            TR1[s] == SR[s];
        } else {
            m = (s + t) / 2;
            MSort(SR, TR2, s, m);
            MSort(SR, TR2, m+1, t);
            Merge(TR2, TR1, s, m, t);
        }
    }
    
    //合并排序算法
    void Merge(int SR[], int TR[], int i, int m, int n) {//将有序的SR[i到m]和有序的SR[m到n归并为有序的TR
        int j, k, l;
        for (j = m+1, k = i;i <= m && j <= n; k++) {
            if (SR[j] < SR[i]) {
                TR[k] = SR[i++];
            } else {
                TR[k] = SR[j++];
            }
        }
        if (i <= m) {
            for (; i<=m; i++) {
                TR[K++] = SR[i];
            }
        }
        if (j <= n) {
            for (; j<=n; j++) {
                TR[k++] = SR[j];
            }
        }
        
    }
    
  • 算法复杂度及稳定性分析

    2.非递归实现归并排序

    void MergeSort2(SqList *L) {//对顺序表L作归并非递归排序
        int*TR = new int[L->length];
        int k = 1;//将相邻距离为k的子序列合并
        while (k < L->length) {
            MergePass(L->r, TR, k, L->length);
            k = 2 * k;
            MergePass(TR, L->r, k, L->length);
            k = 2 * k;
        }
    }
    void MergePass(int SR[], int TR[], int s, int n) {//将SR[]中相邻长度为s的子序列两两归并到TR[]
        int i = 1;
        int j;
        while (i <= n-2*s+1) {//两两归并
            Merge(SR, TR, i, i+s-1, i+2*s-1);
            i = i + 2 * s;
        }
        if (i < n-s+1) {//归并最后两个子序列
            Merge(SR, TR, i, i+s-1, n);
        } else {
            for(j = i; j <= n; j++) {//若最后只剩下一个子序列
                TR[j] = SR[j];
            }
        }
    }
    
    

    在使用归并排序时,应尽量使用非递归方法。

  • 算法复杂度及稳定性分析

    最好和最坏的情况都是O(nlogn),辅助空间是O(n)。算法是稳定的。

LeetCode实战

1.LeetCode之215题 数组中的第k个最大元素

1.题目描述

给定整数数组 nums 和整数 k,请返回数组中第 **k** 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素

示例1:

输入:[3,2,1,5,6,4] 和 k = 2

输出:5

提示

  • 1 <= k <= nums.length <= 104
  • -104 <= nums[i] <= 104

2.解题思路及代码实现

1. 使用 [1]

  • 解题思路

    返回第k个选择的元素,注意这里我们每次选择的是最大的元素。

  • 代码实现

    /*
     * @lc app=leetcode.cn id=215 lang=cpp
     *
     * [215] 数组中的第K个最大元素
     */
    
    // @lc code=start
    class Solution {
    public:
        int findKthLargest(vector<int>& nums, int k) {
            int len = nums.size();
            for (int i=0; i < k; i++) {
                int max = i;
                for (int j = i+1; j<len; j++) {
                    if(nums[j] > nums[max]) {
                        max = j;
                    }
                }
                if (max != i) {
                    swap(nums, i, max);
                }
            }
            return nums[k-1];
        }
    
        void swap(vector<int> &nums, int i,int j) {
            int temp = nums[i];
            nums[i] = nums[j];
            nums[j] =temp;
        }
    };
    // @lc code=end
    

2.使用[2]

  • 解题思路

    构造大顶堆,返回第k个大顶堆堆顶的元素

  • 代码实现

    class Solution {
    public:
        int findKthLargest(vector<int>& nums, int k) {
            int len = nums.size();
            for (int i = len/2-1; i >= 0; i--) {
                HeapAdjust(nums, i, len);
            }
            int count = 0;
            for (int i = len; i >= 0; i--) {
                count++;
                if(count == k) return nums[0];
                swap(nums, 0, i-1);
                HeapAdjust(nums, 0, i-1);
            }
            return 0;
        }
    
        void HeapAdjust(vector<int> &nums, int i,int len) {
            int temp = nums[i];
            while (2*i+1 < len) {
                int max = 2*i+1;
                if (2*i+2 < len && nums[2*i+2] > nums[2*i+1]) {
                    max = 2*i+2;
                }
                if (nums[max] <= temp ) {
                    break;
                }
                nums[i] = nums[max];
                i = max;
            }
            nums[i] = temp;
        }
    
        void swap(vector<int> &nums, int i,int j) {
            int temp = nums[i];
            nums[i] = nums[j];
            nums[j] =temp;
        }
    
    };
    

3.使用[3]

  • 解题思路

    在分解的过程当中,我们会对子数组进行划分,如果划分得到的 中间元素的下标 正好就是我们需要的下标,就直接返回中间元素;否则,如果 中间元素比目标下标小,就递归右子区间,否则递归左子区间。这样就可以把原来递归两个区间变成只递归一个区间,提高了时间效率。这就是「快速选择」算法。

  • 代码实现

    class Solution {
    public:
        int findKthLargest(vector<int>& nums, int k) {
            int low = 0, high = nums.size() - 1;
            return nums[QuickFind(nums, low, high, high - k + 1)];
        }
    
        int QuickFind(vector<int> &nums, int low ,int high, int index) {
            int piv = Partiton(nums, low, high);
            if (piv == index) return piv;
            else {
                return index > piv ? QuickFind(nums, piv+1, high, index) : QuickFind(nums, low, piv-1, index);
            }
        }
    
        int Partiton(vector<int> &nums, int low,int high) {
            int piv;
            
            int m = low + (high - low) / 2;
            if (nums[low] > nums[high]) {
                swap(nums, low ,high);
            }
            if (nums[m] > nums[high]) {
                swap(nums, high, m);
            }
            if (nums[m] > nums[low]) {
                swap(nums, m, low);
            }
            piv = nums[low];
    
            while(low < high) {
                while (low < high && nums[high] >= piv) {
                    --high;
                }
                swap(nums, low, high);
                while(low < high && nums[low] <= piv) {
                    ++low;
                }
                swap(nums, low, high);
            }
            return low;
        }
    
        void swap(vector<int> &nums, int i, int j) {
            int temp = nums[i];
            nums[i] = nums[j];
            nums[j] = temp;
        }
    
    };
    

2. LeetCode之347 前K个高频元素

1.题目描述

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例一

输入:nums = [1,1,1,2,3],k=2

输出:[1,2]

示例二

输入: nums = [1],k=1

输出: [1]

2.解题思路及代码实现

  • 解题思路

    使用map统计各个数字出现的次数,然后使用桶排序对出现的次数进行排序即可.

  • 代码实现

    /*
     * @lc app=leetcode.cn id=347 lang=cpp
     *
     * [347] 前 K 个高频元素
     */
    
    // @lc code=start
    class Solution {
    public:
        vector<int> topKFrequent(vector<int>& nums, int k) {
            map<int,int> con;
            int max_count = 0;
            for (int i = 0; i<nums.size(); i++) {
               max_count = max(max_count, ++con[nums[i]]);
            }
    
            vector<vector<int>> bucket(max_count + 1);
            for (const auto & p : con) {
                bucket[p.second].push_back(p.first);
            }
    
            vector<int> ans;
            for (int i = max_count; i >= 0 && ans.size() < k; i--) {
                for (const auto &num : bucket[i]) {
                    ans.push_back(num);
                    if (ans.size() == k) break;
                }
            }
            return ans;
        } 
    };
    // @lc code=end
    

3.LeetCode之451根据字符出现频率排序

  • 解题思路

    与上面一致。

  • 代码实现

    /*
     * @lc app=leetcode.cn id=451 lang=cpp
     *
     * [451] 根据字符出现频率排序
     */
    
    // @lc code=start
    class Solution {
    public:
        string frequencySort(string s) {
            map<int, int> con;
            int max_count = 0;
            for(const auto &ch : s) {
                max_count = max(max_count, ++con[ch]);
            }
    
            vector<vector<char>> bucket(max_count + 1);
            for (const auto &p : con) {
                bucket[p.second].push_back(p.first);
            }
            string res;
            for (int i = max_count; i>0; i--) {
                for (const auto &c : bucket[i]) {
                    for (int j = 0; j < i; j++) {
                        res.push_back(c);
                    }
                }
            }
            return res;
        }
    };
    // @lc code=end
    

4.LeetCode之75颜色分类

  • 解题思路

    与上面差不多

  • 代码实现

    /*
     * @lc app=leetcode.cn id=75 lang=cpp
     *
     * [75] 颜色分类
     */
    
    // @lc code=start
    class Solution {
    public:
        void sortColors(vector<int>& nums) {
            map<int, int> con;
            int max_count = 0;
            for (int i = 0; i < nums.size(); i++) {
                max_count = max(max_count, ++con[nums[i]]);
            }
    
            int index = 0;
            for (int i=0; i <3; i++) {
                for (int j = 0; j<con[i]; j++) {
                    nums[index] = i;
                    ++index;
                }
            }
        }
    };
    // @lc code=end
    

  1. 选择排序。即返回第k个选择的元素,注意这里我们每次选择的是最大的元素。 ↩︎

  2. 堆排序。即构造大顶堆,返回第k个大顶堆堆顶的元素 ↩︎

  3. 快速排序。使用快速排序,返回下标为k的中间枢纽的元素即为所求。 ↩︎

posted @ 2021-11-19 15:25  IU_UI  阅读(168)  评论(0编辑  收藏  举报