【详解排序算法】快速排序

大家好🙌我是你们的好朋友,大数据老虾😀。相遇是缘,既然来了就拎着小板凳坐下来一起唠会儿😎,如果在文中有所收获,请别忘了一键三连,动动你发财的小手👍,你的鼓励,是我创作的动力😁!废话不多说,直接😎 开干吧!

PS:文末干货,记得拎着小板凳离开的时候也给它顺走 🤣
座右铭:“懒”对一个人的毁灭性有多大,早起的重要性就多大

【排序算法】快速排序

算法解析

快速排序算法有两个核心点,分别为哨兵划分递归

1、哨兵划分: 以数组某个元素(一般选取首元素)为基准数,将所有小于准基数的元素移动至其左边,大于基准数的元素移动至其右边

如下图所示:为哨兵划分操作流程。通过一轮哨兵划分,可将数组排序问题擦划分为两个较短数组的排序问题(称之为左(右)子数组)

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

递归: 对左子数组和右子数组分别递归执行哨兵划分,直至子数组长度为1时终止递归,即可完成对整个数组的排序。

如下图所示,为示例 数组[2, 4, 1, 0, 3, 5] 的快速排序流程。观察发现,快速排序和二分法的原来类似,都是以log时间复杂度实现搜索区间的缩小。

image.png

// java code
void quickSort(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);
}

int partition(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, j);
    }
    swap(nums, i, l);
    return i;
}

void swap(int[] nums, int i, int j) {
    // 交换 nums[i] 和 nums[j]
    int tmp = nums[i];
    nums[i] = nums[j];
    nums[j] = tmp;
}

// 调用
int[] nums = { 4, 1, 3, 2, 5 };
quickSort(nums, 0, nums.length - 1);
# python code
def quick_sort(nums, l, r):
    # 子数组长度为 1 时终止递归
    if l >= r: return
    # 哨兵划分操作
    i = partition(nums, l, r)
    # 递归左(右)子数组执行哨兵划分
    quick_sort(nums, l, i - 1)
    quick_sort(nums, i + 1, r)
    
def partition(nums, l, r):
    # 以 nums[l] 作为基准数
    i, j = l, r
    while i < j:
        while i < j and nums[j] >= nums[l]: j -= 1
        while i < j and nums[i] <= nums[l]: i += 1
        nums[i], nums[j] = nums[j], nums[i]
    nums[l], nums[i] = nums[i], nums[l]
    return i

# 调用
nums = [3, 4, 1, 5, 2]
quick_sort(nums, 0, len(nums) - 1)

算法特性

时间复杂度

  • 最佳 Ω(NlogN): 最佳情况下, 每轮哨兵划分操作将数组划分为等长度的两个子数组;哨兵划分操作为线性时间复杂度 O(N);递归轮数共 O(logN) 。

  • 平均Θ(NlogN): 在随机输入数组下,哨兵划分操作的递归轮数也为O(logN) 。

  • 最差 O(N^2): 在某些特殊输入数组下,每轮哨兵划分操作都将长度为 N 的数组划分为长度为 1 和 N−1 的两个子数组,此时递归轮数达到 N 。

空间复杂度

空间复杂度 O(N): 快速排序的递归深度最好与平均皆为logN;输入数组完全倒序下,达到最差递归深度 N。

虽然平均时间复杂度与归并排序和堆排序一致,但再实际使用中快速排序效率更高:

  • 最差情况稀疏性: 虽然快速排序的最差时间复杂度为 O(N^2),差于归并排序和堆排序,但统计意义上看,这种情况出现的机率很低。大部分情况下,快速排序以O(NlogN) 复杂度运行。

  • 缓存使用效率高: 哨兵划分操作时,将整个子数组加载入缓存中,访问元素效率很高;堆排序需要跳跃式访问元素,因此不具有此特性。

  • 常数系数低: 在提及的三种算法中,快速排序的 比较、赋值、交换 三种操作的综合耗时最低(类似于插入排序快于冒泡排序的原理)。

  • 原地: 不用借助辅助数组的额外空间,递归仅使用O(logN) 大小的栈帧空间。

  • 非稳定: 哨兵划分操作可能改变相等元素的相对顺序。

  • 自适应: 若每轮哨兵划分操作都将长度为 N 的数组划分为长度 1 和 N−1 两个子数组,则时间复杂度劣化至 O(N^2)。

