快排/堆排序 及其相关应用算法题

64 阅读4分钟

在各种排序算法中,面试最常考应该就是快排和堆排序了。今天我们就来实现以下。

快速排序

快排的核心思想就是从一个数组中挑选出一个中间值,并把数组分为两个部分,左边的数据都小于中间值,右边的数据都大于中间值。经过不断地递归挑选、分割数组,就可以完成数组的排序。

class Solution {
public:
    /*
    * 1. 必须左右都是闭区间,因为两边都会被用到
    * 2. 虽然采用闭区间,但循环结束条件必须是小于
    * 3. 移动值的时候需要囊括等于的情况
    * 4. 别忘记递归结束条件
    */
  int partition(vector<int>& nums, int left, int right) {
    int idx = rand() % (right - left + 1) + left;
    int pivot = nums[idx];
    swap(nums[left], nums[idx]);
    while(left < right) {
      while(left < right && nums[right] >= pivot) --right;
      nums[left] = nums[right];
      while(left < right && nums[left] <= pivot) ++left;
      nums[right] = nums[left];
    }
    nums[left] = pivot;
    return left;
  }
  void quickSort(vector<int>& nums, int left, int right) {
    if(left > right) return;
    int mid = partition(nums, left, right);

    quickSort(nums, left, mid - 1);
    quickSort(nums, mid + 1, right);
  }
  vector<int> sortArray(vector<int>& nums) {
    quickSort(nums, 0, nums.size() - 1);
    return nums;
  }
};

本代码通过了leetcode 912.排序数组的所有测试用例。需要注意的是,快速排序挑选pivot的时候最好是随机选取,否则面对基本有序的数组会退化成O(n)的算法。如果数组中有大量的重复用例,可以考虑把数组分割成三个部分[小于中间值的, 所有等于中间值的, 大于中间值的]减少递归层数。 总结:快排是一种不稳定的算法,平均时间复杂度为O(n)

快排应用--无序数组的中位数

有序数组的中位数求取很简单,直接找到数组中间的数字或者是中间两个数字的平均数。那么如何求解无序数组的中位数呢?核心思想其实还是对数组进行排序,但我们可以尽量减少排序的部分
对于快排来说,每次选取的中间数的位置就是它最后的位置,这点需要注意到。我们可以利用这点,每次对partition的返回值mid进行判断。如果mid < index,说明分割的数据位于中间位置的左边,我们只需对右边进行排序即可。

class Solution {
public:
  int partition(vector<int>& nums, int left, int right) {
    int pivot = nums[left];
    while(left < right) {
      while(left < right && nums[right] >= pivot) --right;
      nums[left] = nums[right];
      while(left < right && nums[left] <= pivot) ++left;
      nums[right] = nums[left];
    }
    nums[left] = pivot;
    return left;
  }
  //index就是我们关心的位置,求中位数的话,就是vec.size()/2,偶数位数组还需要-1
  void quickSort(vector<int>& nums, int left, int right, int index) {
    if(left > right) return;
    int mid = partition(nums, left, right);
    //只排序需要排序的部分
    if(mid <= index) {
      quickSort(nums, mid + 1, right, index);
    }
    else {
      quickSort(nums, left, mid - 1, index);
    }
  }
};

需要注意的是:如果数组的长度为偶数,例如[2, 4, 2, 1],index的值需要中间值-1,即vec.size()/2 - 1,这样才能确保中间两个位置的数字都是排序过后的。

快排应用--无序数组的第k大/小的数字

既然我们可以求取中位数,当然可以求取任意位置的数字。只要修改我们需要求取的index即可,估计还需要把上面的代码做一定修改。并且堆排序可能还是更适合一些,故不做展开阐述。

堆排序

堆排序常用来解决第K大/小的数字问题。与快排是每次找到数组中的一个数字序号不同。大顶堆/小顶堆每次都是找到数组中的极大值/极小值。
需要注意的是:自底向上建堆的时间复杂度是O(n),整个堆排序的时间复杂度也是O(logN)。实现如下:

class Solution {
public:
    void adjustdown(vector<int>& nums, int size, int index) {
        int left = index * 2 + 1;
        int right = index * 2 + 2;
        int maxIdx = index;
        
        if(left < size && nums[left] > nums[maxIdx]) maxIdx = left;
        if(right < size && nums[right] > nums[maxIdx]) maxIdx = right;
        if(index != maxIdx) {
            swap(nums[index], nums[maxIdx]);
            adjustdown(nums, size, maxIdx);
        }
    }
    void headSort(vector<int>& nums, int size, int k) {
        //建大顶堆
        for(int i = size/2 - 1; i >= 0; --i) {
            adjustdown(nums, size, i);
        }
        //排序
        for(int i = size - 1; i >= size - k; --i) {
            swap(nums[0], nums[i]);
            adjustdown(nums, i, 0);
        }
    }
    int findKthLargest(vector<int>& nums, int k) {
        headSort(nums, nums.size(), k);
        return nums[nums.size() - k];
    }

};