【算法日积月累】6-快速排序

【算法日积月累】6-快速排序

理解 Partition

要理解快速排序,首先要理解的一个重要操作是 Partition。Partition 其实并不难理解,它的基本思想如下所述。

Partition 每一次排定一个元素,这个元素不像选择排序或者冒泡排序那样排在数组的开头或者末尾,可以在数组的任何位置,既然是排定它,那么它就要在数组最终确定的位置上,所以它具有这样的性质:位于它前面的元素都小于它,位于它后面的元素都大于等于它。

例如数组是 [4,6,2,3,1,5,7,8]

我们首先选择数组的第 1 1 1 个元素 4 4 4 。经过 Partition 以后,我们要达到的是这样一个效果:[2,3,1,4,6,5,7,8],这个数组有什么特点呢?

image-20190113011144274

1、我们刚开始选定的元素 4 4 4 就放在了最终它应该放在的位置上;

2、元素 4 4 4 之前的所有元素都比它小,严格说应该是“不大于” 4 4 4,并且它们的相对顺序和 Partition 以前一致;

3、对称地,元素 4 4 4 之后的所有元素都比它大,严格说应该是“不小于” 4 4 4,并且它们的相对顺序和 Partition 以前一致。

这是怎么做到的呢?其实很简单,我们就拿 4 4 4 作为“基准元素”,在剩下的数组元素中扫一遍,比 4 4 4 的“依次”拿出来形成“列表1”,比 4 ​ 4​ 4 大的“依次”拿出来形成“列表2”,那么 [列表1, 元素 4, 列表2 ] 就是上面的结果。在 Python 中,你可以很轻松地实现这个操作。

def partition(nums):
    pivot = nums[0]
    return [i for i in nums[1:] if i < pivot] + [pivot] + [i for i in nums[1:] if i >= pivot]


if __name__ == '__main__':
    nums = [4, 3, 1, 2, 7, 8, 5]
    result = partition(nums)
    print(result)

而快速排序是这样的:

def quick_sort(nums):
    if len(nums) <= 1:
        return nums
    pivot = nums[0]
    left = [num for num in nums[1:] if num <= pivot]
    right = [num for num in nums[1:] if num > pivot]
    return quick_sort(left) + [p] + quick_sort(right)

如果你没有看懂这里 Python 的语法,也没有关系。Python 就是这么神奇,看起来像伪代码一样,但它真的可以运行。快速排序的逻辑在上面这段代码中也很清晰了:每次排定一个元素,因为排定以后满足上面提到的性质2和性质3,然后对这个元素的左边和右边再递归进行下去。是不是看到了“分治”的影子?只不过他不像归并排序那样,最后要“合”起来。因为一开始,我们不是“无脑地”一分为二,我们做了上面的性质2和性质3,这一点要仔细体会。

当然,我们不会这么做,因为这样做其实是使用了辅助的空间。实际上,我们可以通过交换数组的元素达到同样的效果。这个操作是非常经典的,是需要在理解的基础上记忆的方法。

image-20190113015630329

def partition(nums, left, right):
    pivot = nums[left]
    j = left
    for i in range(left + 1, right + 1):
        if nums[i] < pivot:
            j += 1
            nums[j], nums[i] = nums[i], nums[j]
    nums[left], nums[j] = nums[j], nums[left]
    return j

在编写这个方法的过程中,我们使用了两个“指针”变量:i 和 j。i 用于遍历,表示我要把除了 pivot 的剩下所有数组元素全部看完。而 j 维护了一个循环不变的性质,就是区间 [left,j) 内的元素都严格小于 pivot,这是代码编写的关键之处,注意这里 j 并不包含,所以初始值设置为 left,区间 [left,left) 内的元素为空,这也是合理的。接下来,只要遇到比 pivot 小的元素,就交换到 j 这个位置,因此 j + 1 就表示下一个要交换到这里的元素的位置。把数组中剩下的元素全部看过一遍以后,再把 j 和 left 交换就可以了。

最后,我们要把 j 返回回去,供快速排序后序调用。

刚开始接触的时候,可能会比较 (๑•ᴗ•๑) ,要反复看,用几个小数组手写一遍程序运行的过程。并且在自己编写的代码中添加一些打印输出,观察自己写的代码是不是按照自己认为的那个逻辑在运行

例如,我在编写代码的时候,就会以注释的形式写上我理解的 partition 过程。

image-20190113020754827

我们再画一个简单的图来加深对“大放过,小操作”这个过程的理解。

image-20190113012840221

下面是我以前用 Java 写的时候的笔记:

image-20190113013147369

在理解了 Partition 的基础,我们不难写出第 1 版快速排序的代码。

编写第 1 版快速排序

def partition(nums, left, right):
    pivot = nums[left]
    j = left
    for i in range(left + 1, right + 1):
        if nums[i] < pivot:
            j += 1
            nums[j], nums[i] = nums[i], nums[j]
    nums[left], nums[j] = nums[j], nums[left]
    return j


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


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

写好以后,不妨用前面我们介绍的方法,自己写一个生成随机数组的方法,再写一个判断数组是否有序的方法,来检验我们自己编写的排序算法是否正确。

我可以,你也一定可以!

image-20190113014637420

看到这样的输出,心里很欣慰,居然写对了!

下面是我自己编写的测试代码,供参考,你们完全可以写出比我写的更好的代码。

image-20190113014808146

image-20190113015052533

image-20190113014947691

下面这个方法看起来有点“傻”。

image-20190113015132283

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值