0. 排序与搜索
排序算法(Sorting algorithm)是一种能将一串数据依照特定顺序进行排序的一种算法。
0.1 排序算法的稳定性
稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序。也就是如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。
当相等的元素是无法分辨的,比如像是整数,稳定性并不是一个问题。然而,假设以下的数对将要以它们的第一个数字来排序(升序)。
(4, 1) (3, 1) (3, 7) (5, 6)
在这个状况下,有可能产生两种不同的结果,一种是让相等键值的记录维持相对的次序,而另一种则没有:
(3, 1) (3, 7) (4, 1) (5, 6) (维持次序) -> 稳定的
(3, 7) (3, 1) (4, 1) (5, 6) (次序被改变) -> 不稳定的
这就是排序算法的稳定性最通俗易懂的解释😂
1. 冒泡排序 (Bubble Sort)
冒泡排序(英语:Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序算法的运作如下:
- 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
1.1 冒泡排序的分析
交换过程图示(第一次):
1.2 冒泡排序法动画演示
如果按照这样的思路走下去,我们需要进行 n − 1 n-1 n−1次冒泡过程,每次对应的比较计数如下:
第几次遍历 | 比较次数 |
---|---|
1 | n − 1 n-1 n−1 |
2 | n − 2 n-2 n−2 |
3 | n − 3 n-3 n−3 |
… | … |
n − 1 n-1 n−1 | 1 |
1.3 冒泡法Python程序实现
def bubble_sort(alist):
"""
线性表都可以,我们这里用顺序表
如果作用到链表上去,那么交换的就不是数据区的数据,
而是交换节点(比顺序表复杂)
"""
n = len(alist)
"""
注意索引是从0开始的,所以对于n个元素来说:
+ 第一个元素应该是0;
+ 最后一个元素应该是n-1
而我们比大小的时候,将指针放到n-2那么数就可以了,
没必要非得到n-1(可以减少部分计算量)
第1次走:0 ~ n-2 -> range(n-1) j=0
第2次走:0 ~ n-3 -> range(n-1-1) j=1
第3次走:0 ~ n-4 -> range(n-1-1-1) j=2
...
第n-1次走:0 ~ 1 -> range(n-1-j) j=n-2
第n次走:0 ~ 0 -> range(n-1-j) j=n-1
"""
for j in range(n-1): # 总共需要n-1个大循环
for i in range(n-1-j):
if alist[i] > alist[i+1]: # 前一个位置的数据比后一个数据大 -> 交换二者
alist[i], alist[i+1] = alist[i+1], alist[i]
else:
pass # 不需要交换
if __name__ == "__main__":
ls = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(ls)
bubble_sort(ls)
print(ls)
结果:
[54, 26, 93, 17, 77, 31, 44, 55, 20]
[17, 20, 26, 31, 44, 54, 55, 77, 93]
Python中的List,如果不用深拷贝则都是浅拷贝,通俗易懂的说法就是,List里面的值一动,那么不管有没有赋值给List,List里面的值也会发生变化(因为List是顺序表,动里面的值,相当于是把数据区的值改变了,而地址不发生变化)
1.4 冒泡排序法的两种代码
1.4.1 正循环
def bubble_sort(alist):
for j in range(len(alist) - 1):
for i in range(len(alist) - 1 - j):
if alist[i] > alist[i+1]:
alist[i], alist[i+1] = alist[i+1], alist[i]
for j in range(len(alist) - 1):
:j
从0开始,一直到n-2
[0, 1, 2, 3, ..., n-2]
1.4.2 逆循环
def bubble_sort(alist):
for j in range(len(alist)-1, 0, -1):
for i in range(j):
if alist[i] > alist[i+1]:
alist[i], alist[i+1] = alist[i+1], alist[i]
for j in range(len(alist)-1, 0, -1):
:j
从n-1
开始,一直到1
。
[n-1, n-2, n-3, ..., 1]
这两种方法都可以,哪个好理解就用哪个。
1.5 冒泡法时间复杂度
-
最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
-
最优时间复杂度: O ( n 2 ) O(n^2) O(n2)
因为不会停,所以仍然是 O ( n 2 ) O(n^2) O(n2)
-
稳定性:稳定
1.6 冒泡排序法的优化版
1.6.1 举例
对于这样一个顺序表[1, 2, 3, 4, 5, 6]
,即便它本身已经是有序了的,但仍需要重头往后走走一遍。时间复杂度仍是
O
(
n
2
)
O(n^2)
O(n2)
1.6.2 优化思路
在第一次排序时,如果从头走到尾都没有发生序列的交换 -> Okay,该顺序表是有序的,没必要排序!
1.6.3 代码实现
def bubble_sort(alist):
for j in range(len(alist)-1):
count = 0 # 交换计数
for i in range(len(alist)-1-j):
if alist[i] > alist[i+1]:
alist[i], alist[i+1] = alist[i+1], alist[i]
count += 1
if count == 0:
print("数据是有序的, 无须排序!")
return
if __name__ == "__main__":
ls = [1, 2, 3, 4, 5, 6]
print(ls)
bubble_sort(ls)
print(ls)
结果:
[1, 2, 3, 4, 5, 6]
数据是有序的,无须排序!
[1, 2, 3, 4, 5, 6]
1.6.4 优化冒泡法后的时间复杂度
-
最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
-
最优时间复杂度: O ( n ) O(n) O(n)
(表示遍历一次发现没有任何可以交换的元素,排序结束)
-
稳定性:稳定
2. 选择排序(Selection Sort)
2.1 选择排序的概念及定义
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 然后再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的未尾。
- 以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 n n n 个元素的表进行排序总共进行至多 n − 1 n-1 n−1 次交换。
选择排序最多执行 n − 1 n-1 n−1 次元素交换
在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
注意这里的限定条件:完全依靠交换去移动元素的排序方法中 😂
2.1 选择排序分析
2.2 选择排序动画演示(选择最小的)
2.3 选择排序代码实现
def selection_sort(alist):
"""选择排序"""
n = len(alist)
for j in range(0, n-1): # [0, n-2]
"""
j = [0, 1, 2, 3, ..., n-2]
如果从1开始,那么最后一个元素为n -> j = [1, 2, 3, ..., n]
如果从0开始,那么最后一个元素为n-1 -> j = [0, 1, 2, ..., n-1]
选择排序总共需要排n-1次即可,而j的索引是从0开始的,
即最后一个元素的索引为n-1,所以这里j的最大索引因该
为n-1-1 -> n-2,即range(0, n-1)
"""
min_idx = j # 最小值的索引
for i in range(j+1, n): # [j+1, n-1]
"""
这里 i = [1, 2, 3, 4, ..., n]
而我们是前一个数和后一个数作比较,
所以i只需取到倒数第一个元素即可
所以 i = [1, 2, 3, 4, ..., n-1],
即range(j+1, n)
"""
if alist[min_idx] > alist[i]:
min_idx = i # 不停更改min的index
# 上面循环结束 -> 找到了最小元素的索引 -> 进行元素交换
alist[j], alist[min_idx] = alist[min_idx], alist[j]
if __name__ == "__main__":
ls = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(f"before: {ls}")
selection_sort(ls)
print(f"after: {ls}")
结果:
before: [54, 26, 93, 17, 77, 31, 44, 55, 20]
after: [17, 20, 26, 31, 44, 54, 55, 77, 93]
2.4 简洁代码-选择排序
def selection_sort(alist):
n = len(alist)
for j in range(0, n-1): # j \in [0, n-2]
min_idx = j
for i in range(j+1, n): # i \in [1, n-1]
if alist[min_idx] > alist[i]:
min_idx = i
alist[j], alist[min_idx] = alist[min_idx], alist[j]
2.5 选择排序的时间复杂度
def selection_sort(alist):
n = len(alist)
for j in range(0, n-1): # j \in [0, n-2] -> O(n)
min_idx = j
for i in range(j+1, n): # i \in [1, n-1] -> O(n)
if alist[min_idx] > alist[i]:
min_idx = i
alist[j], alist[min_idx] = alist[min_idx], alist[j]
-
最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2)
-
最优时间复杂度为 O ( n 2 ) O(n^2) O(n2)
即便是一个有序的列表,选择排序还是会比较每一个元素
-
稳定性:不稳定
目前选择排序没有更优的方式来实现
2.6 选择排序不稳定证明
e.g.
升序每次选择最大的情况
Data: [26, 16, 17, 15, 26, 11, 10, 9]
如果是一个稳定的算法,那么一开始蓝色26在粉色26的前面,最后排完序后,蓝色的26也还应该在粉色26的前面。但选择排序不行,所以它是不稳定的。
3. 插入算法(Insertion Sort)
3.1 插入算法的概念
插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,
找到相应位置井插入。
插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
3.2 插入算法动画演示
3.3 插入排序分析
data = [54, 26, 93, 17, 77, 31, 44, 55, 20]
"""
核心思想:依次交换,保证前面数据的有序性
"""
No.1: [54, 26, 93, 17, 77, 31, 44, 55, 20]
No.1: [26, 54, 93, 17, 77, 31, 44, 55, 20]
No.2: [26, 54, 93, 17, 77, 31, 44, 55, 20]
No.2: [26, 54, 17, 93, 77, 31, 44, 55, 20]
No.2: [26, 17, 54, 93, 77, 31, 44, 55, 20]
No.3: [17, 26, 54, 93, 77, 31, 44, 55, 20]
No.4: [17, 26, 54, 77, 93, 31, 44, 55, 20]
No.5: [17, 26, 54, 77, 31, 93, 44, 55, 20]
No.5: [17, 26, 54, 31, 77, 93, 44, 55, 20]
No.5: [17, 26, 31, 54, 77, 93, 44, 55, 20]
No.6: [17, 26, 31, 54, 77, 44, 93, 55, 20]
No.6: [17, 26, 31, 54, 44, 77, 93, 55, 20]
No.6: [17, 26, 31, 44, 54, 77, 93, 55, 20]
No.7: [17, 26, 31, 44, 54, 77, 55, 93, 20]
No.7: [17, 26, 31, 44, 54, 55, 77, 93, 20]
No.8: [17, 26, 31, 44, 54, 55, 77, 20, 93]
No.8: [17, 26, 31, 44, 54, 55, 20, 77, 93]
No.8: [17, 26, 31, 44, 54, 20, 55, 77, 93]
No.8: [17, 26, 31, 44, 20, 54, 55, 77, 93]
No.8: [17, 26, 31, 20, 44, 54, 55, 77, 93]
No.8: [17, 26, 20, 31, 44, 54, 55, 77, 93]
No.8: [17, 20, 26, 31, 44, 54, 55, 77, 93]
3.4 选择排序和插入排序的区别
选择排序和插入排序都将序列看成了两部分,如下所示:
- 选择排序是对后面的无序数据选择最小(最大)的,然后按顺序放置 -> 保证前面的是有序
- 插入排序,直接取后面的一个数,保证前面的是有序
二者有一些类似,但是对后面无序数据的处理方式是不同的。
3.5 插入算法代码实现
3.5.1 第一种方法
def insertion_sort(alist):
"""插入排序"""
n = len(alist)
for j in range(1, n): # j \in [1, n-1]
"""
外层循环:从右边的无序序列中取出多少个元素执行内层循环
i:内层循环起始值
内层循环:执行从右边的无序序列中依次取出第一个元素,
即i位置的元素;然后将其插入到前面的正确位置中
"""
i = j # i = [1, 2, 3, ..., n-1]
while i > 0:
if alist[i] < alist[i-1]:
# 交换位置
alist[i], alist[i-1] = alist[i-1], alist[i]
i -= 1 # 让其继续让前面的数作比较
else: # >= 不用移动(循环可以退出)
break
if __name__ == "__main__":
ls = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(f"before: {ls}")
insertion_sort(ls)
print(f"after: {ls}")
结果:
before: [54, 26, 93, 17, 77, 31, 44, 55, 20]
after: [17, 20, 26, 31, 44, 54, 55, 77, 93]
3.5.2 第二种方法
def insertion_sort(alist):
# 从第二个位置,即下标为1的元素开始向前插入
for i in range(1, len(alist)): # i∈[1, n-1] -> 比较n-1次
# 从第i个元素开始向前比较,如果小于前一个元素,则交换位置
for j in range(i, 0, -1): # 逆着比较
if alist[j] < alist[j-1]:
alist[j], alist[j-1] = alist[j-1], alist[j]
if __name__ == "__main__":
ls = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(f"before: {ls}")
insertion_sort(ls)
print(f"after: {ls}")
结果:
before: [54, 26, 93, 17, 77, 31, 44, 55, 20]
after: [17, 20, 26, 31, 44, 54, 55, 77, 93]
3.6 插入排序时间复杂度
3.6.1 第一种方法的时间复杂度
def insertion_sort(alist):
n = len(alist)
for j in range(1, n): # O(n)
i = j
while i > 0: # 最坏的情况就是最后一个是最小的 -> O(n)
if alist[i] < alist[i-1]:
alist[i], alist[i-1] = alist[i-1], alist[i]
i -= 1
else:
break
- 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 最优时间复杂度: O ( n ) O(n) O(n)(升序排列,且已有序)
3.6.2 第二种方法的时间复杂度
def insertion_sort(alist):
for i in range(1, len(alist)): # O(n)
for j in range(i, 0, -1): # O(n)
if alist[j] < alist[j-1]:
alist[j], alist[j-1] = alist[j-1], alist[j]
-
最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
-
最优时间复杂度: O ( n 2 ) O(n^2) O(n2)
即便排好了,也要继续
3.7 ⭐️第二种方法的改进
def insertion_sort(alist):
# 从第二个位置,即下标为1的元素开始向前插入
for i in range(1, len(alist)): # i∈[1, n-1] -> 比较n-1次
# 从第i个元素开始向前比较,如果小于前一个元素,则交换位置
for j in range(i, 0, -1): # 逆着比较
if alist[j] < alist[j-1]:
alist[j], alist[j-1] = alist[j-1], alist[j]
else: # 倘若不成立,则立马break
break
if __name__ == "__main__":
ls = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(f"before: {ls}")
insertion_sort(ls)
print(f"after: {ls}")
3.8 小结
- 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 最优时间复杂度: O ( n ) O(n) O(n)(升序排列,且已有序)
- 稳定性:稳定
4. 快速排序(快排,Quick Sort)
4.1 快排的概念和定义
快速排序(Quick sort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
步骤为:
-
从数列中挑出一个元素,称为“基准”(pivot)
-
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基
准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
-
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子
数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的送代(iteration)中,它至少会把一个元素摆到它最后的位置去。
pivot 英
[ˈpɪvət]
美[ˈpɪvət]
n. 支点; 枢轴; 中心点; 最重要的人(或事物); 中心; 核心;
v. (使)在枢轴上旋转(或转动);
partition 英[
pɑːˈtɪʃn
] 美[pɑːrˈtɪʃn
]
n. 隔断; 分割; 隔扇; 隔板墙; 分治; 瓜分;
vt. 分割; 使分裂;
recursive 英[
rɪˈkɜːsɪv
] 美[rɪˈkɜːrsɪv
]
adj. 递归的; 循环的;
4.2 快排的动画演示
4.3 快速排序分析
之前我们将的选择排序和插入排序都是将序列划分为两部分来排序,将其中一部分的元素往另外一部分去做,保证一部分是有序的即可。
现在我们需要改变一下思路。
Data: [54, 26, 93, 17, 77, 31, 44, 55, 20]
对于上面的无序序列,我们不把它视为两部分。序列的第一个元素是54
,那我们就看一下,54放到有序序列中的位置是哪儿?——我们直接找54
的位置。
想要达到这样的效果,我们需要有两个游标(指针)。
如果我们找到了54
的位置:
如果我们按照升序去排序,那么:
- 54左边的都比54小
- 54右边的都比54大
只有满足这两个条件,54的位置才是正确的。
为了达到这样的方式,我们需要使用low
和high
游标:
low
:所经历过的元素必须全部比54小high
:所经历过的元素必须全部比54大
当二者交汇时,54的位置就确定了!
那么这个过程应该如何使用代码实现呢?
-
首先让
low
指向剩下元素的第一个,让high
指向最后一个元素 -
让
low
走(high
不动) -
26 < 54 26 < 54 26<54 -> 符合条件
-
93 > 54 93 > 54 93>54 -> 不符合条件 ->
low
停! -
让
high
走(low
停了)-
20
<
54
20 < 54
20<54 -> 不符合条件 ->
high
停
-
20
<
54
20 < 54
20<54 -> 不符合条件 ->
-
此时我们发现,
low
指向的元素比54
大,high
指向的元素比54
小 -> 交换这两个元素的位置,两个指针就可以继续走了 -
low
继续走- 17 < 54 17 < 54 17<54 -> 符合条件 -> 继续走
-
77
>
54
77 > 54
77>54 -> 不符合条件 ->
low
停!
-
high
继续走- 55 > 54 55 > 54 55>54 -> 符合条件 -> 继续走
-
44
<
54
44 < 54
44<54 -> 不符合条件 ->
high
停!
-
交换元素
-
low
继续走-
31
<
54
31 < 54
31<54 -> 符合条件 -> 继续走 ->
low
和high
重合!
-
31
<
54
31 < 54
31<54 -> 符合条件 -> 继续走 ->
-
54
去到自己的位置
此时通过54
把整个序列分成了两部分:
- 左边的比54小
- 右边的比54大
接下来再对序列的两个部分按照相同的方式去排序。
4.4 更容易理解的快排分析
4.4.1 第一次
4.4.2 第二次
4.5 快排代码实现-1
def quick_sort(alist, first: int, last: int):
"""快速排序"""
# 使用递归嵌套时,一定要终止递归的条件
if first >= last: # 当传入的first >= last时 -> 序列中只有一个元素或者first > last时 -> 退出
return
mid_value = alist[first] # 存放中间值
low = first # low指针(指向的是索引)
high = last # high指针(指向的是索引)
while low < high: # low和high没有相遇
# 先执行high -> 左移
# 当low和high不重合时且high一直>mid_value
# 将mid_value与比较值相等的情况放到一边处理
while low < high and alist[high] >= mid_value:
high -= 1 # high继续走
alist[low] = alist[high]
# 再执行low -> 右移
# 如果alist[low] == mid_value,则移到high那边处理
while low < high and alist[low] < mid_value:
low += 1
alist[high] = alist[low]
# 从循环退出时,low == high
alist[low] = mid_value # 使用low还是high都可以(此时二者相等)
"""
此时mid_value就将序列分成了两部分。
我们对这两部分仍然使用上面的代码(快排)进行排序
自然而然想到了“递归”
相当于是二分法,然后我们对分开的再使用递归
需要注意的是:
1. 一旦使用递归,我们一定要有一个退出递归的条件,当被二分的列表中元素的个数只有一个时,退出递归!
2. 我们想要递归的快排也操作原有的序列的话,就需要把原有的序列传进去,而不能使用切片后的序列传进去
"""
# quick_sort(alist[: low-1]) # 这时在新序列上进行快排,与原序列无关(并不能作用到原有的序列上)
# quick_sort(alist[low+1:]) # 这时在新序列上进行快排,与原序列无关(并不能作用到原有的序列上)
"""
所以我们需要传入原有的序列,即:
quick_sort(alist)
quick_sort(alist)
为了让快排知道要排哪些元素,我们还需要引入新的参数:
1. start:序列中的起始位置
2. end:序列中结束的位置
对于左边部分:
quick_sort(alist, 0, low-1)
quick_sort(alist, low+1, n-1)
"""
# 对low左边的序列执行快排
quick_sort(alist, first, low-1)
# 对low右边的序列执行快排
quick_sort(alist, low+1, last)
if __name__ == "__main__":
ls = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(f"before: {ls}")
quick_sort(ls, first=0, last=len(ls)-1)
print(f"after: {ls}")
4.6 快排简洁代码
def quick_sort(alist, start, end):
"""快速排序"""
# 递归的退出条件
if start >= end:
return
# 设定起始元素为要寻找位置的基准元素
mid = alist[start]
# low为序列坐标的由左向右移动的游标
low = start
# high为序列坐标的由右向左移动的游标
high = end
while low < high:
# 如果low与high未重合,high指向的元素不比基准元素小,则high向左移动
while low < high and alist[high] >= mid:
high -= 1
# 将high指向的元素放到low的位置上
alist[low] = alist[high]
# 如果low与high未重合且low指向的元素比基准元素小,则low向右移动
while low < high and alist[low] < mid:
low += 1
# 将low指向的元素放到high的位置上
alist[high] = alist[low]
# 退出循环后,low与high重合,此时所指向的位置即为基准元素的正确位置
# 将基准元素放到该位置上
alist[low] = mid
# 对基准元素左边的子序列进行快速排序
quick_sort(alist, start, low-1)
# 对基准元素右边的子序列进行快速排序
quick_sort(alist, low+1, end)
if __name__ == "__main__":
ls = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(f"before: {ls}")
quick_sort(ls, start=0, end=len(ls)-1)
print(f"after: {ls}")
4.7 时间复杂度
- 最优时间复杂度: O ( n log 2 n ) O(n\log_2^n) O(nlog2n)
- 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 稳定性:不稳定
从一开始快速排序平均需要花费 O ( n log n ) O(n\log^n) O(nlogn) 时间的描述并不明显。但是不难观察到的是分区运算,数组的元素都会在每次循环中走访过一次,使用 O ( n ) O(n) O(n) 的时间。在使用结合(concatenation)的版本中,这项运算也是 O ( n ) O(n) O(n)。
在最好的情况,每次我们运行一次分区,我们会把一个数列分为两个几近相等的片段。这个意思就是每次递归调用处理一半大小的数列。因此,在到达大小为 1 1 1 的数列前,我们只要作 log n \log^n logn 次嵌套的调用。这个意思就是调用树的深度是 O ( log n ) O(\log^n) O(logn)。
但是在同一层次结构的两个程序调用中,不会处理到原来数列的相同部分;因此,程序调用的每一层次结构总共全部仅需要 O ( n ) O(n) O(n) 的时间(每个调用有某些共同的额外耗费,但是因为在每一层次结构仅仅只有 O ( n ) O(n) O(n) 个调用,这些被归纳在 O ( n ) O(n) O(n) 系数中)。
结果是这个算法仅需使用 O ( n log 2 n ) O(n\log_2^n) O(nlog2n) 时间。
所消耗的时间从小到大:
O
(
1
)
<
O
(
log
n
)
<
O
(
n
)
<
O
(
n
log
n
)
<
O
(
n
2
)
<
O
(
2
n
)
<
O
(
n
!
)
<
O
(
n
n
)
O(1) < O(\log^n) < O(n) < O(n\log^n) < O(n^2) < O(2^n) < O(n!) < O(n^n)
O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(2n)<O(n!)<O(nn)
4.8 快排时间复杂度图解
Q:对于上面的序列,到底经过多少个 对半折分 才能拆到子序列中只有一个元素呢?
A: 2 × 2 × 2 × . . . = n 2 \times 2 \times 2 \times ... = n 2×2×2×...=n -> n log 2 n n\log_2^n nlog2n
每次二分为 log 2 n \log_2^n log2n,会经历 n n n 次,所以时间复杂度为 n log 2 n n\log_2^n nlog2n
‘red’>不稳定