摘要:介绍几种常用的排序算法和基本思想
排序算法大致可以从两种思路入手,一种是我们常见的基于比较的排序算法。即通过比较两数大小来得到顺序排列;另一种是通过位置,比如说把数字放到对应数组下标中,用空间换取时间来避免排序(可以证明基于排序的最优只能是 O ( n ∗ l g n ) O(n*lgn) O(n∗lgn)的时间复杂度)。
基于比较有思路最简单的冒泡排序,表现优异的归并排序和快速排序,打扑克常用的插入排序,维护排列简单的堆排序。非比较的主要介绍计数排序和基数排序。
这篇文章主要是总结一下自己对排序算法的理解,具体每个算法的实现、讲解可以参考链接。里面还带有图片,就不盗图了。
https://www.cnblogs.com/onepixel/p/7674659.html
冒泡排序适合初入门了解一下,但是算法设计中用的并不多,毕竟这个时间复杂度有点高,而且系数也不小,优点的话我的感觉就是思路简单,原址排序(在原数组上进行更改)。大致思路是从头到尾比较,如果前一个比后面这个大(大、小、加等号都可),就交换位置,这样每次把未排的最大的放到了最后。重复操作直到排序完成。冒泡排序的简单实现如下:
def BubbleSort(A):
for i in range(len(A)-1):
for j in range(len(A)-1,i,-1):
if A[j] < A[j-1]:
A[j],A[j-1] = A[j-1],A[j]
归并排序的渐进时间是 O ( n ∗ l g n ) O(n*lgn) O(n∗lgn),这点比较好,而且归并排序可以作为分治算法的学习例子,能够很好的理解分治算法的条件,步骤,关键点。但是归并排序并不是原址的。Python内置的排序算法好像就是用归并实现的,所以实际写代码的时候也蛮方便的。归并排序的大致思路是:因为排序具有最优子结构性质,把n规模的数据均分成两部分,分别递归调用后,再根据两个有序的部分通过比较合并成一大个有序的部分。难点即在于合并部分,重点的优化也在这里,分治算法的讲解可以参考我的另一篇博客。分治算法的实现如下:
#n*lgn时间对集合进行排序,稳定的
def Merge(A,p,q,r): #合并部分 pqr为列表下标
np = q - p + 1#包括q
nq = r - q
L = []
R = []
for i in range(np):
L.append(A[p+i])
for j in range(nq):
R.append(A[q+j+1])
L.append(float("inf")) #结尾插入两个标记阻止比较越界
R.append(float("inf"))
i = j = 0
for k in range(p,r+1):
if L[i] <= R[j]:
A[k] = L[i]
i += 1
else:
A[k] = R[j]
j += 1
def MergeSort(A,p,r): #归并排序主体(分治的主体)
if p < r:
q = (p+r)//2
MergeSort(A,p,q)
MergeSort(A,q+1,r)
Merge(A,p,q,r)
快速排序在实际运行过程中通常比堆排序要快,虽然它的最坏运行时间是 O ( n 2 ) O(n^2) O(n2),但期望运行时间和堆排序一样是 O ( n ∗ l g n ) O(n*lgn) O(n∗lgn)。与插入排序类似,快速排序的代码非常紧凑,运行时间中隐含的常数系数较小,而且快速排序是原址排序的,它是排序大数组的时候用的最多的算法。快速排序的思路是每次选一个基准,把比它小的放前面,大的放后面,再分别排序。可以想到基准的选择对排序的性能影响比较大,所以会出现随机快排,即基准数是所有数字中随机的一个,这样就不存在特别坏的输入序列,而最坏情况的发生概率是特别小的,可以放心使用。快速排序的实现如下:
import random
def Partition(A, p, r):
x = A[r]
i = p - 1
for j in range(p, r):
if A[j] <= x:
i = i + 1
A[i], A[j] = A[j], A[i]
A[i+1], A[r] = A[r], A[i+1]
return i+1
def QuickSort(A, p, r):
if p < r:
q = Partition(A, p, r)
QuickSort(A, p, q-1)
QuickSort(A, q+1, r)
def Random_Partition(A, p, r):
i = random.randint(p, r)
A[r], A[i] = A[i], A[r]
return Partition(A, p, r)
def Random_QuickSort(A, p, r):
if p < r:
q = Random_Partition(A, p, r)
Random_QuickSort(A, p, q-1)
Random_QuickSort(A, q+1, r)
插入排序和我们打扑克的时候有点像,每次都维护手中的牌是有序的,把接下来的这张插入到顺序合适的地方。插入排序的最坏运行时间是 O ( n 2 ) O(n^2) O(n2),但是它的内层循环非常紧凑,对于小规模输入,插入排序是非常适用的。插入排序也是一种原址排序算法,原址排序在有的时候非常需要,可以带来一定的便捷,而且省空间。插入排序的复杂度体现在查找新元素的合适位置,它总是从后往前比较找到自己的合适位置,这样从算法上分析查找过程可能就需要 O ( n ) O(n) O(n)的运行时间,这一点不是很划算。可以利用二分查找修改插入排序,达到总的最坏运行时间为 O ( n ∗ l g n ) O(n*lgn) O(n∗lgn)。算法的具体实现如下:
def InsertSort(A):
for j in range(1, len(A)):
key = A[j]#原地排序
#insert A[j] into the sorted sequence A[0]toA[j-1]
for i in range(j-1,-1,-1):
if A[i] > key:
A[i+1] = A[i]
if i == 0: #到0没法往前比了
A[i] = key
else:
A[i+1] = key
通过二分查找修改后的插入排序为:
def ModifyBinary(A,p,q,v):
n = q - p + 1
if n == 0:
return p
elif A[p+n//2] == v:
return p+1 #不稳定排序
elif A[p+n//2] < v:
if q > p + n//2 and A[p + n//2+1] < v:
return ModifyBinary(A, n//2+2, q, v)
else:
return p + n//2+1
else:
if n // 2 > 0 and A[p + n//2 - 1] > v:
return ModifyBinary(A, p, n//2-2, v)
else:
return p + n//2
def ModifyInsert(A):
for j in range(1, len(A)):
key = A[j]#原地排序
#insert A[j] into the sorted sequence A[0]toA[j-1]
A.insert(ModifyBinary(A, 0, j-1, A[j]), A[j])
A.pop(j+1)
堆排序应该是很多算法里面都需要的。尤其是优先队列的维护,每个维护操作的时间只要 O ( l g n ) O(lgn) O(lgn). 这里堆这个数据结构用的不是结构体等,就是一个数组。所以首先需要知道给定一个下标 i,如何获取它的父节点和左右孩子结点。此外,二叉堆可以分为两种形式,一种是最大堆、一种是最小堆。堆排序用的是最大堆,而优先队列通常用最小堆实现。具体的堆中的操作和堆排序算法见代码:
#heaplength = len(A) #数组长度
#heapsize = heaplength #有效数据长
#返回父节点 O(1)
def parent(i):
return i//2
#返回左孩子下标 O(1)
def left(i):
return 2*i
#返回右孩子下标 O(1)
def right(i):
return 2*i + 1
#对可能违反最大堆性质的坐标i调整以满足最大堆性质 O(lgn)
def max_heapify(A, i, heapsize):
l = left(i)
r = right(i)
if l < heapsize and A[l] > A[i]:
largest = l
else:
largest = i
if r < heapsize and A[r] > A[largest]:
largest = r
if largest != i:
A[i], A[largest] = A[largest], A[i]
max_heapify(A, largest, heapsize)
# 建立最大堆 O(n) 证明见p88
def build_max_heap(A, heaplength):
heapsize = heaplength
for i in range(heaplength//2, -1, -1):
max_heapify(A, i, heapsize)
#堆排序 O(n*lgn)
def heap_sort(A, heapsize):
build_max_heap(A, heapsize)
for i in range(heapsize-1, 0, -1):
A[0], A[i] = A[i], A[0]
heapsize -= 1
max_heapify(A, 0, heapsize)
#返回优先队列最大值
def heap_maximum(A):
return A[0]
#去掉并返回最大键字元素
def heap_extract_max(A, heapsize):
if heapsize < 1:
return -1
max = A[0]
A[1] = A[heapsize-1]
heapsize -= 1
max_heapify(A, 0, heapsize)
return max
#将x关键字值增加到k 并保持性质
def heap_increase_key(A, i, key):
if key < A[i]:
print("error")
return
A[i] = key
while i > 0 and A[parent(i)] < A[i]:
A[i], A[parent(i)] = A[parent(i)], A[i]
i = parent(i)
#插入一个元素 思路就是先加成负无穷 然后增加到k
def max_heap_insert(A, key):
A.append(-float("inf"))
heap_increase_key(A, len(A)-1, key)
A = [3, 2, 5, 6, 1]
max_heap_insert(A, 666)
heap_sort(A, len(A))
print(A)
在介绍非比较排序之前我们先探讨探讨排序算法的运行时间下界:考虑一个决策树模型。从根节点开始每个内部结点表示一次比较。左右孩子表示比较的结果后下一次进行的比较操作(不失一般性的假设元素互异)。每个叶子节点对应一个排序的结果。对于一个完整的决策树而言,应该包括 n n n个输入 n ! n! n!个排序的结果,即有 n ! n! n!个叶子节点。树的高度 h h h对应于最坏情况下的输入对应需要 h h h次比较操作。对于高度为 h h h的二叉树,叶子结点最多 2 h 2^h 2h个。所以有 n ! ≤ 2 h n!≤2^h n!≤2h. 所以有 l g n ! ≤ h lgn!≤h lgn!≤h, 即 n ∗ l g n ≤ h n*lgn≤h n∗lgn≤h. 所以基于排序的算法的时间复杂度 T ( n ) T(n) T(n)至少为 O ( h ) = O ( n ∗ l g n ) O(h)=O(n*lgn) O(h)=O(n∗lgn)。
非比较排序
计数排序是一种新思路,通过存储数字在对应数组下标位置达到排序的目的,当数据大小和数据个数规模相当的时候,计数排序能达到
O
(
n
)
O(n)
O(n)的时间复杂度。
def CountingSort(A, B, k): #A B 下标0~len(A)-1 k= len(A)+1
C = [0] * k
for j in range(len(A)):
C[A[j]] += 1 #这个大小的元素个数
for i in range(1, k): #对应的就是元素在B中应该的位置
C[i] += C[i-1]
for j in range(len(A)-1, -1, -1):
B[C[A[j]]-1] = A[j]
C[A[j]] -= 1 #减小放到前一个,因此是稳定排序
计数排序相对好理解一点,但是数字比较大的时候占用空间也比较严重,所以引出更复杂一点的基数排序。
基数排序
算法很简单,从低位到高位分别以每一位作为依据进行稳定的计数排序,若有最大数据有
d
d
d位数字,则需要运行d次。故总时间
O
(
n
∗
d
)
O(n*d)
O(n∗d). 在数据比较大的时候对每位排序起到了非常棒的效果。代码如下:
def GetNumd(num, d): #返回num的第d位数字 最低位为1
return (num//(10**(d-1)))%10
def dCountingSort(A, B, d): #A B 下标0~len(A)-1 k= len(A)+1
k = 10
C = [0] * k
for j in range(len(A)):
C[GetNumd(A[j], d)] += 1 #这个大小的元素个数
for i in range(1, k): #对应的就是元素在B中应该的位置
C[i] += C[i-1]
for j in range(len(A)-1, -1, -1):
B[C[GetNumd(A[j], d)]-1] = A[j]
C[GetNumd(A[j], d)] -= 1 #减小放到前一个,因此是稳定排序
def RadixSort(A, d):
B = A.copy()
for i in range(1, d+1):
dCountingSort(A, B, i)
A = B.copy()
return A