【算法日积月累】7-两路快排

本文介绍了一种针对大量重复元素的快速排序优化方法——两路快排。通过对传统快速排序的改进,采用指针对撞的方式将重复元素分散到数组两端,有效减少递归深度,提高排序效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【算法日积月累】7-两路快排

一、第 1 版快速排序的两个优化和问题

1、两个优化

2、问题

在有很多重复元素的情况下,放在中间的那个 j 的位置也会使得递归的过程变得很不平衡,这个时候我们也可以采取一定的优化措施。

我们可以编写一个测试用例,构造出一个有很多个重复键值的数组,分别使用“归并排序”和“快速排序”,看看它们的耗时。

from sort.sort_helper import generate_random_array
from sort.C_merge_sort_1 import merge_sort
from sort.D_quick_sort import quick_sort
from sort.sort_helper import check_sorted
import time

# 最小值是 10,最大值是 20,都可以取到
# 取了 10000 个元素,用快排1和归并排序测试一下
nums = generate_random_array(10, 20, 10000)
print(nums)

nums_for_merge_sort = nums[:]
nums_for_quick_sort_1 = nums[:]

begin = time.time()
merge_sort(nums_for_merge_sort)
print('归并排序耗时:', time.time() - begin)

begin = time.time()
quick_sort(nums_for_quick_sort_1)
print('快速排序耗时:', time.time() - begin)

check_sorted(nums, nums_for_merge_sort)
check_sorted(nums, nums_for_quick_sort_1)

运行结果:

image-20190113110922529

可以看到,“快速排序”比我们第 1 版没有优化过的“归并排序”都慢很多。

我们不妨将待测试数组的重复元素搞得多一些。

image-20190113111123479

可以看到,此时“归并排序”可以完成排序任务,而我们第 1 版的“快速排序”已经抛出异常了,这个异常不是因为我们编写的逻辑有严重错误,而是因为我们这个测试用例太极端了,这个异常就是“递归深度太深”,因为重复元素太多,都被分到了数组的同一侧,而导致递归深度太深,导致系统栈都不够用了。

二、第 2 版快速排序:双路快排

发现问题:在有很多重复元素的情况下,放在中间的那个 j 的位置也会使得递归的过程变得很不平衡。

基本思想:指针对撞的双路快速排序,在有很多键重复的情况下,重复的键能够比较“均匀地”分布在数组的前后,即将与标定点相等的元素等概率分散到递归函数的两边。
实现方式:把等于标定点的元素“等概率地”分散到了标定元素左右两边

image-20190113112002505

小技巧:在编写与“指针”(不是 C++ 中的指针)相关的逻辑的时候,我们一定要把握住我们设置的指针的含义,在遍历的过程中,位置这个指针的含义不变,这样才能编写出正确的代码。

对于一些边界条件,一定要思考清楚,如果刚开始写有困难的,可以考虑以下几种方式把代码写对:

1、参考他人优秀的代码,即使是抄代码也要抄明白,抄完以后自己复现一下;

2、在代码中输出一些打印语句,或者使用代码编辑器的 debug 功能对代码进行调试;

3、使用小规模的测试用例在纸上走一下代码逻辑,把设置的指针的含义,循环不变量是如何维持的写出来,很多问题就看得比较清晰了。

对于这种比较抽象的逻辑,如果在脑子里不能想得特别清楚,在纸上写写画画是一个很不错的选择,我在写这个逻辑的时候,把“指针”含义和循环不变量是怎么维持的写出来以后,一些边界条件,例如,1、什么时候退出循环;2、退出循环以后,标定点(pivot)和哪个指针交换;3、指针 i 和指针 j 的初始值是多少;这 3 个问题就看得非常清楚了。聪明的你或许不用像我一样写这么多,不过我想写写画画会加速你的思考过程,也能加深你对问题的理解,这其实也是我们常常写代码时“用空间换时间”的一种体现吧。

image-20190113105835255

第 2 版基于“指针对撞”的 partition 的快速排序:

def __partition_2(nums, left, right):
    p = nums[left]
    i = left + 1
    j = right
    while True:
        while i <= right and nums[i] < p:
            i += 1
        while j >= left + 1 and nums[j] > p:
            j -= 1
        if i > j:
            break
        nums[i], nums[j] = nums[j], nums[i]
        i += 1
        j -= 1
    nums[left], nums[j] = nums[j], nums[left]
    return j


def __quick_sort(nums, left, right):
    if left >= right:
        return
    p_index = __partition_2(nums, left, right)
    __quick_sort(nums, left, p_index - 1)
    __quick_sort(nums, p_index + 1, right)


def quick_sort(nums):
    __quick_sort(nums, 0, len(nums) - 1)

此时,我们可以把测试用例弄得再极端一些,发现“快速排序”不仅可以完成排序任务,而且比“归并排序”还要快一些。

image-20190113111817095

总结

这一版“快速排序”最重要的优化就是针对数组中有大量和标定元素重复的元素,我们通过“指针对撞”的方式把它们分散到数组的两端,以减少递归的深度。

关于“指针对撞”其实是一个常用的算法技巧,LeetCode 上有很多关于“双指针”的问题,当然有些是链表中的,有些不是“对撞”,而是一前一后,感兴趣的朋友们不妨练习一下。

image-20190113113555613

其实,我们还可以做得更好一些,我们可以把与标定点相等的元素都赶到数组的中间去,这样在有很多重复元素的数组中,一下子就可以把中间的很多元素排定,同时递归调用的深度也大大减少了,这就是我们第 3 版的快速排序,它用到的技巧我们刚刚提到过,也是“双指针”,只不过不是“对撞”,而是“一前一后”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值