\quad \quad 排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方法 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | Θ ( n 2 ) \Theta(n^2) Θ(n2) | Θ ( n ) \Theta(n) Θ(n) | Θ ( n 2 ) \Theta(n^2) Θ(n2) | O(1) | In-place | 稳定 |
选择排序 | Θ ( n 2 ) \Theta(n^2) Θ(n2) | Θ ( n 2 ) \Theta(n^2) Θ(n2) | Θ ( n 2 ) \Theta(n^2) Θ(n2) | O(1) | In-place | 不稳定 |
插入排序 | Θ ( n 2 ) \Theta(n^2) Θ(n2) | Θ ( n ) \Theta(n) Θ(n) | Θ ( n 2 ) \Theta(n^2) Θ(n2) | O(1) | In-place | 稳定 |
希尔排序 | O(nlogn) | O( n l o g 2 n nlog^2n nlog2n) | O( n l o g 2 n nlog^2n nlog2n) | O(1) | In-place | 不稳定 |
归并排序 | Θ ( n l o g n ) \Theta(nlogn) Θ(nlogn) | Θ ( n l o g n ) \Theta(nlogn) Θ(nlogn) | Θ ( n l o g n ) \Theta(nlogn) Θ(nlogn) | O(n) | out-place | 稳定 |
快速排序 | Θ ( n l o g n ) \Theta(nlogn) Θ(nlogn) | Θ ( n l o g n ) \Theta(nlogn) Θ(nlogn) | Θ ( n 2 ) \Theta(n^2) Θ(n2) | O(logn) | In-place | 不稳定 |
堆排序 | O(nlog n) | O(nlog n) | O(nlogn) | O(1) | In-place | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | Out-place | 稳定 |
桶排序 | O(n+k) | O(n+k) | O( n 2 n^2 n2) | O(n+k) | Out-place | 稳定 |
基数排序 | O( n ∗ k n* k n∗k) | O( n ∗ k n* k n∗k) | O( n ∗ k n* k n∗k) | O(n+k) | Out-place | 稳定 |
n:数据规模
k:"桶"的个数
In-place:占用常数内存,不占用额外内存
Out-place:占用额外内存
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。
走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序对数据操作n-1轮,每轮找出一个最大(小)值。
操作指对相邻两个数比较与交换,每轮会将一个最值交换到数据列首(尾),像冒泡一样。
(1)算法步骤
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3.针对所有的元素重复以上的步骤,除了最后一个。
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
(2)动图演示
(3)伪代码
for i=1 to A.length-1
for j=A.length downto i+1
if A[j]
(4)python 代码
def bubbleSort(A):
for i in range(0, len(A)):
for j in range(1, len(A)-i):
if A[j] < A[j-1]:
A[j], A[j - 1] = A[j - 1], A[j]
return A
A=[5,2,15,6,1,3]
print( bubbleSort(A))
[1, 2, 3, 5, 6, 15]
(1)算法步骤
1.首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
2.再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3.重复第二步,直到所有元素均排序完毕。
(2)动图演示
def select_sort(A):
n=len(A)
for i in range(len(A)-1):#0到n-2
min_index=i
for j in range(i+1,n-1):
if A[j]<A[min_index]:
min_index=j
A[i],A[min_index]=A[min_index],A[i]
return A
(1)算法步骤
1.将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2.从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。
(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
(2)动图演示
(3)伪代码
(4)python 代码
def insertion_sort(A):
for j in range(1,len(A)):
key=A[j]
i=j-1
while i>=0 and A[i]>key:
A[i+1]=A[i]
i-=1
A[i+1]=key
return A
A=[5,2,4,6,1,3]
insertion_sort(A)
[1, 2, 3, 4, 5, 6]
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本,对插入排序减少移动次数优化而来。但希尔排序是非稳定排序算法。
插入排序每次插入都要移动大量数据,前后插入时的许多移动都是重复操作,若一步到位移动效率会高很多。
若序列基本有序,插入排序不必做很多移动操作,效率很高。
希尔排序将序列按固定间隔划分为多个子序列,在子序列中简单插入排序,先做远距离移动使序列基本有序;逐渐缩小间隔重复操作,最后间隔为1时即简单插入排序。
(1)算法步骤
1.选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
2.按增量序列个数 k,对序列进行 k 趟排序;
3.每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
(2)动图演示
(4)python 代码
def ShellSort(A):
def shellinsert(A,d):
n=len(A)
for i in range(d,n):
j=i-d
temp=A[i] #记录要出入的数
while(j>=0 and A[j]>temp): #从后向前,找打比其小的数的位置
A[j+d]=A[j] #向后挪动
j-=d
if j!=i-d:
A[j+d]=temp
n=len(A)
if n<=1:
return A
d=n//2
while d>=1:
shellinsert(A,d)
d=d//2
return A
A=[5,2,4,6,1,3,17,11,23,42]
ShellSort(A)
[1, 2, 3, 4, 5, 6, 11, 17, 23, 42]
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
Divide and Conquer:
1.Divide the problem into a number of subproblems that are smaller instances of the same problem.
2.Conquer the subproblems by solving them recursively. If they are small enough, solve the subproblems as base cases.
3.Combine the solutions to the subproblems into the solution for the original problem.
(1)算法步骤
1.申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
2.设定两个指针,最初位置分别为两个已经排序序列的起始位置;
3.比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
4.重复步骤 3 直到某一指针达到序列尾;
5.将另一序列剩下的所有元素直接复制到合并序列尾。
(2)动图演示
(4)python 代码
def mergeSort(A):
import math
if(len(A)<2):
return arr
middle = math.floor(len(A)/2)
left, right = A[0:middle], A[middle:]
return merge(mergeSort(left), mergeSort(right))
def merge(left,right):
result = []
while left and right:
if left[0] <= right[0]:
result.append(left.pop(0));
else:
result.append(right.pop(0));
while left:
result.append(left.pop(0));
while right:
result.append(right.pop(0));
return result
A=[5,2,4,6,1,3,17,11,23,42]
mergeSort(A)
[1, 2, 3, 4, 5, 6, 7, 8, 9]
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
1.从数列中挑出一个元素,称为 “基准”(pivot);
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
(2)动图演示
(4)python 代码
#(以最后一个数为基准)
def quick_sort_final(A,p,r):
if p>=r:
return(A)
else:
q=PARTITION(A,p,r)
quick_sort_final(A,p,q-1)
quick_sort_final(A,q+1,r)
return(A)
def PARTITION(A,p,r):
q=p#左子数组等于右子树组的边界
pivot=A[r]#基准数
for u in range(p,r):#range函数不包含右边界
if A[u]<=pivot:
A[q],A[u]=A[u],A[q]
q=q+1
A[r]=A[q]
A[q]=pivot
return(q)
#以一个随机数为基准
import random
def PARTITION(A,p,r):
m=random.randint(p,r)#randint产生一个随机数
A[m],A[r]=A[r],A[m]#将上衣程序的基准数改为随机数
q=p#左子数组等于右子树组的边界
pivot=A[r]#基准数
for u in range(p,r):#range函数不包含右边界
if A[u]<=pivot:
A[q],A[u]=A[u],A[q]
q=q+1
A[r]=A[q]
A[q]=pivot
return(q)
def quick_sort_random(A,p,r):
if p>=r:
return(A)
else:
q=PARTITION(A,p,r)
quick_sort_final(A,p,q-1)
quick_sort_final(A,q+1,r)
return(A)
A=[3,2,9,5,6,4,1]
quick_sort_random(A,0,len(A)-1)
[1, 2, 3, 4, 5, 6, 9]
(二叉)堆是一个数组,它可以被看成一个近似的完全二叉树,树上的每一个结点对应数组中的一个元素。
(二叉)堆可以分为两种形式:最大堆(大顶堆)和最小堆(小顶堆)。
最大堆:任意一个父节点都不小于左右两个孩子节点的完全二叉树,根节点上的元素最大。
最小堆:任意一个父节点都不大于左右两个孩子节点的完全二叉树,根节点上的元素最小。
在堆排序中,我们使用的是最大堆;最小堆通常用于构造优先队列
三大过程:
MAX-HEAPIFY:其时间复杂度为 O ( l g n ) O(lgn) O(lgn),它是维护最大堆性质的关键
BUILD-MAX-HEAP(建立最大堆):具有线性时间复杂度,功能是从无序的输入数据数组中构建一个最大堆
HEAPSORT(堆排序):其时间复杂度为 O ( n l g n ) O(nlgn) O(nlgn),功能是对一个数组进行原址排序。
以数组(列表)实现大顶堆即最大堆时,从上到下,从左到右编号。父节点序号为n,则左右孩子节点序号分别为2n+1、2n+2
(1)算法步骤
堆排序首先建立大顶堆(找出一个最大值),然后用最后一个叶子结点代替根节点后做大顶堆的调整(再找一个最大值),重复
(2)动图演示
(3)伪代码
# 调整堆 A:待调整序列 length: 序列长度 i:需要调整的结点
def max_heapify(A,i):
#获取左右叶子节点
largest = i
left=2*i + 1
right=2*i + 2
#执行循环操作:两个任务:1 寻找最大值的下标;2.最大值与父节点交换
if (left < Alen) and (A[left] > A[largest]):
largest = left
#当右叶子节点的下标小于序列长度 并且 右叶子节点的值大于父节点时,将右叶子节点的下标值赋值给largest
if (right < Alen) and (A[right] > A[largest]):
largest = right
#如果largest不等于i 说明当前的父节点不是最大值,需要交换值
if (largest != i):
A[i],A[largest] = A[largest],A[i]
max_heapify(A,largest)
# 构建堆
def build_max_heap(A):
import math
for i in range(math.floor(len(A)/2),-1,-1):
max_heapify(A,i)
def heapsort(A):
#i:当前堆中序列的长度.初始化为序列的长度
global Alen#Python中定义函数时,若想在函数内部对函数外的变量进行操作,就需要在函数内部声明其为global。
Alen = len(A)
#先建立大顶堆,保证最大值位于根节点;并且父节点的值大于叶子结点
build_max_heap(A)
#执行循环:1. 每次取出堆顶元素置于序列的最后(len-1,len-2,len-3...)
# 2. 调整堆,使其继续满足大顶堆的性质,注意实时修改堆中序列的长度
for i in range(len(A)-1,0,-1):
A[i],A[0] = A[0],A[i]
#堆中序列长度减1
Alen -= 1
#调整大顶堆
max_heapify(A,0)
return A
A = [9,5,6,8,2,7,3,4,1]
heapsort(A)
[1, 2, 3, 4, 5, 6, 7, 8, 9]
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。
作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
(1)算法步骤
计数排序用待排序的数值作为计数数组(列表)的下标,统计每个数值的个数,然后依次输出即可。
(2)动图演示
import numpy as np
def count_key_equal(A):
m=np.max(A)#取A的最大值
equal=np.zeros(m+1,int)#因为索引从零开始,故建立一个为m+1的全零数组,记载各个值出现的次数
for i in range(len(A)):#令key等于序列A的每一个值,equal序列相应的索引值就增加一
key=A[i]
equal[key]+=1
return equal
def count_key_less(equal,A):
m=np.max(A)
less=np.zeros(m+1,int)
for j in range(1,m+1):#range函数包头不包尾
less[j]=less[j-1]+equal[j-1]#小于数值i的有几个=小于数值i-1+等于数值i—1
return less
def rearrange(A,less):
m=np.max(A)
Next=np.zeros(m+1,int)
B=np.zeros(len(A),int)
for j in range(m+1):
Next[j]=less[j]+1
for i in range(len(A)):
key = A[i]
index = Next[key]
B[index-1]=A[i]
Next[key]+=1
return B
A=[4,1,5,0,1,6,5,1,5,3]
equal=count_key_equal(A)
less=count_key_less(equal,A)
B=rearrange(A,less)
print(B)
[0 1 1 1 3 4 5 5 5 6]
以上三个分函数结合起来
def count_sort(A):
min_num=min(A)
max_num=max(A)
count_list=[0]*(max_num-min_num+1)
print(count_list)
for i in A:
count_list[i-min_num] +=1
print(count_list)
A.clear()
for ind,i in enumerate(count_list):
while i !=0:
A.append(ind+min_num)
i-=1
return A
A=[4,1,5,0,1,6,5,1,5,3]
result=count_sort(A)
print(result)
[0, 0, 0, 0, 0, 0, 0]
[1, 3, 0, 1, 1, 3, 1]
[0, 1, 1, 1, 3, 4, 5, 5, 5, 6]
桶排序实际上是计数排序的推广,但实现上要复杂许多。
桶排序先用一定的函数关系将数据划分到不同有序的区域(桶)内,然后子数据分别在桶内排序,之后顺次输出。
当每一个不同数据分配一个桶时,也就相当于计数排序。
(2)动图演示
(3)伪代码
def BucketSort(ls):
##############桶内使用快速排序
def QuickSort(ls):
def partition(arr,left,right):
key=left #划分参考数索引,默认为第一个数,可优化
while left<right:
while left<right and arr[right]>=arr[key]:
right-=1
while left<right and arr[left]<=arr[key]:
left+=1
(arr[left],arr[right])=(arr[right],arr[left])
(arr[left],arr[key])=(arr[key],arr[left])
return left
#递归调用
def quicksort(arr,left,right):
if left>=right:
return
mid=partition(arr,left,right)
quicksort(arr,left,mid-1)
quicksort(arr,mid+1,right)
#主函数
n=len(ls)
if n<=1:
return ls
quicksort(ls,0,n-1)
return ls
######################
n=len(ls)
big=max(ls)
num=big//10+1
bucket=[]
buckets=[[] for i in range(0,num)]
for i in ls:
buckets[i//10].append(i) #划分桶
for i in buckets: #桶内排序
bucket=QuickSort(i)
arr=[]
for i in buckets:
if isinstance(i, list):
for j in i:
arr.append(j)
else:
arr.append(i)
for i in range(0,n):
ls[i]=arr[i]
return ls
A=[4,1,5,0,1,6,5,1,5,3]
BucketSort(A)
[0, 1, 1, 1, 3, 4, 5, 5, 5, 6]
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。
由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
(1)算法步骤
基数排序进行多轮按位比较排序,轮次取决于最大数据值的位数。
先按照个位比较排序,然后十位百位以此类推,优先级由低到高,这样后面的移动就不会影响前面的。
基数排序按位比较排序实质上也是一种划分,一种另类的‘桶’罢了。比如,第一轮按各个位比较,按个位大小排序分别装入10个‘桶’中,‘桶’中个位相同的数据视作相等,桶是有序的,按序输出,后面轮次接力完成排序。
(2)动图演示
(4)python 代码
'''
1.将所有代比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
2.然后,从最低位开始,依次进行排序
3.这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
'''
def radix_sort(list):
i=0#记录当前正在排哪一位,最低位为1
max_num=max(list)#最大值
j=len(str(max_num))#记录最大值的位数
while i <j:
bucket_list=[[] for _ in range(10)]#初始化桶数组
for x in list:
bucket_list[int(x/(10**i)) % 10].append(x)#找到位置放入桶数组
print("第{}轮:".format(i+1))
print(bucket_list)
list.clear()
for x in bucket_list:
for y in x:
list.append(y)
print(list)
i +=1
return list
a=[334,5,67,345,7,5345,99,4,23,78]
result=radix_sort(a)
第1轮:
[[], [], [], [23], [334, 4], [5, 345, 5345], [], [67, 7], [78], [99]]
[23, 334, 4, 5, 345, 5345, 67, 7, 78, 99]
第2轮:
[[4, 5, 7], [], [23], [334], [345, 5345], [], [67], [78], [], [99]]
[4, 5, 7, 23, 334, 345, 5345, 67, 78, 99]
第3轮:
[[4, 5, 7, 23, 67, 78, 99], [], [], [334, 345, 5345], [], [], [], [], [], []]
[4, 5, 7, 23, 67, 78, 99, 334, 345, 5345]
第4轮:
[[4, 5, 7, 23, 67, 78, 99, 334, 345], [], [], [], [], [5345], [], [], [], []]
[4, 5, 7, 23, 67, 78, 99, 334, 345, 5345]
基数排序 vs 计数排序 vs 桶排序
基数排序有两种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
基数排序:根据键值的每位数字来分配桶;
计数排序:每个桶只存储单一键值;
桶排序:每个桶存储一定范围的数值;
参考资料:动图演示