快速排序,算法经典。我这里要做的,就是分析其设计思想并一步步来实现它。
分治法
快速排序采用的是“分治法”,其英文名现能体现其特点”divide and conquer”,分割&征服。具体到本算法是这样的:
(以数组从小到大排序为例)
- 从数组是选取一个基准数,根据大于或小于该基准数将元素分列到数组左右两端
- 对上一步分割出的左右两子数组再次执行步骤1
- 循环递归,直到子数组长度为1
以上如何体现出“分治”了呢?每操作一次,虽然单个元素并不有序,但左右两块是有序的(左边都小于基准数,右边都大于基准数),这就是conquer,继续对左右子数组进行同样操作,直到左右两块都是单个元素,这就是divide。
左右划分的实现
好的,既然思路已经有了,我们就来一步步实现吧,为了方便,依然使用Python。第一步将是关键的部分,以下是代码:(所有的说明全在注释里了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| __author__ = 'chen' a = [92, 27, 26, 42, 24, 23, 5, 38, 95, 10, 42, 26, 81, 58, 17, 78, 14, 30, 55, 83, 61] def qs(a, s, e): print a[s: e + 1] i = s j = e k = (s + e) / 2 m = a[k] print "standard num : a[%d]=%d"%(k,m) while i < j: while a[i] < m and i < j: i += 1 a[k] = a[i] k = i while a[j] >= m and i < j: j -= 1 a[k] = a[j] k = j print "end at no.%d"%i a[i] = m print a[s:e + 1] qs(a, 0, len(a) - 1)
|
运行,输出如下 :
1 2 3 4
| [92, 27, 26, 42, 24, 23, 5, 38, 95, 10, 42, 26, 81, 58, 17, 78, 14, 30, 55, 83, 61] standard num : a[10]=42 #基准数 end at no.11. #i,j相遇位置(结束位) [30, 27, 26, 14, 24, 23, 5, 38, 17, 10, 26, 42, 81, 58, 92, 78, 95, 42, 55, 83, 61]
|
可以看到,虽然看起来还是杂乱无章,但是以11号元素(42)为分割,左右分别是小于和大于(等于)它的数。
递归调用
至此步骤1就已经完成了,2,3实质为一步,只需要在函数qs(a,s,e)最后加上如下语句即可:
1 2 3 4 5
| if i>s: qs(a,s,i) if i < e-1: qs(a,i+1,e)
|
在文件最后加上print a
之后,输出如下 :(为了便于阅读,删除了部分输出)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| [92, 27, 26, 42, 24, 23, 5, 38, 95, 10, 42, 26, 81, 58, 17, 78, 14, 30, 55, 83, 61] standard num : a[10]=42 end at a[11] = 26 [30, 27, 26, 14, 24, 23, 5, 38, 17, 10, 26, 42, 81, 58, 92, 78, 95, 42, 55, 83, 61] [30, 27, 26, 14, 24, 23, 5, 38, 17, 10, 26, 42] standard num : a[5]=23 end at a[4] = 24 [10, 17, 5, 14, 23, 30, 24, 38, 26, 27, 26, 42] [10, 17, 5, 14, 23] standard num : a[2]=5 end at a[0] = 10 [5, 17, 10, 14, 23] [17, 10, 14, 23] standard num : a[2]=10 end at a[1] = 17 [10, 17, 14, 23] [17, 14, 23] standard num : a[3]=14 end at a[2] = 17 [14, 17, 23] [17, 23] standard num : a[3]=17 end at a[3] = 17 [17, 23] [30, 24, 38, 26, 27, 26, 42] standard num : a[8]=26 end at a[6] = 24 [24, 26, 38, 30, 27, 26, 42] …… [81, 58, 92, 78, 95, 42, 55, 83, 61] standard num : a[16]=95 end at a[20] = 61 [81, 58, 92, 78, 61, 42, 55, 83, 95] …… [5, 10, 14, 17, 23, 24, 26, 26, 27, 30, 38, 42, 42, 55, 58, 61, 78, 81, 83, 92, 95]
|
这样快速排序就完成了。实际实现的过程中遇到了一些小问题(毕竟程序一次写对的机率不大),主要是子数组的划分和循环结束的判定条件。
最好在划分子数组前加上if e-s>1:
的判定条件,即:
1 2 3 4 5
| if e - s > 1: if i > s: qs(a, s, i) if i < e - 1: qs(a, i + 1, e)
|
即可以优化算法(避免某些二元数组排序两遍)又可以避免出错(陷入死循环)。
性能比较
那么快速排序到底有多快?写了一个简单地冒泡法与之比较:
1 2 3 4 5
| def bubble(b): for i in range(0, len(b)): for j in range(0, len(b) - i - 1): if b[j] > b[j + 1]: b[j], b[j + 1] = b[j + 1], b[j]
|
以相同的测试数组大小为20000,其代码与结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| a = [] for i in range(0, 20000): a.append(random.randint(0, 20000)) b = a[:] start = time.time() qs(a, 0, len(a) - 1) end = time.time() print end -start start = time.time() bubble(b) end = time.time() print end -start print a[0:11],a[-11:-1] print b[0:11],b[-11:-1]
|
输出如下 :
1 2 3 4
| 0.0928180217743 35.1762809753 [0, 0, 1, 2, 3, 3, 5, 6, 7, 9, 11] [19985, 19988, 19989, 19989, 19992, 19993, 19993, 19993, 19996, 19997] [0, 0, 1, 2, 3, 3, 5, 6, 7, 9, 11] [19985, 19988, 19989, 19989, 19992, 19993, 19993, 19993, 19996, 19997]
|
输出一致,但时间分别是0.093s和35.176s,差了300倍还多,可见 n*log(n) 与 n^2 的差异是十分明显的,而且随着输入规模的增加会更明显。