快速排序与冒泡排序类似,也属于交换排序,通过元素之间的比较和交换位置实现排序。不同的地方在于,冒泡排序在每一次循环只把一个元素冒泡到数组的一端,而快速排序在每一轮挑选一个枢纽元,并让其他比它大的元素移动到数组的一边,比它小的元素移动到数组另一边,从而把数组拆分成两个部分。
例如输入如下的数组,枢纽元选取为6[8, 1, 4, 9, 0, 3, 5, 2, 7, 6]
分割之后结果如下
[2, 1, 4, 5, 0, 3, 6, 8, 7, 9]
与归并排序一样,快速排序也是一种分治的递归算法。在分治法的思想下,原数组在每一轮都被拆分成两个部分,每一部分在下一轮又被拆分成两部分,直到不可再分为止。
快速排序的平均时间复杂度是 O(n logn) , 最坏情形的时间复杂度为 O(n2) ,即每一轮循环枢纽元都选到最大或最小的元素,但经过稍许努力可以使这种情况极难出现。快速排序有两个核心的问题,枢纽元的选择,以及元素的交换。
选取枢纽元
枢纽元(pivot),也叫基准元素。在分治过程中,以枢纽元为中心,把其他元素移动到它的左右两边。选取枢纽元主要有三种方法。
1)选取第一个元素
最简单的方法就是将第一个元素作为枢纽元。但是这种策略不可取,假设输入的数组是预排序的,那么枢纽元必然会选到最大或最小的元素,这种情况下每一轮循环数组并没有被分成两半,不能发挥分治法的优势。在这种极端情况下,快速排序需要进行 n 轮,时间复杂度退化为 O(n2) 。
2)随机选取元素
一种非常安全的做法是随机选取枢纽元。说这个策略安全,是因为随机的枢纽元不可能在每一轮循环都产生劣质的分割(即选到了最大或最小值)。但是这个策略也有问题,一是即使是随机选取的枢纽元,也有一定概率会选中最大或最小值,影响分治效率;二是随机数的生成一般开销很大,影响整体算法效率。
3)三数中值分割法(Median-of-Three Partitioning)
枢纽元的最好选择是数组的中值(也叫做中位数,是第 N/2 个最大的数),但是中值难以求出,并且会明显降低快速排序的效率。一般的做法是使用左端、右端和中间三个元素的中值作为枢纽元。例如输入的数组如下:
[8, 1, 4, 9, 6, 3, 5, 2, 7, 0]
左端元素是8,右端元素是0,中间的元素是6,因此可以确定枢纽元 v = 6
。显然,使用三数中值分割法消除了预排序输入导致的最坏情形。
分割策略
选取了枢纽元之后,下一步就需要移动元素,将数组拆分成两个部分。有个简便的方法是直接开三个数组,分别是 smaller
, same
,larger
,然后循环遍历数组的每个元素,将每个元素与枢纽元对比,小于枢纽元的 push 进 smaller
数组,等于枢纽元的 push 进 same
数组,大于枢纽元的 push 进 larger
数组,这样就实现了拆分。但是这样做无疑会占用额外空间,实际的快排(例如 JDK 的 sort 方法)都是直接对原数组进行排序的,这样就要求直接在数组中交换元素,实现数组的分割。主要有两种方法。
1)双边循环法
首先选定枢纽元 pivot ,并且设置两个指针 left 和 right ,初始状态下 left 和 right 分别位于数组最左和最右侧。
接下来进行第1次循环,从 right 指针开始,让指针所指向的元素和 pivot 进行比较,如果大于或等于 pivot ,则指针向左移动;如果小于 pivot ,则 right 指针停止移动,切换到 left 指针。
轮到 left 指针行动,让指针所指向的元素和 pivot 进行比较,如果小于或等于 pivot ,则指针向右移动;如果大于 pivot ,则 left 停止移动。
当两个指针都停止移动时,让 left 和 right 指针所指向的元素进行交换。
然后进行下一轮循环,以此类推。当 left 指针与 right 指针重合停止循环,最后一步是将 pivot 元素与重合点的元素进行交换。
例如我们需要对下面的数组进行排序,选定值为 4 的元素作为枢纽元,初始状态下 left 和 right 指针分别位于左右两侧:
进行第1次循环,从 right 指针开始,由于 1 < 4 ,所以 right 指针直接停止移动,换到 left 指针。由于 left 刚开始指向的是基准元素,判断肯定相等,因此 left 右移一位。
由于 7 > 4 ,left 指针在元素7的位置停下,这时,让left 和 right 指针所指向的元素进行交换。
接下来进入第2次循环,从 right 指针开始,向左移动,先移动到8,8 > 4 ,继续左移,由于 2 < 4 ,停在2的位置。切换到 left 指针,left 停在 6 的位置。
将 2 和 6 进行交换。
第3次循环,right 指针停在 3 的位置,left 停在 5 的位置。
将 3 和 5 进行交换。
第4次循环,right 指针停在 3 的位置,和 left 指针重合。
最后一步,把 pivot 元素与重合的元素 3 进行交换。
2)单边循环法
参考
《数据结构与算法分析(Java 语言描述)》
《漫画算法》
图解排序算法(五)之快速排序——三数取中法
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。