排序算法是最基础的、在程序中使用频率最高的算法之一,通过排序算法,可以培养我们看出算法中做无用功的门道。
排序算法根据时间复杂度可分为O(nlogn)、O(n2),本质上O(nlogn)的算法就是移除了O(n2)算法中的无用项。
要理解它们,关键要掌握两个计算机科学的精髓——递归和分治
选择排序:冒泡排序,比较相邻元素,保证大的在后面,时间复杂度为O(n^2)
def selection_sort(a):
for i in range(len(a) - 1, -1, -1):
for j in range(0, i):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
return a
插入排序:从后往前扫描,找到相应的位置插入,时间复杂度为O(n^2)
def insertion_sort(a):
for i in range(1, len(a)):
temp = a[i]
j = i
while j > 0 and a[j - 1] > temp:
a[j] = a[j - 1]
j -= 1
a[j] = temp
return a
上述两种排序说明效率不高的算法的主要问题是存在大量的,甚至重复的无用功,而提高算法效率,就需要分析哪些计算是不可或缺的、哪些是无用功。
归并排序、快速排序、堆排序
这里给出归并排序的代码:
def merge(left_arr, right_arr):
arr = []
while left_arr and right_arr:
if left_arr[0] <= right_arr[0]:
arr.append(left_arr.pop(0))
else:
arr.append(right_arr.pop(0))
while left_arr:
arr.append(left_arr.pop(0))
while right_arr:
arr.append(right_arr.pop(0))
return arr
def merge_sort(a):
size = len(a)
if size < 2:
return a
mid = len(a) // 2
left_a, right_a = a[0: mid], a[mid:]
return merge(merge_sort(left_a), merge_sort(right_a))
以上三种排序算法的对比:
算法 | 平均时间复杂度 | 最坏时间复杂度 | 额外空间复杂度 | 稳定性 |
---|---|---|---|---|
归并排序 | O(nlogn) | O(nlogn) | O(n) | True |
堆排序 | O(nlogn) | O(nlogn) | O(1) | False |
快速排序 | O(nlogn) | O(n^2) | O(logn) | False |
为什么归并排序要比选择排序和插入排序要快呢?因为在归并排序的合并过程中,获得当前最小的元素只需要让两个可能的最小元素进行一次比较,而在选择排序中,则需要让一个元素和几乎所有元素比较。
计算机科学领域做事的两个原则:
归并排序、桶排序、快速排序都有各自的优缺点,特别是快速排序,由于不能满足稳定性,在处理多列表格时比较麻烦。因此,今天人们对排序算法的改进大多是结合多种排序算法地思想,形成混合排序算法(Hybrid Sorting Algorithm),比如将快速排序和堆排序结合起来的内省排序(Introspective Sort,大多数标准函数库中地排序使用的算法),还有蒂姆排序(Timsort,是Java和安卓操作系统内部使用的排序算法,也是Python默认使用的排序算法)
Timsort灵活地利用了插入排序简单直观及归并排序效率高的特点,并且找到了归并排序的一些可以进一步提高的地方,即归并过程中过多地一对一比较大小。
能够在解决实际问题时自觉应用Timsort的那些原则,就有了成为3-2.5级工程师的潜力。
假定有25名短跑选手比赛争夺前三名,赛场上有五条赛道,一次可以有五名选手同时比赛。比赛并不计时,只看相应的名次。加入选手的发挥是稳定的,即A比B跑得快,B比C跑得快,A一定比C跑得快,最少需要几组比赛能决出前三名?
分析:
A 1 , A 2 , A 3 , A 4 , A 5 B 1 , B 2 , B 3 , B 4 , B 5 C 1 , C 2 , C 3 , C 4 , C 5 D 1 , D 2 , D 3 , D 4 , D 5 E 1 , E 2 , E 3 , E 4 , E 5 A_1, A_2, A_3, A_4, A_5 \\ B_1, B_2, B_3, B_4, B_5 \\ C_1, C_2, C_3, C_4, C_5 \\ D_1, D_2, D_3, D_4, D_5 \\ E_1, E_2, E_3, E_4, E_5 A1,A2,A3,A4,A5B1,B2,B3,B4,B5C1,C2,C3,C4,C5D1,D2,D3,D4,D5E1,E2,E3,E4,E5
因为要得出前三名,显然每组的后两个选手一定不会进入前三名,因此排除了共10个选手
最终只剩下 A 2 , A 3 , B 1 , B 2 , C 1 A_2, A_3, B_1, B_2, C_1 A2,A3,B1,B2,C1共5个未确定名次,再进行一组即可得出最终的前三名,因此一共需要七组比赛。
如果有N个区间 [ l 1 , r 1 ] , [ l 2 , r 2 ] , . . . , [ l N , r N ] [l_1, r_1], [l_2, r_2], ...,[l_N, r_N] [l1,r1],[l2,r2],...,[lN,rN],只要满足下面的条件我们就说这些区间是有序的:存在 x i ∈ [ l i , r i ] x_i \in [l_i, r_i] xi∈[li,ri]其中 i = 1 , 2 , . . . , N i=1, 2, ..., N i=1,2,...,N,满足 x 1 < x 2 < . . . < x N x_1 < x_2 < ... < x_N x1<x2<...<xN
比如,[1, 4]、[2, 3]和[1.5, 2.5]是有序的,因为我们可以从这三个区间中选择1.1、2.1和2.2三个数。同时[2, 3]、[1, 4]和[1.5, 2.5]也是有序的,因为我们可以选择2.1、2.2和2.4。但是[1, 2]、[2.7, 3.5]和[1.5, 2.5]不是有序的。
对于任意一组区间,如何将它们进行排序?
易得,要想保证区间是有序的,只需要保证 l i < = r j , i < j l_i <= r_j, i < j li<=rj,i<j即可,因此针对两个区间 [ l 1 , r 1 ] , [ l 2 , r 2 ] [l_1, r_1], [l_2, r_2] [l1,r1],[l2,r2]判断是否有序只需要保证 l 1 < r 2 l_1 < r_2 l1<r2即可保证两个区间是有序的,其实这里对应着六种情况(想象一下两个区间在数轴的情况,对应六种情况):
因此只有为第一种情况时,才需要交换两区间顺序。综上,针对整个区间排序可以采用归并排序的思想,只需要修改merge的部分即可,对应伪代码如下:
def interval_merge(left_interval: list, right_interval: list) -> list:
arr = []
while left_interval and right_interval:
if left_interval[0][0] > right_interval[0][1]:
arr.append(right_interval.pop(0))
else:
arr.append(left_interval.pop(0))
while left_interval:
arr.append(left_interval.pop(0))
while right_interval:
arr.append(right_interval.pop(0))
return arr
def interval_merge_sort(arr):
if len(arr) <= 1:
return arr
left = 0
right = len(arr)
mid = (left + right) // 2
return interval_merge(interval_merge_sort(arr[left: mid]), interval_merge_sort(arr[mid: right]))
已知条件如下:
根据以上四个条件,可得 l o g ( N ! ) = O ( N l o g N ) log(N!) = O(NlogN) log(N!)=O(NlogN)