快速排序
算法描述
假定有一些元素 a 0 , a 1 , a 2 , ⋯ , a n a_0, a_1, a_2, \cdots, a_n a0,a1,a2,⋯,an. 快速排序的思想是从中选一个主元 a i a_i ai, 然后将元素分成两部分, 一部分中的每个元素都大于等于 a i a_i ai, 另一部分则都小于等于 a i a_i ai, 和 a i a_i ai 相等的那些显然随便放在哪一部分都可以 (值得注意的是, 这样分完后, a i a_i ai 就到达了它最终应当处于的位置. 这是快速排序之所以快的原因之一.). 然后递归的处理两部分元素.
下面通过一个例题说明主元具体的移动方法.
例题 1: 这里有一些数
13
,
43
,
31
,
57
,
26
,
0
,
0
13,~ 43,~ 31,~ 57,~ 26,~ 0,~ 0
13, 43, 31, 57, 26, 0, 0. 用快速排序的方法按照从小到大的顺序排序.
解: 这里我们一直选定某一部分的主元是该部分的第一个元素.
-
将主元 13 13 13 (加粗)移动到它最终应在的位置, 并且它前面的元素都比它小, 后面的元素都比它大.
-
初始状态:
13 ⏟ p 1 , 43 , 31 , 57 , 26 , 0 , 0 ⏟ p 2 . \underbrace{\bm{13}}_{p1}, 43, 31, 57, 26, 0, \underbrace{0}_{p2}. p1 13,43,31,57,26,0,p2 0. -
由于 13 > 0 13 > 0 13>0, 交换二者位置, 并且将指针 p 1 p1 p1 后移, 成为:
0 , 43 ⏟ p 1 , 31 , 57 , 26 , 0 , 13 ⏟ p 2 . 0, \underbrace{43}_{p1}, 31, 57, 26, 0, \underbrace{\bm{13}}_{p2}. 0,p1 43,31,57,26,0,p2 13. -
由于 13 < 43 13 < 43 13<43, 交换二者位置, 并且将指针 p 2 p2 p2 前移, 成为:
0 , 13 ⏟ p 1 , 31 , 57 , 26 , 0 ⏟ p 2 , 43. 0, \underbrace{\bm{13}}_{p1}, 31, 57, 26, \underbrace{0}_{p2}, 43. 0,p1 13,31,57,26,p2 0,43. -
由于 13 > 0 13 > 0 13>0, 交换二者位置, 并且将指针 p 1 p1 p1 后移, 成为:
0 , 0 , 31 ⏟ p 1 , 57 , 26 , 13 ⏟ p 2 , 43. 0, 0, \underbrace{31}_{p1}, 57, 26, \underbrace{\bm{13}}_{p2}, 43. 0,0,p1 31,57,26,p2 13,43. -
由于 13 < 31 13 < 31 13<31, 交换二者位置, 并且将指针 p 2 p2 p2 前移, 成为:
0 , 0 , 13 ⏟ p 1 , 57 , 26 ⏟ p 2 , 31 , 43. 0, 0, \underbrace{\bm{13}}_{p1}, 57, \underbrace{26}_{p2}, 31, 43. 0,0,p1 13,57,p2 26,31,43. -
由于 13 < 26 13 < 26 13<26, 将指针 p 2 p2 p2 前移, 成为:
0 , 0 , 13 ⏟ p 1 , 57 ⏟ p 2 , 26 , 31 , 43. 0, 0, \underbrace{\bm{13}}_{p1}, \underbrace{57}_{p2}, 26, 31, 43. 0,0,p1 13,p2 57,26,31,43. -
由于 13 < 57 13 < 57 13<57, 将指针 p 2 p2 p2 前移, 成为:
0 , 0 , 13 ⏟ p 1 , p 2 , 57 , 26 , 31 , 43. 0, 0, \underbrace{\bm{13}}_{p1,~ p2}, 57, 26, 31, 43. 0,0,p1, p2 13,57,26,31,43. -
由于 p 1 = p 2 p1 = p2 p1=p2, 这表明 13 13 13 已经到达正确的位置, 且其前面的元素都比 13 13 13 小, 后面的元素都比 13 13 13 大.
-
-
递归的将 13 13 13 前面的元素排序.
-
递归的将 13 13 13 后面的元素排序.
证毕
复杂度分析
若每次都差不多的将元素分成了 2 2 2 部分, 那么这就和基本的分治完全相同, 时间复杂度是 O ( n log n ) O(n\log n) O(nlogn). 但是若我们考虑一种极端情况:
例题 2: 将
1
,
2
,
⋯
,
n
1, 2, \cdots, n
1,2,⋯,n 排序.
解: 这原本就是有序的, 我们仍然选用第一个元素作为主元. 在这种情况下, 每次将主元移动到合适的位置后, 分成的两部分中, 有一部分是空的, 有一部分则有
n
−
1
n - 1
n−1 个元素. 注意到移动主元的过程(也就是将元素分成两部分的过程)的时间复杂度是
O
(
n
)
O(n)
O(n), 有:
T
(
n
)
=
O
(
n
)
+
T
(
n
−
1
)
=
O
(
n
)
+
O
(
n
−
1
)
+
T
(
n
−
2
)
=
⋯
=
O
(
n
2
)
T(n) = O(n) + T(n - 1) = O(n) + O(n - 1) + T(n - 2) = \cdots = O(n^{2})
T(n)=O(n)+T(n−1)=O(n)+O(n−1)+T(n−2)=⋯=O(n2)
证毕
那么如何解决这种情形. 首先想一想这是什么因素导致了上述的情况. 实际上, 这是因为元素本来就是有序排列而引起的, 这里的有序实际上取决于两个因素. 第一, 元素原本就是有序的. 第二, 我们选用主元的方式导致了元素有序性在这里发挥了它的作用. 总的来说, 是元素的有序性与选主元的方式这二者的耦合性导致了快速排序的时间复杂度的增长. 那么就可以针对性的对算法进行改善, 进行解耦. 下面是改善的两种方法:
-
我们可以使用快速排序前, 先将元素随机打乱.
-
换一种选主元的方法. 既然元素是有序的, 那么我们换一种选主元的方式, 让元素的有序性无法发挥其作用. 譬如说: 选用第一个元素, 最后一个元素, 中间一个元素三者的中位数作为主元.(具体方式可以是将中位数移动到这一堆元素的初始位置)
例题3: 这里有一些数
13
,
43
,
31
,
57
,
26
,
0
,
0
13,~ 43,~ 31,~ 57,~ 26,~ 0,~ 0
13, 43, 31, 57, 26, 0, 0. 用快速排序的方法按照从小到大的顺序排序.
解:
- 我们首先将
13
,
57
,
0
13, 57, 0
13,57,0 这三者中的最大值放到最后面, 再将中位数放到最前面, 最小值自然是中间:
13 , 43 , 31 , 0 , 26 , 0 , 57 13, 43, 31, 0, 26, 0, 57 13,43,31,0,26,0,57 - 按照前面的方法排序即可
证毕