本文是个人使用Python实现常见的排序算法的学习笔记,由于水平有限,难免出现错漏,敬请批评改正。
- 排序:将一组杂乱无章的数据按一定规律顺次排列起来。
- 排序的目的:便于查找。
- 内部排序:待排序记录都在内存中。(本文里的排序算法均属于内部排序)。
- 外部排序:待排序记录一部分在内存,一部分在外存。外部排序时,要将数据分批调入内存来排序,中间结果还要及时放入外存,显然外部排序要复杂得多。
- 衡量算法的好坏:
- 时间复杂度:排序速度(比较次数与移动次数)
- 空间复杂度:占内存辅助空间的大小
- 稳定性:A和B的关键字相等,排序后A、B的先后次序保持不变,则称这种排序算法是稳定的。
直接插入排序 (Straight Insertion Sort) 是一种最简单的排序方法,其基本操作是将一条记录插入到已排好序的有序表中, 从而得到一个新的、 记录数量增1的有序表。
每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。即边插入边排序,保证子序列中随时都是排好序的。
def insert_sort(lists):# 一组记录lists,初始把lists[0]第一个记录看成一个有序序列
n=len(lists) # 序列的元素个数
for i in range(1,n):# i从1开始,循环n-1次
key=lists[i] # key初始为未排序的首个元素
j=i-1 # j初始指向前面已经排好序的序列的最后一个元素
while j>=0: # 依次将当前的key拆入到已经排好序的序列中去,直到最后一个插入为止
if lists[j]>key:
lists[j+1]=lists[j]
lists[j]=key
j-=1
return lists
lists=[49,38,65,97,76,13,27,49]
insert_sort(lists)
[13, 27, 38, 49, 49, 65, 76, 97]
希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。算法思想的出发点:直接插入排序在基本有序时,效率较高;在待排序的记录个数较少时,效率较高。
先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
def shell_sort(lists):
n=len(lists)# 序列的元素个数
step=2 # 增量以step倍递减
group=n//step # group表示分割成子序列的个数,其数值上,又等价于增量d
while group>0:
for i in range(0,group): # 对每个子序列作直接插入排序
j=i+group
while j<n:
# 这一部分,和直接插入排序一样
k=j-group # k初始指向前面已经排好序的序列的最后一个元素
key=lists[j]# key初始为未排序的首个元素
while k>=0:# 依次将当前的key拆入到已经排好序的序列中去,直到最后一个插入为止
if lists[k]>key:
lists[k+group]=lists[k]
lists[k]=key
k-=group
j+=group
group//=step # 更新增量
return lists
lists=[49,38,65,97,76,13,27,49,55,4]
shell_sort(lists)
[4, 13, 27, 38, 49, 49, 55, 65, 76, 97]
冒泡排序(Bubble Sort)是一种最简单的交换排序方法,它通过两两比较相邻记录的关键字,如果发生逆序,则进行交换,从而使关键字小的记录如气泡一般逐渐往上 "漂浮"(左移),或者使关键字大的记录如石块一样逐渐向下 "坠落”(右移)。
每趟不断将记录两两比较,并按“前小后大” 规则交换。
- 优点: 每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;一旦下趟没有交换,还可提前结束排序。
def bubble_sort(lists):# 一组记录lists
n=len(lists) # 序列的元素个数
for i in range(n-1):# 循环n-1趟
for j in range(n-i-1): # i从0开始,第i+1趟时,比较n-(i+1)=n-i-1次
if lists[j]>lists[j+1]:
lists[j],lists[j+1]=lists[j+1],lists[j] # 相邻元素两两交换
return lists
lists=[49,38,65,97,76,13,27,49]
bubble_sort(lists)
[13, 27, 38, 49, 49, 65, 76, 97]
快速排序 (Quick Sort) 是由冒泡排序改进而得的。 在冒泡排序过程中, 只对相邻的两个记录进行比较, 因此每次交换两个相邻记录时只能消除一个逆序。 如果能通过两个(不相邻)记录的一次交换,消除多个逆序, 则会大大加快排序的速度。 快速排序方法中的一次交换可能消除多个逆序。
- 任取一个元素 (如第一个) 为中心
- 所有比它小的元素一律前放,比它大的元素一律后放,形成左右两个子表
- 对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个
def quick_sort(lists,left,right):
if left>=right: # 终止条件,左指针>=右指针
return lists
key=lists[left] # 待排序列中的第一个记录作为枢轴,枢轴关键字key
# 两个指针 low和 high
low=left
high=right
while left<right:
# 从表的最右侧位置依次向左搜索 ,找到第一个关键字小于枢轴关键字 key 的记录,将其移到 low 处
while left<right and lists[right]>=key:
right-=1
lists[left]=lists[right] # 交换值
# 从表的最左侧位置,依次向右搜索找到第一个关键字大于 key 的记录和枢轴记录交换。
while left<right and lists[left]<=key:
left+=1
lists[right]=lists[left] # 交换值
lists[right]=key
quick_sort(lists,low,left-1) # 递归排序左序列
quick_sort(lists,left+1,high) # 递归排序右序列
return lists
lists=[49,38,65,97,76,13,27,49]
quick_sort(lists,0,len(lists)-1)
[13, 27, 38, 49, 49, 65, 76, 97]
最好情况:每一趟排序后都能将记录序列均匀地分割成两个长度大致相等的子表,类似折半查找。在 n 个元素的序列中,对枢轴定位所需时间为 O ( n ) O(n) O(n) 。
最坏情况:在待排序序列已经排好序的情况下,其递归树成为单支树,每次划分只得到一个比上一次少一个记录的子序列。这样,必须经过n-1趟才能将所有记录定位,而且第i趟需要经过n-i次比较。这样,总的关键字比较次数为
理论上,平均情况下,快速排序的时间复杂度为 O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n)。
空间复杂度
记录非顺次的移动导致排序方法是不稳定的。
排序过程中需要定位表的下界和上界,所以适合用于顺序结构,很难用于链式结构。
当n较大时,在平均情况下快速排序是所有内部排序方法中速度最快的一种,所以其适合初始记录无序、 n较大时的情况。
简单选择排序 (Simple Selection Sort)也称作直接选择排序。
每一趟从待排序的记录中选出关键字最小的记录,按顺序放在已排 序的记录序列的最后,直到全部排完为止。
def select_sort(lists): # 一组记录lists
n=len(lists) # 序列的元素个数
for i in range(n): # 循环n-1趟,第i+1趟,i从0开始
min_loc=i # 存储最小记录位置,初始为未排序序列的首个元素
for j in range(i+1,n):# 经过n-(i+1)次比较,得到未排序序列中最小记录的位置
if lists[min_loc]>lists[j]: # 选出n-i中最小的记录
min_loc=j
lists[min_loc],lists[i]=lists[i],lists[min_loc] # 交换lists[i]和lists[min_loc]。
return lists
lists=[49,38,65,97,76,13,27,49]
select_sort(lists)
[13, 27, 38, 49, 49, 65, 76, 97]
堆排序 (Heap Sort) 是一种树形选择排序,在排序过程中,将待排序的记录看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序的序列中选择关键字最大(或最小)的记录。
- 将无序序列建成一个堆
- 输出堆顶的最小(大)值
- 使剩余的n-1个元素又调整成一个堆,则可得到n个元素的次小值
- 重复执行,得到一个有序序列
# 调整堆
def adjust_heap(lists,i,n):
lchild=2*i+1 # 左子树的结点位置
rchild=2*i+2 # 右子树的结点位置
max_loc=i # 存储堆中最大记录的位置,初始化为结点i
if i<n//2:
if lchild<n and lists[lchild]>lists[max_loc]:# 比较当前节点与左节点大小
max_loc=lchild
if rchild<n and lists[rchild]>lists[max_loc]:# 比较当前节点与右节点大小
max_loc=rchild
if max_loc!=i: # 最大记录的位置不等于初始化位置
lists[max_loc],lists[i]=lists[i],lists[max_loc] # 交换值
adjust_heap(lists,max_loc,n) # 递归调整堆
# 建初堆
def build_heap(lists,n):
for i in range(0,(n//2))[::-1]:# 倒序,[n//2-1,n//2-2,...,0],遍历n//2次
adjust_heap(lists,i,n)
# 堆排序
def heap_sort(lists):
n=len(lists) # 序列元素个数
build_heap(lists,n) # 建大根堆
for i in range(0,n)[::-1]: # 倒序,[n-1,n-2,...,0]
lists[0],lists[i]=lists[i],lists[0] # 堆顶值与堆底值交换,输出堆顶值
adjust_heap(lists,0,i) # 剩余的n-1个元素又调整成一个堆,则可得到n个元素的次小值
lists=[49,38,65,97,76,13,27,49]
heap_sort(lists)
print(lists)
[13, 27, 38, 49, 49, 65, 76, 97]
归井排序(Merging Sort)就是将两个或两个以上的有序表合并成一个有序表的过程。将两个有序表合并成一个有序表的过程称为2-路归并,2-路归并最为简单和常用。
- 初始序列看成n个有序子序列,每个子序列长度为1
- 两两合并,得到 ⌊ n / 2 ⌋ \lfloor n/2\rfloor ⌊n/2⌋个长度为2或1的有序子序列
- 再两两合并,重复直至得到一个长度为n的有序序列为止
# 将两个有序序列合并成一个有序序列
def merge(left,right):
i,j=0,0 # i指向左序列,j指向右序列
res=[] # 存储合并结果的列表,初始为空列表
while i<len(left) and j<len(right): # while执行完毕后,意味着至少一个序列已全部有序地并入res序列
if left[i]<=right[j]:
res.append(left[i])
i+=1
else:
res.append(right[j])
j+=1
# 将剩下的有序序列并入res有序序列
res+=left[i:]
res+=right[j:]
return res
# 归并排序
def merge_sort(lists):
if len(lists)<=1:
return lists
mid = len(lists)//2
left=merge_sort(lists[:mid])# 递归归并左序列
right=merge_sort(lists[mid:]) # 递归归并右序列
return merge(left,right)
lists=[49,38,65,97,76,13,27,49]
merge_sort(lists)
[13, 27, 38, 49, 49, 65, 76, 97]
分配类排序不需要比较关键字的大小,它是根据关键字中各位的值, 通过对待排序记录进行若干趟 “ 分配 ” 与“收集” 来实现排序的,是一种借助于多关键字排序的思想对单关键字排序的方法。基数排序(Radix Sorting)是典型的分配类排序。
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort 或 bin sort),排序的过程就是将最低位优先法用于单关键字的情况。
import math
def radix_sort(lists,radix=10):
k=int(math.ceil(math.log(max(lists),radix))) # Math.ceil()向上取整,k为序列元素的最大位数
buckets = [[] for i in range(radix)] #生成radix个桶(列表),buckets是radix个桶(列表)构成的列表
for i in range(1, k+1): # 遍历k次,即最大位数多少,就遍历多少次
for val in lists: #
#将val添加进相同位数的桶里面
buckets[val//(radix**(i-1)) % radix].append(val)
del lists[:] # 清空序列
#江铜里的数值串接起来
for z in buckets:
lists += z
del z[:]
return lists
lists=[49,38,65,97,76,13,27,49]
radix_sort(lists)
[13, 27, 38, 49, 49, 65, 76, 97]
更多精彩内容,可点击进入Python日常小操作专栏查看
[1] 严蔚敏,吴伟民. 数据结构(C语言版). 北京: 清华大学出版社,2020
[2] 严蔚敏,李冬梅,吴伟民. 数据结构(C语言版)(第二版). 北京: 人民邮电出版社,2021