排序
主要为自己学习排序的笔记,如有错误请大家勘正
先导概念:排序数据是由一组元素组成的表,元素由若干个数据项组成,数据项对应的值称为关键字。通俗理解这里将list称为表,将其中的数称为关键字.
- 内排序:整个排序表都放在内存中处理,排序时不涉及数据的内、外存交换。根据是否基于比较,内排序分为基于比较的:插入排序、交换排序、选择排序和归并排序;和不基于比较的基数排序。
- 外排序:排序过程中要进行数据的内、外存交换。
本文中初始待排序序列 L i s t [ 0 , 1 , 2 , … , n − 1 ] List[0,1,2,\dots,n-1] List[0,1,2,…,n−1],长度为 n n n。
1 插入排序
插入排序基本思想:每轮将一个待排序元素按其关键字大小插入已经排好的子表中的适当的位置,直到全部元素插入完为止。
1.1 直接插入排序
思路:排序过程的某一时刻被分为两个子区间
L
i
s
t
[
0
,
1
,
…
,
i
−
1
]
List[0,1,\dots,i-1]
List[0,1,…,i−1]和
L
i
s
t
[
i
,
i
+
1
,
…
,
n
−
1
]
List[i,i+1,\dots,n-1]
List[i,i+1,…,n−1]前者是已经排好序的有序区,后者是当前未排序的待排序区间,称无序区。每轮操作是将无序区的开头元素
L
i
s
t
[
i
]
List[i]
List[i]插入有序区中的适当位置,使得
L
i
s
t
[
0
,
1
,
…
,
i
]
List[0,1,\dots,i]
List[0,1,…,i]变为新的有序区。
对于每轮操作,先将
L
i
s
t
[
i
]
List[i]
List[i]暂放到
t
e
m
p
temp
temp中,
j
j
j从有序区的最后一个元素
L
i
s
t
[
i
−
1
]
List[i-1]
List[i−1]向前查找,凡是大于
t
e
m
p
temp
temp的都向后移一位,直到找到小于等于
t
e
m
p
temp
temp的
L
i
s
t
[
j
]
List[j]
List[j]将
t
e
m
p
temp
temp插入到
L
i
s
t
[
j
+
1
]
List[j+1]
List[j+1]。
- 递增排序
# 递增排序
def InsertSort(List: list)->list:
for i in range(1,len(List)):
if List[i]<List[i-1]: # 反序时
temp = List[i] # 暂存当前元素
j = i-1 # 向前遍历起始点
while j>=0 and List[j]>temp:# 向前遍历,不超过列表范围,且大于当前元素向前继续查找小于等于的元素
List[j+1] = List[j] # 大于则后移一位
j -= 1 # 继续向前查找下一位
List[j+1] = temp # 找到小于等于的第j位,插入到j+1位
return List # 返回排好序的列表
- 递减排序
# 递减排序
def InsertSort(List: list)->list:
for i in range(1,len(List)):
if List[i]>List[i-1]: # 反序时
temp = List[i] # 暂存当前元素
j = i-1 # 向前遍历起始点
while j>=0 and List[j]<temp:# 向前遍历,不超过列表范围,且小于当前元素向前继续查找大于等于的元素
List[j+1] = List[j] # 小于则后移一位
j -= 1 # 继续向前查找下一位
List[j+1] = temp # 找到大于等于的第j位,插入到j+1位
return List # 返回排好序的列表
⋅ \cdot ⋅ 时间复杂度 O ( n 2 ) O(n^2) O(n2):最好情况列表正序,时间复杂度为 O ( n ) O(n) O(n);最坏情况列表倒序,时间复杂度为 O ( n 2 ) O(n^2) O(n2);平均时间复杂度 O ( n 2 ) O(n^2) O(n2)
⋅ \cdot ⋅ 空间复杂度 O ( 1 ) O(1) O(1)
1.2折半插入排序
思路:直接插入排序采用遍历的方式查找插入点,折半插入的思路是采用二分查找的方法查找插入点,再将插入点之后的元素后移一位,并将 L i s t [ i ] List[i] List[i]插入到插入点。
- 递增排序
# 折半插入递增排序
def BinInsertSort(List: list)->list:
for i in range(1,len(List)):
if List[i]<List[i-1]:
temp = List[i]
low, high = 0, i-1
while low<=high: # high为第一个小于等于temp的值,low为第一个大于temp的值
mid = (low+high)//2
if List[mid]>temp: # 当前值大于temp,则小于等于temp的值在左侧
high = mid-1
else: # 当前值小于等于temp,则大于temp的值在右侧
low = mid+1
for j in range(i-1,high,-1):
List[j+1] = List[j]
List[high+1] = temp
return List
- 递减牌组
# 折半插入递减排序
def BinInsertSort(List: list)->list:
for i in range(1,len(List)):
if List[i]>List[i-1]:
temp = List[i]
low, high = 0, i-1
while low<=high: # high为第一个大于等于temp的值,low为第一个大于temp的值
mid = (low+high)//2
if List[mid]<temp: # 当前值小于temp,则大于等于temp的值在左侧
high = mid-1
else: # 当前值大于等于temp,则小于temp的值在右侧
low = mid+1
for j in range(i-1,high,-1):
List[j+1] = List[j]
List[high+1] = temp
return List
⋅ \cdot ⋅ 时间复杂度 O ( n 2 ) O(n^2) O(n2)
⋅ \cdot ⋅ 空间复杂度 O ( 1 ) O(1) O(1)
1.3 希尔排序
希尔排序是一种分组插入排序的方法。
思路:
- 先取一个小于 n n n的整数 d 1 d_1 d1作为第一个增量,将全部元素分为 d 1 d_1 d1组,每组相邻元素在 L i s t List List中相距 d 1 d_1 d1;
- 再对各组元素进行直接插入排序;
- 然后取第二个增量 d 2 ( d 2 < d 1 ) d_2 \; (d_2<d_1) d2(d2<d1),重复上述的分组和排序,直到增量 d t = 1 ( d t < d t − 1 < ⋯ < d 2 < d 1 ) d_t=1\;(d_t<d_{t-1}<\dots<d_2<d_1) dt=1(dt<dt−1<⋯<d2<d1);
- 所有元素被分为一组后再进行一次直接插入排序,从而使所有元素有序。
最常见 d 1 = n 2 , d i + 1 = ⌊ d i 2 ⌋ d_1=\frac{n}{2},\;d_{i+1}=\left\lfloor{\frac{d_i}{2}}\right\rfloor d1=2n,di+1=⌊2di⌋,直到 d t = 0 d_t=0 dt=0。
- 递增排序
# 希尔排序递增
def ShellSort(List: list)->list:
d = len(List)//2
while d>0: # d未到达1时一直改变增量进行排序
for i in range(d,len(List)): # 从分组后每组第二个元素开始遍历
if List[i]<List[i-d]: # 组内反序
temp = List[i]
j = i-d # 插入排序开始查找位置未组内前一个元素的位置
while j>=0 and List[j]>temp: # 当向前查找的都大于时继续组内查找,直到找到小于等于的
List[j+d] = List[j] # 组内大于的值都在组内后移一个
j -= d
List[j+d] = temp # 找到组内小于等于的第一个元素,并插入
d //= 2 # 更新增量
return List
- 递减排序
# 希尔排序递减
def ShellSort(List: list)->list:
d = len(List)//2
while d>0: # d未到达1时一直改变增量进行排序
for i in range(d,len(List)): # 从分组后每组第二个元素开始遍历
if List[i]>List[i-d]: # 组内反序
temp = List[i]
j = i-d # 插入排序开始查找位置未组内前一个元素的位置
while j>=0 and List[j]<temp: # 当向前查找的都小于时继续组内查找,直到找到大于等于的
List[j+d] = List[j] # 组内小于的值都在组内后移一个
j -= d
List[j+d] = temp # 找到组内大于等于的第一个元素,并插入
d //= 2 # 更新增量
return List
时间复杂度 O ( n 1.58 ) O(n^{1.58}) O(n1.58)
空间复杂度 O ( 1 ) O(1) O(1)
2 交换排序
交换排序的思想:两两比较待排序列表的关键字,发现这两个元素反序时进行交换,直到没有反序元素为止。
2.1 冒泡排序
思路:通过对无序区中相邻元素之间的比较和位置交换,使最小(或最大)的元素如气泡一般逐渐上浮至“水面”。每一轮从最后边开始遍历,对相邻元素进行比较,使较小元素交换到较大元素的前边,这样经过一轮排序后,最小元素就上升到最前边。以此类推,直到只剩一个元素需要冒泡,这样所有的元素都有序了。
注意:冒泡排序中如果某一轮没有出现元素交换说明所有元素都已经有序了,就可以提前停止算法。同时冒泡排序每轮产生的有序区一定是全局有序的。
- 递增排序
# 冒泡排序升序
def BubbleSort(List: list)->list:
for i in range(len(List)-1): # 要冒出到第i个位置
exchange = False # 标志,本轮是否
for j in range(len(List)-1,i,-1): # 从后向前冒泡
if List[j]<List[j-1]:
List[j], List[j-1] = List[j-1], List[j] # 存在较小的值在后边,冒泡上浮
exchange = True
if not exchange: return List # 本轮没有交换,说明排序以完成,结束算法
return List
- 递减排序
# 冒泡排序降序
def BubbleSort(List: list)->list:
for i in range(len(List)-1): # 要冒出到第i个位置
exchange = False # 标志,本轮是否
for j in range(len(List)-1,i,-1): # 从后向前冒泡
if List[j]>List[j-1]:
List[j], List[j-1] = List[j-1], List[j] # 存在较大的值在后边,冒泡上浮
exchange = True
if not exchange: return List # 本轮没有交换,说明排序以完成,结束算法
return List
这种写法是从后向前交换,那么能不能按照遍历顺序从前向后交换呢?当然是可以的,下面我们来看前向遍历交换的算法。
- 递增排序
# 冒泡排序升序,从前向后遍历
def BubbleSort(List: list)->list:
for i in range(len(List)-1): # 要冒出到第i个位置
exchange = False # 标志,本轮是否
for j in range(i+1,len(List)): # 从后向前冒泡
if List[i]>List[j]:
List[i], List[j] = List[j], List[i] # 存在较小的值在后边,冒泡上浮
exchange = True
if not exchange: return List # 本轮没有交换,说明排序以完成,结束算法
return List
- 递减排序
# 冒泡排序降序,从前向后遍历
def BubbleSort(List: list)->list:
for i in range(len(List)-1): # 要冒出到第i个位置
exchange = False # 标志,本轮是否
for j in range(i+1,len(List)): # 从后向前冒泡
if List[i]<List[j]:
List[i], List[j] = List[j], List[i] # 存在较大的值在后边,冒泡上浮
exchange = True
if not exchange: return List # 本轮没有交换,说明排序以完成,结束算法
return List
⋅ \cdot ⋅ 时间复杂度 O ( n 2 ) O(n^2) O(n2):最好情况正序排列 O ( n ) O(n) O(n),最坏情况反序排列 O ( n 2 ) O(n^2) O(n2),平均情况 O ( n 2 ) O(n^2) O(n2)
⋅ \cdot ⋅ 空间复杂度 O ( 1 ) O(1) O(1)
2.2 快速排序
快速排序是由冒泡排序改进来的,它的基本思想是:在长度大于1的列表中,取第一个元素作为基准,将基准归位(放到最终位置),将小于(或大于)基准的元素放到其前面(左子表),将大于(或小于)基准的元素放到其后面(右子表),完成一次划分;然后对左右子表重复上述过程,直到每个子表只有一个元素或者为空为止。
先列出升序和降序的最简单的一种写法:
- 递增序列
# 最简单的写法,升序
def QuickSort(List: list, left: int, right: int)->list:
if left>=right: return # 递归停止条件
base = List[left] # 以left为基准
i, j = left, right
while i<j: # 从表两端向中间搜索
while i<j and List[j]>=base:
j -= 1 # 从后向前遍历,查找一个小于基准的值
while i<j and List[i]<=base:
i += 1 # 从前向后遍历,查找一个大于基准的值
if i<j:
List[i], List[j] = List[j], List[i] # 将查找到的大小值呼唤,使其正序
List[left], List[i] = List[i], List[left] # 将小值于基准互换,使其位于基准前,此时大值在基准后
# 递归排序左子表和右子表
QuickSort(List,left,i-1) # 左子表排序
QuickSort(List,i+1,right) # 右子表排序
- 递减学列
# 最简单的写法,降序
def QuickSort(List: list, left: int, right: int)->list:
if left>=right: return # 递归停止条件
base = List[left] # 以left为基准
i, j = left, right
while i<j: # 从表两端向中间搜索
while i<j and List[j]<=base:
j -= 1 # 从后向前遍历,查找一个大于基准的值
while i<j and List[i]>=base:
i += 1 # 从前向后遍历,查找一个小于基准的值
if i<j:
List[i], List[j] = List[j], List[i] # 将查找到的大小值呼唤,使其正序
List[left], List[i] = List[i], List[left] # 将大值于基准互换,使其位于基准前,此时小值在基准后
# 递归排序左子表和右子表
QuickSort(List,left,i-1) # 左子表排序
QuickSort(List,i+1,right) # 右子表排序
第二种写法通过改进交换方式减少了移动的次数,这也是最常用的一种划分算法,默认情况下均指该算法。
- 递增排序
# 第二种划分方式的升序
def QuickSort(List: list, left: int, right: int)->list:
if left>= right: return # 递归停止条件
i, j = left, right
base = List[left] # 以left为基准
while i!=j: # 从两端交替向中间遍历,直到i==j
while j>i and List[j]>=base: # 从后向前遍历,找一个小于基准的值
j -= 1
if j>i:
List[i] = List[j] # List[j]前移覆盖List[i]
i += 1
while i<j and List[i]<=base: # 从前向后遍历,找一个大于基准的值
i += 1
if i<j:
List[j] = List[i] # List[i]后移覆盖List[j]
j -= 1
List[i] = base # 基准归位
# 递归排序左子表和右子表
QuickSort(List,left,i-1) # 左子表排序
QuickSort(List,i+1,right) # 右子表排序
- 递减排序
# 第二种划分方式的降序
def QuickSort(List: list, left: int, right: int)->list:
if left>= right: return # 递归停止条件
i, j = left, right
base = List[left] # 以left为基准
while i!=j: # 从两端交替向中间遍历,直到i==j
while j>i and List[j]<=base: # 从后向前遍历,找一个大于基准的值
j -= 1
if j>i:
List[i] = List[j] # List[j]前移覆盖List[i]
i += 1
while i<j and List[i]>=base: # 从前向后遍历,找一个小于基准的值
i += 1
if i<j:
List[j] = List[i] # List[i]后移覆盖List[j]
j -= 1
List[i] = base # 基准归位
# 递归排序左子表和右子表
QuickSort(List,left,i-1) # 左子表排序
QuickSort(List,i+1,right) # 右子表排序
时间复杂度 O ( n log 2 n ) O(n\log_2n) O(nlog2n):最好情况正序 O ( n log 2 n ) O(n\log_2n) O(nlog2n),最坏情况 O ( n 2 ) O(n^2) O(n2),平均情况 O ( n log 2 n ) O(n\log_2n) O(nlog2n)
空间复杂度 O ( log 2 n ) O(\log_2n) O(log2n):由于使用了递归,最好情况 O ( log 2 n ) O(\log_2n) O(log2n),最坏情况 O ( n ) O(n) O(n),平均情况 O ( log 2 n ) O(\log_2n) O(log2n)
3 选择排序
选择排序的基本思想是:将排序列表分为有序区和无序区,每一轮排序从无序区中选出最小元素放在有序区的最后,从而扩大有序区,直到全部元素有序位置。
3.1 简单选择排序
简单选择排序的思路是:利用最简单的逐个元素比较的方式,从无序区选出最小的元素。第 i i i轮排序开始前,有序区和无序区分别为 L i s t [ 0 , … , i − 1 ] List[0,\dots,i-1] List[0,…,i−1]和 L i s t [ i , … , n − 1 ] List[i,\dots,n-1] List[i,…,n−1],并且有序区为全局有序的。采用最简单的逐个元素比较的方法,从 L i s t [ i , … , n − 1 ] List[i,\dots,n-1] List[i,…,n−1]中选出最小的元素 L i s t [ m i n ] List[min] List[min],将其与 L i s t [ i ] List[i] List[i]交换,这样有序区变为 L i s t [ 0 , … , i ] List[0,\dots,i] List[0,…,i],无序区变为 L i s t [ i + 1 , … , n − 1 ] List[i+1,\dots,n-1] List[i+1,…,n−1]。进行 n − 1 n-1 n−1轮排序,最后剩一个元素,它一定是最大的,无需排序。
- 递增排序
# 简单选择排序,升序
def SelectSort(List: list)->list:
for i in range(len(List)-1): # 进行n-1轮排序
minj = i
for j in range(i+1,len(List)): # 查找无序区中的最小
if List[j]<List[minj]:
minj = j
if minj!=i:
List[i], List[minj] = List[minj], List[i]
- 递减排序
# 简单选择排序,降序
def SelectSort(List: list)->list:
for i in range(len(List)-1): # 进行n-1轮排序
minj = i
for j in range(i+1,len(List)): # 查找无序区中的最大
if List[j]>List[minj]:
minj = j
if minj!=i:
List[i], List[minj] = List[minj], List[i]
时间复杂度 O ( n 2 ) O(n^2) O(n2)
空间复杂度 O ( 1 ) O(1) O(1)
3.2 堆排序
堆排序利用二叉树来代替简单选择方法来查找最大或最小值,属于一种树形选择排序算法。那么什么是堆呢? n n n个关键字序列 k 0 , k 1 , … , k n − 1 k_0,k_1,\dots,k_{n-1} k0,k1,…,kn−1称为堆,当且仅当该序列满足如下性质:(1) k i ⩽ k 2 i + 1 k_i\leqslant k_{2i+1} ki⩽k2i+1且 k i ⩽ k 2 i + 2 k_i\leqslant k_{2i+2} ki⩽k2i+2;或 (2) k i ⩾ k 2 i + 1 k_i\geqslant k_{2i+1} ki⩾k2i+1且 k i ⩾ k 2 i + 2 k_i\geqslant k_{2i+2} ki⩾k2i+2( 0 ⩽ i ⩽ ⌊ n / 2 ⌋ − 1 0 \leqslant i \leqslant \left\lfloor n/2 \right\rfloor-1 0⩽i⩽⌊n/2⌋−1)。满足条件(1)的称为小根堆,小根堆中根结点都是最小的;满足条件(2)的称为大根堆,大根堆中根结点都是最大的。
- 小根堆排序有序区中的所有元素均小于无序区的所有元素;
- 大根堆排序有序区中的所有元素均大于无序区的所有元素。
下面我们以大根堆为例来说明堆排序:
1)堆排序的核心是筛选过程,用于将一颗表示待排序列表的完全二叉树调整为大根堆,调整后该完全二叉树的左右子树均为大根堆,但加上根结点后不再是大根堆(筛选条件)。
假设
R
[
l
o
w
,
…
,
h
i
g
h
]
R[low,\dots,high]
R[low,…,high]为完全二叉树,其根节点为
R
[
l
o
w
]
R[low]
R[low],最后一个叶子结点为
R
[
h
i
g
h
]
R[high]
R[high]。
- 先将 i i i指向根节点 R [ l o w ] R[low] R[low],取出根结点 t e m p ( t e m p = R [ i ] ) temp\;(temp=R[i])\; temp(temp=R[i]) j j j指向它的左孩子 ( j = 2 i + 1 ) (j=2i+1) (j=2i+1),在 j ⩽ h i g h j \leqslant high j⩽high时循环;
- 若 R [ i ] R[i] R[i]的右孩子 R [ j + 1 ] R[j+1] R[j+1]较大,那么让 j j j指向其右孩子 j = j + 1 j=j+1 j=j+1,否则 j j j不变,总之让 j j j指向 R [ i ] R[i] R[i]最大的孩子;
- 如果最大的孩子 R [ j ] R[j] R[j]比双亲结点 R [ i ] R[i] R[i]大,则将 R [ j ] R[j] R[j]移到双亲 R [ i ] R[i] R[i],这样可以破坏以 R [ j ] R[j] R[j]为子树的堆性质,于是继续筛选 R [ j ] R[j] R[j]的子树 ( i = j , j = 2 i + 1 ) (i=j,\; j=2i+1) (i=j,j=2i+1);
- 如果最大孩子 R [ j ] R[j] R[j]比双亲结点 R [ i ] R[i] R[i]小,说明 R [ i ] R[i] R[i]已经大于它所有的孩子,说明已经满足堆性质,退出循环;
- 最后使R
R
[
i
]
=
t
e
m
p
R[i]=temp
R[i]=temp,将原根结点放到最终位置。
上述筛选是从根向下进行的,称为自顶向下筛选,对应算法为:
# 定义自顶向下的筛选过程
def siftDown(List: list, low: int, high: int):
i = low
j = 2*i+1 # j是i的左孩子
temp = R[i] # 取出根结点
while j <=high:
if j<high and List[j+1]>List[j]: # 如果右孩子大则调整j到右孩子位置
j += 1
if List[j]>temp: # temp的孩子较大
List[i] = List[j] # 将j调整到双亲的位置
i, j = j, 2*i+1 # 继续向下筛选
else:
break # 如果孩子较小则筛选结束
List[i] = temp
同样也有自底向上的筛选算法
# 定义自底向上的筛选过程
def siftUp(List: list, low: int, high: int):
i = (j-1)//2
while True:
if List[j]>List[i]:
List[i], List[j] = List[j], List[i]
if i==0: break
j = i
i = (j-1)//2
2)筛选算法建立后,我们需要建立初始堆:
对于一颗完全二叉树,按层序遍历编号后,所有分支结点的编号为
0
∼
⌊
n
/
2
⌋
−
1
0 \sim \left\lfloor n/2 \right\rfloor-1
0∼⌊n/2⌋−1,其中编号为
⌊
n
/
2
⌋
∼
n
−
1
\left\lfloor n/2 \right\rfloor \sim n-1
⌊n/2⌋∼n−1的结点均为叶子结点。
从
i
=
⌊
n
/
2
⌋
−
1
i= \left\lfloor n/2 \right\rfloor-1
i=⌊n/2⌋−1到
0
0
0的顺序调用自顶向下筛选算法建堆,让大者上浮,小的下沉:
# 建立初始堆
for i in range(n//2-1,-1,-1):
siftDown(List,i,n-1)
3)堆排序
- 初始堆建立后根节点一定是最大的关键字结点,将其放在排序序列的最后,也就是将堆的根节点与最后一个叶子结点交换。
- 此时最大的元素已经归位,那么剩下待排序的列表的元素就减少了一个,即除了最后一个叶子结点,其他的需要继续排序;
- 由于根结点在交换中发生了改变,此时的二叉树不一定为堆,但是左子树和右子树依然为堆,那么就再调用一次筛选算法,就可以找到次大的元素;
- 再次执行第1步通过交换顺序,将其置于导数第二片叶结点;
- 新的无序区又减少了一个,剩下 n − 2 n-2 n−2个元素,以此类推,重复执行123步,直到完全二叉树中只剩一个结点待排序,那么该节点一定是最小的元素。
# 堆排序,升序
def HeapSort(List: list)->None:
n = len(List)
for i in range(n//2-1,-1,-1): # 从最后一个结点开始循环建立初始堆
siftDown(List,i,n-1) # 对i-- n-1进行筛选
for i in range(n-1,0,-1): # 进行n-1轮排序,每一轮排序后无序区中元素数量减少一个
List[0], List[i] = List[i], List[0] # 将根结点与最后一个叶子结点交换,此时有序区为[i,n-1]
siftDown(List,0,i-1) # 对无序区[0,i-1]继续筛选
- 下面我们再来看一下用小根堆形成降序排列的算法,同样采用自顶向下的筛选算法
# 定义自顶向下的筛选过程,小根堆
def siftDown(List: list, low: int, high: int):
i = low
j = 2*i+1 # j是i的左孩子
temp = List[i] # 取出根结点
while j <=high:
if j<high and List[j+1]<List[j]: # 如果右孩子小则调整j到右孩子位置
j += 1
if List[j]<temp: # temp的孩子较小
List[i] = List[j] # 将j调整到双亲的位置
i, j = j, 2*i+1 # 继续向下筛选
else:
break # 如果孩子较大则筛选结束
List[i] = temp
# 堆排序,降序
def HeapSort(List: list)->None:
n = len(List)
for i in range(n//2-1,-1,-1): # 从最后一个结点开始循环建立初始堆
siftDown(List,i,n-1) # 对i-- n-1进行筛选
for i in range(n-1,0,-1): # 进行n-1轮排序,每一轮排序后无序区中元素数量减少一个
List[0], List[i] = List[i], List[0] # 将根结点与最后一个叶子结点交换,此时有序区为[i,n-1]
siftDown(List,0,i-1) # 对无序区[0,i-1]继续筛选
⋅ \cdot ⋅ 时间复杂度 O ( n log 2 n ) O(n\log_2n) O(nlog2n)
⋅ \cdot ⋅ 空间复杂度 O ( 1 ) O(1) O(1)
4 归并排序
归并排序是多次将两个或两个以上的相邻有序表合并成一个新的有序表。可以分为二路、三路、多路归并排序。
4.1 二路归并排序(自底向上)
思路:将 L i s t [ 0 , 1 , … , n − 1 ] List[0,1,\dots,n-1] List[0,1,…,n−1]看成 n n n个长度为1的有序子表,然后进行两两合并,形成 ⌈ n / 2 ⌉ \left\lceil n/2 \right\rceil ⌈n/2⌉个长度为2的有序表,然后再两两合并,以此类推,直到合并为一个长度为 n n n的有序表。
- 首先我们看怎么归并两个列表:如果是升序(降序),首先创建一个两个列表长度和大小的临时列表,用于存储新列表,然后两个列表从第一个元素开始比较,将较小(大)的元素加入临时列表,然后继续遍历,直到遍历完一个列表,然后将另一个列表剩余的元素加入临时列表后边:
# 二路归并升序
def Merge(List: list, low: int, mid: int, high: int)->None:
List1 = [None]*(high-low+1) # 分配临时归并空间
i, j, k = low, mid+1, 0 # k是临时存储空间的下标,i,j分别为第一段和第二段的下标
while i<=mid and j<=high: # 第一段和第二段均遍历完时结束循环
if List[i]<=List[j]: # 将第一段的元素放入临时空间
List1[k] = List[i]
i, k = i+1, k+1
else: # 将第二段的元素放入临时空间
List1[k] = List[j]
j, k = j+1, k+1
while i<=mid: # 将第一段剩余的部分复制到临时空间
List1[k] = List[i]
i, k = i+1, k+1
while j<=high: # 将第二段剩余的部分复制到临时空间
List1[k] = List[j]
j, k = j+1, k+1
List[low:high+1] = List1[0:high-low+1] # 将临时空间复制回原序列
- 然后看一轮归并,我们需要遍历相邻两个有序列表,设当前有序列表长度为length,那么新列表长度为2*length:
# 一轮二路归并
def MergeEpoch(List: list, length: int)->None: # length为有序子段的长度
n = len(List)
i = 0
while i+2*length-1<n: # 归并length长的两个子表
Merge(List,i,i+length-1,i+2*length-1)
i += 2*length
if i+length<n: # 余下两个子表,但是最后一个子表小于length时
Merge(List,i,i+length-1,n-1) # 归并这两个表
- 最后循环归并,直到形成一个长度为n的有序列表
# 完整的归并排序
def MergeSort(List: list)->None:
length = 1 # 初始化每个元素为一个有序表
while length<len(List):
MergeEpoch(List, length)
length *= 2
- 二路归并的降序算法
# 二路归并降序
def Merge(List: list, low: int, mid: int, high: int)->None:
List1 = [None]*(high-low+1) # 分配临时归并空间
i, j, k = low, mid+1, 0 # k是临时存储空间的下标,i,j分别为第一段和第二段的下标
while i<=mid and j<=high: # 第一段和第二段均遍历完时结束循环
if List[i]>=List[j]: # 将第一段的元素放入临时空间
List1[k] = List[i]
i, k = i+1, k+1
else: # 将第二段的元素放入临时空间
List1[k] = List[j]
j, k = j+1, k+1
while i<=mid: # 将第一段剩余的部分复制到临时空间
List1[k] = List[i]
i, k = i+1, k+1
while j<=high: # 将第二段剩余的部分复制到临时空间
List1[k] = List[j]
j, k = j+1, k+1
List[low:high+1] = List1[0:high-low+1] # 将临时空间复制回原序列
# 一轮二路归并
def MergeEpoch(List: list, length: int)->None: # length为有序子段的长度
n = len(List)
i = 0
while i+2*length-1<n: # 归并length长的两个子表
Merge(List,i,i+length-1,i+2*length-1)
i += 2*length
if i+length<n: # 余下两个子表,但是最后一个子表小于length时
Merge(List,i,i+length-1,n-1) # 归并这两个表
# 完整的归并排序
def MergeSort(List: list)->None:
length = 1 # 初始化每个元素为一个有序表
while length<len(List):
MergeEpoch(List, length)
length *= 2
时间复杂度 O ( n log 2 n ) O(n\log_2n) O(nlog2n)
空间复杂度 O ( n ) O(n) O(n)
5 基数排序
前面的排序算法都是基于对列表元素的比较进行排序的,而基数排序则不进行元素大小的比较,而是通过“分配”和“收集”过程来实现排序的。
from LinkList import LinkList
def geti(key: int,r: int,i: int):
k = 0
for j in range(i+1):
k = key%r
key = key//r
return k
def RadixSort(L:LinkList, d: int, r: int):
front = [None]*r # 建立链表的队头数组
rear = [None]*r # 建立链表的队尾数组
for i in range(d): # 从低位到高位循环
for j in range(r): # 初始化各链表的首尾指针
front[j] = rear[j] = None
p = L.head.next # p指向单链表L的首结点
while p!=None: # 分配:对于原链表中的每个结点循环
k = geti(p.data,r,i) # 提取结点关键字的第i位k
if front[k]==None: # 第k个链表空时,对头队尾均指向p结点
front[k] = p
rear[k] = p
else: # 第k个链表非空时,p结点进队
rear[k] .next = p
rear[k] = p
p = p.next # 取下一个结点
t = L.head # 重新收集所有结点
for j in range(r): # 收集:对于每一个链表循环
if front[j]!=None: # 若第j个链表的第一个非空队列
t.next = front[j]
t = rear[j]
t.next = None # 尾结点的next置空
return L
6 各种内排序方法的比较和选择
7 外排序
当数据量特别大时,无法整体进行排序。因此将一部分存储在文件中,将另一部分调到内存中进行排序,这样进行多次内外村数据交换,因此称为外排序。它主要分为两个阶段:
- 生成初始归并串:将一个文件(含待排序数据)中的数据分段读入内存,每个段在内存中进行排序,并将有序数据段写到外村文件上,从而得到若干初始归并段。
- 多路归并:对这些初始归并段进行多路归并,使得有序归并段逐渐扩大,最后在外村上形成整个文件的单一归并段,从而完成整个文件的外排序。
碍于时间限制,本文都是文字和代码,希望后边有时间可以添加一些图片,尽量使得文章更加形象便于理解。