Python实现常见的排序算法
Python实现常见的排序算法
- 前言
- 相关介绍
- 直接插入排序
-
- 基本思想
- 代码实现
- 算法分析
- 希尔排序
-
- 基本思想
- 代码实现
- 算法分析
- 冒泡排序
-
- 基本思想
- 代码实现
- 算法分析
- 快速排序
-
- 基本思想
- 代码实现
- 算法分析
- 简单选择排序
-
- 基本思想
- 代码实现
- 算法分析
- 堆排序
-
- 基本思想
- 代码实现
- 算法分析
- 归并排序
-
- 基本思想
- 代码实现
- 算法分析
- 基数排序
-
- 基本思想
- 代码实现
- 算法分析
- 参考文献
前言
本文是个人使用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]
算法分析
- 从时间来看, 排序的基本操作为: 比较两个关键字的大小和移动记录。
- 最好情况下:
- 每趟只需比较 1 次,不移动
- 总比较次数为 n-1
- 最坏情况下:第 i 趟比较i次,移动i+1次
- 比较次数:
- 移动次数:
- 若出现各种可能排列的概率相同,则可取最好情况和最坏情况的平均情况,平均情况比较次数和移动次数为 n2 / 4 n^2/4 n2/4
- 时间复杂度为 O (n2 ) O(n^2) O(n2)
- 空间复杂度为 O ( 1 ) O(1) O(1),只需要一个记录的辅助空间
- 稳定排序。
- 算法简便,且容易实现。
- 也适用于链式存储结构,只是在单链表上无需移动记录,只需修改相应的指针。
- 更适合于初始记录基本有序(正序)的情况,当初始记录无序, n较大时,此算法时间复杂度较高,不宜采用。
希尔排序
希尔排序(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 listslists=[49,38,65,97,76,13,27,49,55,4]shell_sort(lists)
[4, 13, 27, 38, 49, 49, 55, 65, 76, 97]
算法分析
- 当增量大于1时,关键字较小的记录就不是一步一步地挪动,而是跳跃式地移动,从而使得在进行最后一趟增量为1的插入排序中,序列已基本有序,只要做记录的少量比较和移动即可完成排序, 因此希尔排序的时间复杂度比直接插入排序低。
- 时间复杂度约为 O (n1.3 ) O(n^{1.3}) O(n1.3)
- 空间复杂度为 O ( 1 ) O(1) O(1)
- 记录跳跃式地移动导致排序方法是不稳定的。
- 只能用于顺序结构,不能用于链式结构。
- 增量序列可以有各种取法,但应该使增量序列中的值没有除1之外的公因子,并且最后一个增量值必须等于1。
- 记录总的比较次数和移动次数都比直接插入排序要少,n越大时,效果越明显。所以适合初始记录无序、n较大时的情况。
冒泡排序
冒泡排序(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]
算法分析
- 最好情况下:只需 1趟排序,比较次数为 n-1,不移动
- 最坏情况下:需 n-1趟排序,第i趟比较n-i次,移动3(n-i)次
- 比较次数:
- 移动次数:
- 时间复杂度为 O (n2 ) O(n^2) O(n2)
- 空间复杂度为 O ( 1 ) O(1) O(1),冒泡排序只有在两个记录交换位置时需要一个辅助空间用做暂存记录。
- 稳定排序。
- 可用于链式存储结构。
- 移动记录次数较多,算法平均时间性能比直接插入排序差。当初始记录无序,n较大时,此算法不宜采用。
快速排序
快速排序 (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 listslists=[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(nlo g 2 n) O(nlog_2 n)O(nlog2n)。
-
空间复杂度
- 快速排序是递归的,执行时需要有一个栈来存放相应的数据。最大递归调用次数与递归树的深度一致,所以最好情况下的空间复杂度为 O ( l o g 2n ) O(log_2n) O(log2n),最坏情况下为 O ( n ) O(n) O(n)。
-
记录非顺次的移动导致排序方法是不稳定的。
-
排序过程中需要定位表的下界和上界,所以适合用于顺序结构,很难用于链式结构。
-
当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]
算法分析
- 简单选择排序过程中,所需进行记录移动的次数较少。
- 最好情况(正序):不移动;
- 最坏情况(逆序):移动3(n-l)次。
- 无论记录的初始排列如何,所需进行的关键字间的比较次数相同,均为:
- 时间复杂度:O (n2 ) O(n^2) O(n2)
- 空间复杂度:O ( 1 ) O(1) O(1),同冒泡排序一样,只有在两个记录交换时需要一个辅助空间。
- 就选择排序方法本身来讲,它是一种稳定的排序方法,但在前面“选择排序基本思想”图示中,所表现出来的现象是不稳定的,这是因为上述实现选择排序的算法采用 “交换记录” 的策略所造成的,改变这个策略,可以写出不产生 ”不稳定现象” 的选择排序算法。
- 可用于链式存储结构。
- 移动记录次数较少,当每一记录占用的空间较多时,此方法比直接插入排序快。
堆排序
堆排序 (Heap Sort) 是一种树形选择排序,在排序过程中,将待排序的记录看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序的序列中选择关键字最大(或最小)的记录。
- 堆:n个元素的序列{k1 ,k2 , … ,kn } \{k_1,k_2,…,k_n\} {k1,k2,…,kn},当且仅当满足下列关系时,成为堆:
- 如果将序列看成一个完全二叉树,非终端结点的值均小于或大于左右子结点的值。
- 利用树的结构特征来描述堆,所以树只是作为堆的描述工具,堆实际是存放在线形空间中的。
- 堆顶元素(根)为最小值或最大值。
基本思想
- 将无序序列建成一个堆
- 输出堆顶的最小(大)值
- 使剩余的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]
算法分析
- 时间复杂度:O ( n l og2 n ) O(nlog_2n) O(nlog2n),堆排序的运行时间主要耗费在建初堆和调整堆时进行的反复“筛选”上。
- 空间复杂度:O ( 1 ) O(1) O(1)
- 是不稳定排序。
- 只能用于顺序结构,不能用于链式结构 。
- 初始建堆所需的比较次数较多,因此记录数较少时不宜采用。堆排序在最坏情况下时间复杂度为O ( n l og2 n ) O(nlog_2n) O(nlog2n), 相对于快速排序最坏情况下的O (n2 ) O(n^2) O(n2)而言是一个优点,当记录较多时较为高效。
归并排序
归井排序(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]
算法分析
- 时间复杂度为O ( n l og2 n ) O(nlog_2n) O(nlog2n)。
- 当有n个记录 时, 需进行 ⌈ l o g 2n ⌉ \lceil log_2n\rceil ⌈log2n⌉趟归并排序, 每一趟归并, 其关键字比较次数不超过n, 元素移动次数都是n。
- 空间复杂度
- 用顺序表实现归并排序时, 需要和待排序记录个数相等的辅助存储空间, 所以空间复杂度为 O ( n ) O(n) O(n)。
- 是稳定排序。
- 可用于链式结构, 且不需要附加存储空间,但递归实现时仍需要开辟相应的递归工作栈。
基数排序
分配类排序不需要比较关键字的大小,它是根据关键字中各位的值, 通过对待排序记录进行若干趟 “ 分配 ” 与“收集” 来实现排序的,是一种借助于多关键字排序的思想对单关键字排序的方法。基数排序(Radix Sorting)是典型的分配类排序。
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort 或 bin sort),排序的过程就是将最低位优先法用于单关键字的情况。
基本思想
代码实现
import mathdef 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 listslists=[49,38,65,97,76,13,27,49]radix_sort(lists)
[13, 27, 38, 49, 49, 65, 76, 97]
算法分析
- 时间复杂度为O ( n l ogr m ) O(nlog_rm) O(nlogrm),其中r为所采取的基数,而m为堆数
- 在某些时候,基数排序法的效率高于其它的稳定性排序法。
- 是稳定排序。
- 可用于链式结构, 也可用于顺序结构。
更多精彩内容,可点击进入Python日常小操作专栏查看
参考文献
[1] 严蔚敏,吴伟民. 数据结构(C语言版). 北京: 清华大学出版社,2020
[2] 严蔚敏,李冬梅,吴伟民. 数据结构(C语言版)(第二版). 北京: 人民邮电出版社,2021