常见的排序算法:选择排序,插入排序,冒泡排序,快速排序,堆排序,桶排序,基数排序。
一个一个来聊吧。
目录
冒泡排序:
'''
冒泡排序
它重复地遍历要排序的队列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。
这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端
'''
A = [12,5,2,4,6,2,1,26,36,30,5]
for i in range(len(A)):
for j in range(len(A)-i-1): # 控制每轮相邻比较的次数
if A[j] > A[j+1]: # 如果前面的元素的值大于后面一个元素 互换值
tmp = A[j]
A[j] = A[j+1]
A[j+1] = tmp
选择排序:
'''
选择排序
从所有序列中先找到最小的,然后放到第一个位置。之后把剩余元素中最小的,放到第二个位置 …… 以此类推
'''
for i in range(len(A)):
min_index = i
for j in range(i+1, len(A)):
if A[j] < A[min_index]:
min_index=j
tmp = A[i]
A[i] = A[min_index]
A[min_index] = tmp
这两种排序平均时间复杂度都很高,经常讨论和优化的是以下几个:
插入排序:
"""
插入排序:通过遍历,和前边元素依次比较,然后插入到合适位置
"""
def insert_sort1(A):
# 基于数组
for k in range(1, len(A)):
tmp = A[k]
j = k
while j > 0 and A[j-1] > tmp:
A[j] = A[j-1]
j -= 1
A[j] = tmp
def insert_sort2(L):
# 基于链表, 对于链表内部实现细节改日再聊
if len(L) > 1:
marker = L.first() # marker标记序列排序部分最右边的位置
while marker != L.last():
pivot = L.after(marker)
value = pivot.element()
if value > marker.element():
marker = pivot
else:
walk = marker
while walk != L.first() and L.before(walk).element() > value:
walk = L.before(walk)
L.delete(pivot)
L.add_before(walk, value)
归并排序:
说归并排序之前先聊聊分治法:
在分治法的算法设计模式当中,使用了递归的方法,递归可以十分简练的描述一个算法。主要分三步:
- 分解:输入规格小于确定的阈值(比如一个或两个元素), 我们就可以直接处理后返回答案。否则,我们把输入值分解为两个或更多的互斥子集。
- 解决子问题:递归地解决这些与子集相关的子问题。
- 合并:整理这些子问题的解,然后把它们合并成一个整体用以解决最开始的问题。
可以用一个二叉树T形象化一个递归排序算法的执行过程,称这个二叉树为归并排序树,T的每一个节点表示归并排序算法的一个递归调用。归并排序树算法的可视化容易理解分析整个归并排序的运行时间,归并排序树的高度大约是h <= logn(证明忽略)。归并排序的时间复杂度为O(nlogn) (树的每一层运行总时间为O(n), 最后乘以树的高度,至于每一层时间怎么得来,不详细描述)
"""
归并排序: 使用分治法,先拆分,然后递归解决每个小序列,最后合并得到结果。基于数组实现。
"""
def merge(S1, S2, S):
# 把两个有序序列S1, S2合并成一个有序序列S
i = j = 0
while i + j < len(S):
if j == len(S2) or (i < len(S1) and S1[i] < S2[j]):
S[i+j] = S1[i]
i += 1
else:
S[i+j] = S2[j]
j += 1
def merge_sort(S):
# 结合merge方法, 对S进行排序
n = len(S)
if n < 2:
return
mid = n // 2 # 根据mid将S一分为二
S1 = S[0:mid]
S2 = S[mid:n]
merge_sort(S1) # 对每个子序列递归调用
merge_sort(S2)
merge(S1, S2, s) # 从树的最底层开始,由下往上, 由小往大合并子序列。最后得到总序列
A = [1, 2, 4, 6, 3, 0, 2]
merge_sort(A)
以上归并排序实现是基于数组,下面换链表实现。
"""
基于链表实现归并排序
"""
def merge(S1, S2, S):
while not S1.is_empty() and not S2.is_empty():
if S1.first() < S2.first():
S.enqueue(S1.dequeue())
else:
S.enqueue(S2.dequeue())
while not S1.is_empty():
S.enqueue(S1.dequeue())
while not S2.is_empty():
S.enqueue(S2.dequeue())
def merge_sort(S):
n = len(S)
if n < 2:
retrun
S1 = LinkedQueue() # 实例化一个链表
S2 = LinkedQueue()
while len(S1) < n // 2:
S1.enqueue(S.dequeue())
while not S.is_empty():
S2.enqueue(S.dequeue())
merge_sort(S1)
merge_sort(S2)
merge(S1, S2, S)
以上两种实现都是基于递归,下面还有一种非递归方式实现归并排序,运行时间同为O(nlogn)。实际中,比递归还要快些,回避了递归调用的额外开销并且在每一级都有临时存储器。这种算法主要思想是自底向上,即对归并排序树自底向上逐层逐行执行合并。
"""
二维数组实现归非递归并排序
"""
def merge(src, result, start, inc):
# 把src[start: start+inc]和src[start+inc: start+2*inc]合并成result
end1 = start + inc
end2 = min(start = 2*inc, len(src))
x, y, z = start, start+inc, start
while x < end1 and y < end2:
if src[x] < src[y]:
result[z] = src[x]
x += 1
else:
result[z] = src[y]
y += 1
z += 1
if x < end1:
result[z:end2] = src[x:end1]
elif y < end2:
result[z:end2] = src[y:end2]
def merge_sort(S):
n = len(S)
logn = math.ceil(math.log(n, 2))
src, dest = S, [None] * n
for i in (2**k for k in range(logn)):
for j in range(0, n, 2*i):
merge(src, dest, j, i)
src, dest = dest, src
if S is not src:
S[0:n] = src[0:n]
快速排序:
同样基于分治法,不同于归并排序,快速排序的实现使用了相反的方式
- 分解:选择一个基准值,以此分成三个序列
- 解决子问题
- 按顺序合并放回
"""
基于链表实现快速排序
"""
def quick_sort(S):
n = len(S)
if n < 2:
return
p = S.first() # 以P为基准值,把序列分成三部分
L = LinkedQueue()
E = LinkedQueue()
G = LinkedQueue()
while not S.is_empty():
if S.first() < P:
L.enqueue(S.dequeue())
elif p < S.first():
G.enqueue(S.dequeue())
else:
E.enqueue(S.dequeue())
quick_sort(L)
quick_sort(G)
while not L.is_empty():
S.enqueue(L.dequeue())
while not E.is_empty():
S.enqueue(E.dequeue())
while not G.is_empty():
S.enqueue(G.dequeue())
快速排序的额外优化:就地算法, 仅仅使用少量的内存。
"""
实现就地快速排序
"""
def partition(A, p, r):
x = A[r] # 把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):
# p, r 分别为起始、终止索引
if p < r:
q = partition(A, p, r)
quickSort(A, p, q - 1)
quickSort(A, q + 1, r)
A = [2, 8, 7, 1, 3, 5, 6, 4]
quickSort(A, 0, 7)
有两点对于快速排序经常提到:
1. 与归并排序不同的是, 当序列已经是有序的,快速排序的复杂度将达到O(n**2)。在这种情况下,把最后一个数作为基准值,会产生长度为n-1的字序列L, 长度为1的子序列E和长度为0的子序列G。那么递归下去,L的长度每减1,都会产生三个类似的序列。快速排序树的高度将达到n-1。
2. 基准值的选择。盲目使用最后一个数作为基准值,使其会受到O(n**2)最坏情况的影响。那么最合适的就是取头部、中部、尾部三数的中位数,这种三位取中的启发式搜索法将更多地选择到好的基准值。
堆排序:
首先这里提到的堆是一颗二叉树,与内存堆毫无关系。详细描述堆,就得聊聊二叉树。在堆T中,对于除了根T的每个位置P, 存储在p中的键值大于或等于存储在p的父节点的键值。所以最小键总是存于树的顶部,因此命名为堆。
因为涉及到二叉树,改篇再细致描述。这里简单提下。
桶排序和基数排序:
如果一个应用程序用小的整数键、字符串、和离散范围的d元祖键对条目进行排序,那么桶排序和基数排序是很好的选择。运行时间为O(d(n+N)), [0,N-1]是整形键的范围(对于桶排序来说,d = 1)。所以效率明显高于nlogn的运行复杂度。但是篇幅有限而且思路与该篇不一致,这里简单一提。
总结
1,对于插入排序,对于小序列是非常好的选择(将近几十个元素),适用于几乎已经排好序的序列。但是对于其他情况O(n**2)的复杂度不可取。
2,对于堆排序,最坏情况下运行时间是O(nlogn),当输入的数据可以适应主存时,堆排序很容易执行。
3,对于快速排序,最坏情况时间复杂度也达到O(n**2),但是正常情况O(nlogn)的复杂度也是期待得。几十年来,快速排序是一种通用的内存排序算法的默认选择,某些方面还是优于其他排序的。
4,对于归并排序,最坏情况下时间复杂度为O(nlogn),但是对于数组的合并排序的就地操作很难,并且对于分配临时数组的额外开销无法实现最优化。但是即便如此,计算机的各级存储器结构(告诉缓存,主存储器,外部存储器)之间被分层的情况,归并排序仍然是一个优秀的算法考虑。归并排序在很长的合并流中处理数据的方法,最好地利用了在各级存储器中以块存储的所有数据,因此减少了内存交换的总数。
Python的list类中sort方法,已经成为Tim-sort的混合方法。本质上是由自下而上的归并排序,之后进入额外的插入排序。所以实际情况中混合排序的选择还是很多的。
5,对于桶排序和基数排序,使用场景和以上几种方法就不同了,就不一起讨论了。