【算法日积月累】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]
,这个数组有什么特点呢?
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,这一点要仔细体会。
当然,我们不会这么做,因为这样做其实是使用了辅助的空间。实际上,我们可以通过交换数组的元素达到同样的效果。这个操作是非常经典的,是需要在理解的基础上记忆的方法。
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 过程。
我们再画一个简单的图来加深对“大放过,小操作”这个过程的理解。
下面是我以前用 Java 写的时候的笔记:
在理解了 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)
写好以后,不妨用前面我们介绍的方法,自己写一个生成随机数组的方法,再写一个判断数组是否有序的方法,来检验我们自己编写的排序算法是否正确。
我可以,你也一定可以!
看到这样的输出,心里很欣慰,居然写对了!
下面是我自己编写的测试代码,供参考,你们完全可以写出比我写的更好的代码。
下面这个方法看起来有点“傻”。