算法优化

快排的常见优化手段:【Tail Call】和【随机基准数】两种

Tail Call:

1、由于普通快速排序每轮选取子数组最左元素作为基准数,因此在输入数组完全倒序时,partition()的递归深度会达到N,即最差空间复杂度为O(N)。

2、每轮递归时,仅对较短的子数组执行哨兵划分partition(),就可将最差的递归深度控制在O(logN)(每轮递归的子数组长度都 ≤ 当前数组长度),即实现最差空间复杂度 O(logN) 。

代码仅修改quick_sort()方法处:

// java code
void quickSort(int[] nums, int l, int r) {
    // 子数组长度为 1 时终止递归
    while (l < r) {
        // 哨兵划分操作
        int i = partition(nums, l, r);
        // 仅递归至较短子数组,控制递归深度
        if (i - l < r - i) {
            quickSort(nums, l, i - 1);
            l = i + 1;
        } else {
            quickSort(nums, i + 1, r);
            r = i - 1;
        }
    }
}
# python code
def quick_sort(nums, l, r):
    # 子数组长度为 1 时终止递归
    while l < r:
        # 哨兵划分操作
        i = partition(nums, l, r)
        # 仅递归至较短子数组,控制递归深度
        if i - l < r - i:
            quick_sort(nums, l, i - 1)
            l = i + 1
        else:
            quick_sort(nums, i + 1, r)
            r = i - 1

随机基准数:

1、由于快速排序每轮选取子数组最左元素作为基准数,因此在输入数组 完全有序 或 完全倒序 时, partition() 每轮只划分一个元素,达到最差时间复杂度 O(N^2)。

2、因此,可使用 随机函数 ,每轮在子数组中随机选择一个元素作为基准数,这样就可以极大概率避免以上劣化情况。

3、值得注意的是,由于仍然可能出现最差情况,因此快速排序的最差时间复杂度仍为 O(N^2)。

代码仅需修改 partition() 方法:

// java code
int partition(int[] nums, int l, int r) {
    // 在闭区间 [l, r] 随机选取任意索引,并与 nums[l] 交换
    int ra = (int)(l + Math.random() * (r - l + 1));
    swap(nums, l, ra);
    // 以 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, j);
    }
    swap(nums, i, l);
    return i;
}
# python code
def partition(nums, l, r):
    # 在闭区间 [l, r] 随机选取任意索引,并与 nums[l] 交换
    ra = random.randrange(l, r + 1)
    nums[l], nums[ra] = nums[ra], nums[l]
    # 以 nums[l] 作为基准数
    i, j = l, r
    while i < j:
        while i < j and nums[j] >= nums[l]: j -= 1
        while i < j and nums[i] <= nums[l]: i += 1
        nums[i], nums[j] = nums[j], nums[i]
    nums[l], nums[i] = nums[i], nums[l]
    return i

文末彩蛋🤩

🚗🤩😉💖🌹👀✨给各位朋友安利一下平时收集的各种学习资料!!!有需要的朋友点击一下传送门,自行领取。程序员经典名言:“收藏了就等于学会啦”。 做人也要像蜡烛一样,在有限的一生中有一分热发一份光,给人以光明,给人以温暖!
图灵程序丛书300+
Linux实战100讲
Linux书籍
计算机基础硬核总结
计算机基础相关书籍
操作系统硬核总结
Java自学宝典
Java学习资料
Java硬核资料
Java面试必备
Java面试深度剖析
阿里巴巴Java开发手册
MySQL入门资料
MySQL进阶资料
深入浅出的SQL
Go语言书籍

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值