Day17:剑指 Offer 40. 最小的 k 个数(快排)

Day17:剑指 Offer 40. 最小的 k 个数(快排)

快排简介

算法分析

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;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值