快排简介
算法分析
1.快排有两个核心点。分别是 哨兵划分 和 递归 。哨兵划分: 以数组某个元素(一般选取首元素)为 基准数 ,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。**递归:**对 左子数组 和 右子数组 分别递归执行 哨兵划分,直至子数组长度为 1 时终止递归,即可完成对整个数组的排序。
2.时间复杂度。最佳 Ω(NlogN): 每轮哨兵划分操作将数组划分为两个等长的数组,哨兵划分为线性时间复杂度 O(N),递归轮数为O(logN)。 平均 Ω(NlogN): 对于随机输入的数组,也一样。最差 O(N2): 对于某些特殊输入数组,类似完全倒叙的数组,每次哨兵划分只能分为长度为1和长度为N-1的两个数组,此时递归轮数达到N。
3.空间复杂度。快速排序的递归深度最好与平均皆为 log N ;输入数组完全倒序下,达到最差递归深度 N*2 。
算法优化
快速排序的常见优化手段有「Tail Call」和「随机基准数」两种。
Tail Call :
每轮递归时,仅对 较短的子数组 执行哨兵划分 partition() ,就可将最差的递归深度控制在 O(logN) (每轮递归的子数组长度都 ≤ 当前数组长度 / 2 ),即实现最差空间复杂度 O(logN) 。
随机基准数 :
同样地,由于快速排序每轮选取「子数组最左元素」作为「基准数」,因此在输入数组 完全有序 或 完全倒序 时, partition() 每轮只划分一个元素,达到最差时间复杂度 O(N^2)。
因此,可使用 随机函数 ,每轮在子数组中随机选择一个元素作为基准数,这样就可以极大概率避免以上劣化情况。
代码
C++ :
int partition(vector<int>& nums, int l, int r) {
// 以 nums[l] 作为基准数
int i = l, j = r;
while (i < j) {
while (i < j && nums[j] >= nums[l]) j--;
while (i < j && nums[i] <= nums[l]) i++;
swap(nums[i], nums[j]);
}
swap(nums[i], nums[l]);
return i;
}
void quickSort(vector<int>& nums, int l, int r) {
// 子数组长度为 1 时终止递归
if (l >= r) return;
// 哨兵划分操作
int i = partition(nums, l, r);
// 递归左(右)子数组执行哨兵划分
quickSort(nums, l, i - 1);
quickSort(nums, i + 1, r);
}
// 调用
vector<int> nums = { 4, 1, 3, 2, 5, 1 };
quickSort(nums, 0, nums.size() - 1);
题目
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
法一
本题使用排序算法解决最直观,对数组 arr 执行排序,再返回前 k 个元素即可。
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
quickSort(arr, 0, arr.size() - 1);
vector<int> res;
res.assign(arr.begin(), arr.begin() + k);
return res;
}
private:
void quickSort(vector<int>& arr, int l, int r) {
// 子数组长度为 1 时终止递归
if (l >= r) return;
// 哨兵划分操作(以 arr[l] 作为基准数)
int i = l, j = r;
while (i < j) {
while (i < j && arr[j] >= arr[l]) j--;
while (i < j && arr[i] <= arr[l]) i++;
swap(arr[i], arr[j]);
}
swap(arr[i], arr[l]);
// 递归左(右)子数组执行哨兵划分
quickSort(arr, l, i - 1);
quickSort(arr, i + 1, r);
}
};
法二
题目只要求返回最小的 k 个数,对这 k 个数的顺序并没有要求。因此,只需要将数组划分为 最小的 kk 个数 和 其他数字 两部分即可,而快速排序的哨兵划分可完成此目标。
根据快速排序原理,如果某次哨兵划分后 基准数正好是第 k+1k+1 小的数字 ,那么此时基准数左边的所有数字便是题目所求的 最小的 k 个数 。
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
// 基于快排的数组划分
// 1.题目只要求返回第k小的数组,对这个数组并没有顺序要求。
// 所以只要当哨兵为第k+1小的数字的时候,它的右边即为第k小的数
if (k >= arr.size()) return arr;
return quicksort(arr, 0, arr.size()-1, k);
}
vector<int> quicksort(vector<int>& arr, int left, int right, int k){
// 1.退出条件
// 因为有if (k >= arr.size()) return arr,所以我们的快排
// 排到最后,所以也就不需要 if(i>=j)这样的退出条件。
// if (left > right) return arr;
// 2.while
int i = left, j = right;
while(i < j){
while(i < j && arr[j] >= arr[left]) j--;
while(i < j && arr[i] <= arr[left]) i++;
swap(arr[i], arr[j]);
}
swap(arr[i], arr[left]);
// 3.递归
if (i > k) return quicksort(arr, left, i-1, k);
if (i < k) return quicksort(arr, i+1, right, k);
vector<int> res;
res.assign(arr.begin(), arr.begin()+k);
return res;
}
};