算法图解笔记——Chapter 4 Quick Sorting
Author: Seven Zou
Email: zoushiqi0404@gmail.com
Language: Python2.7
4 快速排序
- 一种经典的递归类方法,D&C (Divide and Conquer)。
- 在上述方法基础上,了解快速排序。
4.1 D&C
本节所讲延续了上一个Chapter中Recusive概念。D&C所表达的是一种解决方法,一种解决问题的思路。针对这种方法,打算借用书中的例子来记录学习。
Case 1:假设你有一块土地,打算将其均匀分割成方块,并分出的方块尽可能大。
如下图所示,分法各式各样,但是不能保证分出来的方块是最大的。
这时引入D&C策略进行解决问题,D&C算法是递归的,满足如下条件。
- 1.找出基线条件,这种条件必须尽可能简单;
- 2.不断将问题分解(或者说缩小规模),直到符合基线条件。
e.g. 如果一条边 25 c m 25cm 25cm,另一边长 50 c m 50cm 50cm,那么可使用的最大方块为 25 m × 25 m 25m \times 25m 25m×25m。则对这块地分割完成。
Solution to Case 1:
路线:1.找出基线条件。2. 不断缩小规模,直到符合基线条件。
-
1.可以对这块土地的长进行以64基数进行分割,但会余 400 m 400m 400m长。
-
2.此处Attention,那也可对余下的土地进行相同的分割操作。有结论: 适用于这小块地的最大方块,也是适用于整块地的最大方块。[Euclid Algorithm]那么由此结论,问题就可以被逐步简化,变成了分割 640 m × 400 m 640m \times 400m 640m×400m的问题。
-
3.以此类推,余下的土地会被继续分割。
-
4.最终,会被分为最大方块为 80 m × 80 m 80m \times 80m 80m×80m的最小单元。
Case 2:对数组[1 2 3]求和。
在学习本节之前,我的第一反应也是应用循环进行求和。
def sum(arr):
total = 0
for x in arr:
total += x
return total
print sum([1, 2, 3])
Solution to Case2:应用递归思想进行求解的话。
-1.找出基线条件。继续简化问题,可以容易的得出,其最小单元为
∅
,
n
o
e
l
m
e
n
t
\empty,no \text{ } elment
∅,no elment或者
α
,
o
n
e
e
l
m
e
n
t
\alpha,one \text{ } elment
α,one elment。
-2.缩小规模。
s
u
m
(
[
1
2
3
]
)
sum([1 \text{ } 2 \text{ } 3])
sum([1 2 3])和
1
+
s
u
m
(
[
2
3
]
)
1+sum([2\text{ }3])
1+sum([2 3])相比,显然是后者可以达到缩小规模的效果。所以,可以应用此种方法进行解决本Case。
4.2 快速排序
本节可接3.27节选择排序之后。两者相较,快速排序更块得多。继续Case2讨论。
在Case2中,建立了最小单元,也是根本不需要进行排序得数组。因此,在这种情况下,只需要原样返回数组(不用排序)。
def quicksort(array):
if len(array) < 2:
return array
但针对多元素数组而言,就会变得麻烦了许多。引入快速排序的路线:
-1.在数组中选择一个元素,称为基准值(pivot);
-2.找出比基准值小、和比基准值大的元素,这一动作称为分区(partitioning);
-3.虽然此时得到了三部分,[小于基准值的子数组][基准值][大于基准值的子数组],但是子数组是无序的,这里就可对子数组进行快速排序,再进行合并就可以得到一个有序数组。
quicksort([15,10] + [33] +quicksort([]))
Output:[10,15,33]
Code for 快速排序:
def quicksort(array):
if len(array) < 2:
return array # 基线条件:为空或只包含一个元素的数组是“有序”的
else:
pivot = array[0] # 递归条件
less = [i for i in array[1:] if i <= pivot]
# 由所以小于基准值的元素组成的子数组
greater = [i for i in array[1:] if i > pivot]
# 由所以大于基准值的元素组成的子数组
return quicksort(less) + [pivot] + quicksort(greater)
print(quicksort([10,5,2,3]))
print(quicksort([10,5,2,3,11,15,12]))
Output:[2, 3, 5, 10]
[2, 3, 5, 10, 11, 12, 15]
4.3 大 O O O表示法
如下给出常见的大
O
O
O运行时间。
还有一种合并排序(merge sort),其运行时间为
O
(
n
l
o
g
n
)
O(n log n)
O(nlogn),比选择排序快。快速排序根据情况不同其运行时间也不同,在平均情况下为
O
(
n
l
o
g
n
)
O(n log n)
O(nlogn),在最糟情况下为
O
(
n
2
)
O(n^2)
O(n2)。
-1.合并排序
对于如下打印列表中每个元素的简单函数。思路为遍历列表中的每个元素并将其打印出来,迭代整个列表一次,运行时间为
O
(
n
)
O(n)
O(n)。
def print_items(list):
for item in list:
print item
如使其打印每个元素前都休眠1秒钟,则修改如下。
from time import sleep
def print_items2(list):
for item in list:
sleep(1)
print item
如输入[2 4 6 8 10],则两函数的输出实际上是不同的。
print_items: 2 4 6 8 10
print_items2: 2 <sleep> 4 <sleep> 6 <sleep> 8 <sleep> 10
虽然二者在大
o
o
o表示法里速度相同,但实际上,还是print_items
更快一些。
c
c
c是算法所需的固定时间量,被称为常量。比如print_items
可以需要
10
m
s
×
n
10ms \times n
10ms×n,而print_items2
需要
1
s
×
n
1s \times n
1s×n。通常不考虑这个常量,因为如果两种算法的大
O
O
O运行时间不同,这种常量无关紧要。但有时,其影响可能很大,快速查找和合并查找就是一个case,快速查找的常量比合并查找要小,所以在运行时间都是
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)情况下,快速查找的速度要更快。(其平均情况比最糟情况更多)
-2. 平均情况和最糟情况
快速排序的性能高度依赖于所选的基准值。假设选取总是把第一个元素作为基准值,那么数组没有分为两部分,子数组中始终有一个为空,调用栈会占用很长。
假设总是选取中间元素作为基准值那么调用栈会短的多。
针对第二种假设,层数(调用栈的高度)为
O
(
l
o
g
n
)
O(logn)
O(logn),而每层需要时间为
O
(
n
)
O(n)
O(n)。则整个算法需要运行时间为
O
(
n
)
×
O
(
l
o
g
n
)
=
O
(
n
l
o
g
n
)
O(n) \times O(logn)=O(nlogn)
O(n)×O(logn)=O(nlogn),此为最佳情况(平均情况)。
在第一种假设,层数为
O
(
n
)
O(n)
O(n),则整个运行时间
O
(
n
)
×
O
(
n
)
=
O
(
n
2
)
O(n) \times O(n)=O(n^2)
O(n)×O(n)=O(n2),此为最糟情况。
只要每次都随机地选择一个数组元素作为基准值,那么快速排序的平均运行时间就为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)。
Reference
[美]Aditya Bhargava/袁国忠, 算法图解, 北京:人民邮电出版社, 2017.3.
附个人Github地址: https://github.com/shiqi0404/Algorithm_Diagram,其中包括笔记、Code还有书本pdf版。