快速排序算法复习

快速排序算法复习

作者:何贤

基础理论

快速排序算法是什么?

快速排序算法由 C. A. R. Hoare 在 1960 年提出。它的时间复杂度也是 O(nlogn)O(nlogn),但它在时间复杂度为 O(nlogn)O(nlogn) 级的几种排序算法中,大多数情况下效率更高,所以快速排序的应用非常广泛。再加上快速排序所采用的分治思想非常实用,使得快速排序深受面试官的青睐,所以掌握快速排序的思想尤为重要。

基本思想

从数组中取出一个数,称之为基数(pivot)
遍历数组,将比基数大的数字放到它的右边,比基数小的数字放到它的左边。遍历完成后,数组被分成了左右两个区域
将左右两个区域视为两个数组,重复前两个步骤,直到排序完成

事实上,快速排序的每一次遍历,都将基数摆到了最终位置上。第一轮遍历排好 1 个基数,第二轮遍历排好 2 个基数(每个区域一个基数,但如果某个区域为空,则此轮只能排好一个基数),第三轮遍历排好 4 个基数(同理,最差的情况下,只能排好一个基数),以此类推。总遍历次数为 logn~n 次,每轮遍历的时间复杂度为 O(n)O(n),所以很容易分析出快速排序的时间复杂度为 O(nlogn)O(nlogn) ~ O(n^2)O(n2),平均时间复杂度为 O(nlogn)O(nlogn)。

自用笔记

在这里插入图片描述

在这里插入图片描述

快速排序例题

快速排序

给你一个整数数组 nums,请你将该数组升序排列。用快速排序

#include <vector>
#include <iostream>
using namespace std;
/*
 * @lc app=leetcode.cn id=912 lang=cpp
 *
 * [912] 排序数组
 */

// @lc code=start
class Solution
{
private:
    // 快速排序 非递归版
    void quick_sort(vector<int> &nums, int left, int right)
    {
        // 边界条件判断
        if (left >= right)
        {
            return;
        }
        // 设定新的基准 这里只是假定每次最左 实际上应该随机拟定新的基准
        int mid = left + (right - left) / 2;
        int flag = nums[mid], i = left - 1, j = right + 1;
        while (i < j)
        {
            do
            {
                i++;
            } while (nums[i] < flag);
            do
            {
                j--;
            } while (nums[j] > flag);
            if (i < j)
            {
                swap(nums[i], nums[j]);
            }
        }
        quick_sort(nums, left, j);
        quick_sort(nums, j + 1, right);
    }

public:
    vector<int> sortArray(vector<int> &nums)
    {
        quick_sort(nums, 0, nums.size() - 1);
        return nums;
    }
};
// @lc code=end

多数元素
CategoryDifficultyLikesDislikes
algorithmsEasy (66.52%)1246-
Tags Companies

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

#include <vector>
#include <iostream>
using namespace std;
/*
 * @lc app=leetcode.cn id=912 lang=cpp
 *
 * [912] 排序数组
 */

// @lc code=start
class Solution
{
private:
    // 快速排序 非递归版
    void quick_sort(vector<int> &nums, int left, int right)
    {
        // 边界条件判断
        if (left >= right)
        {
            return;
        }
        // 设定新的基准 这里只是假定每次最左 实际上应该随机拟定新的基准
        int mid = left + (right - left) / 2;
        int flag = nums[mid], i = left - 1, j = right + 1;
        while (i < j)
        {
            do
            {
                i++;
            } while (nums[i] < flag);
            do
            {
                j--;
            } while (nums[j] > flag);
            if (i < j)
            {
                swap(nums[i], nums[j]);
            }
        }
        quick_sort(nums, left, j);
        quick_sort(nums, j + 1, right);
    }

public:
    vector<int> sortArray(vector<int> &nums)
    {
        quick_sort(nums, 0, nums.size() - 1);
 

        return nums;
    }
};
// [2,2,1,3,1,1,4,1,1,5,1,1,6]
// @lc code=end

快速排序的递归框架

顺带一提用i做划分时的模板

