排序算法:快速排序

快速排序与冒泡排序类似,也属于交换排序,通过元素之间的比较和交换位置实现排序。不同的地方在于,冒泡排序在每一次循环只把一个元素冒泡到数组的一端,而快速排序在每一轮挑选一个枢纽元,并让其他比它大的元素移动到数组的一边,比它小的元素移动到数组另一边,从而把数组拆分成两个部分

例如输入如下的数组,枢纽元选取为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 。显然,使用三数中值分割法消除了预排序输入导致的最坏情形。

分割策略

选取了枢纽元之后,下一步就需要移动元素,将数组拆分成两个部分。有个简便的方法是直接开三个数组,分别是 smallersamelarger ,然后循环遍历数组的每个元素,将每个元素与枢纽元对比,小于枢纽元的 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 指针所指向的元素进行交换

然后进行下一轮循环,以此类推。

2)单边循环法

参考

《数据结构与算法分析(Java 语言描述)》
《漫画算法》
图解排序算法(五)之快速排序——三数取中法

你可能感兴趣的:(前端算法-数据结构)