void quick_sort(int q[], int l, int r)
{
    if(l >= r) return;

    int i = l - 1, j = r + 1, x = q[l + r + 1 >> 1];//注意是向上取整,因为向下取整可能使得x取到q[l]
    while(i < j)
    {
        do i++; while(q[i] < x);
        do j--; while(q[j] > x);
        if(i < j) swap(q[i], q[j]);
    }
    quick_sort(q, l, i - 1), quick_sort(q, i, r);//不用q[l..i],q[i+1..r]划分的道理和分析4中j的情况一样
}

还有从大到小排序的模板(仅仅改两个地方的判断符号)

void quick_sort(int q[], int l, int r)
{
    if(l >= r) return;

    int i = l - 1, j = r + 1, x = q[l + r >> 1];
    while(i < j)
    {
        do i++; while(q[i] > x); // 这里和下面
        do j--; while(q[j] < x); // 这行的判断条件改一下
        if(i < j) swap(q[i], q[j]);
    }
    quick_sort(q, l, j), quick_sort(q, j + 1, r);

}

边界问题证明

边界情况分析

若x选为q[r],数组中q[l…r-1] < x,那么这一轮循环结束时i = r, j = r,显然会造成无限划分

快排属于分治算法,最怕的就是 n分成0和n,或 n分成n和0,这会造成无限划分

以j为划分时,x不能选q[r] (若以i为划分,则x不能选q[l])

假设 x = q[r]

关键句子**quick_sort(q, l, j)**, quick_sort(q, j + 1, r);

由于j的最小值是l,所以q[j+1..r]不会造成无限划分

但q[l..j](即quick_sort(q, l, j))却可能造成无限划分,因为j可能为r

do i++; while(q[i] < x)和do j–; while(q[j] > x)不能用q[i] <= x 和 q[j] >= x

假设q[l..r]全相等

则执行完do i++; while(q[i] <= x);之后,i会自增到r+1

然后继续执行q[i] <= x 判断条件,造成数组下标越界(但这貌似不会报错)

并且如果之后的q[i] <= x (此时i > r) 条件也不幸成立,

就会造成一直循环下去(亲身实验),造成内存超限(Memory Limit Exceeded)

if(i < j) swap(q[i], q[j])能否使用 i <= j

可以使用if(i <= j) swap(q[i], q[j]);

因为 i = j 时,交换一下q[i],q[j] 无影响,因为马上就会跳出循环了

最后一句能否改用quick_sort(q, l, j-1), quick_sort(q, j, r)作为划分(用i做划分时也是同样的道理,)

不能

根据之前的证明,最后一轮循环可以得到这些结论

j <= i 和 q[l..i-1] <= x, q[i] >= x 和 q[j+1..r] >= x, q[j] <= x

所以,q[l..j-1] <= x 是显然成立的,

但quick_sort(q, j, r)中的q[j] 却是 q[j] <= x,这不符合快排的要求

另外一点,注意quick_sort(q, l, j-1), quick_sort(q, j, r)可能会造成无线划分

当x选为q[l]时会造成无限划分,报错为(MLE),

如果手动改为 x = q[r],可以避免无限划分

但是上面所说的q[j] <= x 的问题依然不能解决,这会造成 WA (Wrong Answer)

j的取值范围为[l..r-1]

证明:

假设 j 最终的值为 r ,说明只有一轮循环(两轮的话 j 至少会自减两次)

说明q[r] <= x (因为要跳出do-while循环)

说明 i >= r(while循环的结束条件), i 为 r 或 r + 1(必不可能成立)

说明 i 自增到了 r , 说明 q[r] >= x 和 q[l..r-1] < x,

得出 q[r] = x 和 q[l..r-1] < x 的结论,但这与 x = q[l + r >> 1]矛盾

反证法得出 j < r

假设 j 可能小于 l 说明 q[l..r] > x ,矛盾

反证法得出 j >= l

所以 j的取值范围为[l..r-1],不会造成无限划分和数组越界

请注意 —— 边界证明非原创

作者:醉生梦死
链接:https://www.acwing.com/solution/content/16777/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值