Python数据结构与算法 实现八大经典排序算法

简介: 在面试题中可能会遇到排序算法,毕竟作为程序员内功心法,熟练掌握排序算法是很重要的,本文总结了八大经典排序算法的 Python 实现。排序算法是《数据结构与算法》中最基本的算法之一。排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存

一、前言


在面试题中可能会遇到排序算法,毕竟作为程序员内功心法,熟练掌握排序算法是很重要的,本文总结了八大经典排序算法的 Python 实现。排序算法是《数据结构与算法》中最基本的算法之一。排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。



名词解释:


  • n:数据规模
  • k:"桶"的个数
  • In-place:占用常数内存,不占用额外内存
  • Out-place:占用额外内存
  • 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同


关于时间复杂度:


  • 平方阶 (O(n²)) 排序

各类简单排序:直接插入、直接选择和冒泡排序。

  • 线性对数阶 (O(nlogn)) 排序

快速排序、堆排序和归并排序。

  • 线性阶 (O(n)) 排序

基数排序,此外还有桶、箱排序。


关于稳定性:


  • 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。(冒插归基)
  • 不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。(选快希堆)


二、冒泡排序

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢 “浮” 到数列的顶端。 这个算法的名字由来是因为越小的元素会经由交换慢慢 “浮” 到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名 “冒泡排序”。


算法原理:


  • 比较相邻的元素,如果第一个比第二个大,就交换他们两个。
  • 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  • 针对所有的元素重复以上的步骤,除了最后一个。
  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。


动图演示过程如下:



算法实现:


defbubble_sort(nums):
forpass_numinrange(len(nums) -1, 0, -1):   # n-1趟flag=True# 每一趟置flag为Trueforiinrange(pass_num):
ifnums[i] >nums[i+1]:
nums[i], nums[i+1] =nums[i+1], nums[i]
flag=False# 有交换:flag置为False# 每一趟结束后大的数会往后冒泡# flag为True  说明到这趟已经没有交换 提前跳出循环 提高算法效率# print(nums)ifflag:
breakreturnnumss= [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
print(s)
print(bubble_sort(s))
# 结果如下:[3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]



  • 无序表初始数据项的排列状况对冒泡排序没有影响,算法过程总需要 n-1 趟,随着趟数的增加,比对次数逐步从 n-1 减少到1,并包括可

能发生的数据项交换。比对次数是1~n-1的累加,比对的时间复杂度是O(n²)。

  • 关于交换次数,时间复杂度也是O(n2),通常每次交换包括3次赋值。最好的情况是列表在排序前已经有序,交换次数为0;最差的情况是每次比对都要进行交换,交换次数等于比对次数,平均情况则是最差情况的一半。
  • 冒泡排序通常作为时间效率较差的排序算法,来作为其它算法的对比基准。其效率主要差在每个数据项在找到其最终位置之前,必须要经过多次比对和交换,其中大部分的操作是无效的。
  • 冒泡排序的优势在于无需任何额外的存储空间开销。


三、选择排序


选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。


算法原理:


  • 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  • 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  • 重复第二步,直到所有元素均排序完毕。


动图演示如下:


算法实现:


defselect_sort(nums):
forpass_numinrange(len(nums) -1, 0, -1):   # n-1趟pos_max=0forlocationinrange(1, pass_num+1):
ifnums[location] >nums[pos_max]:
pos_max=location# 每一趟只交换一次nums[pass_num], nums[pos_max] =nums[pos_max], nums[pass_num]
returnnumss= [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
print(s)
print(select_sort(s))
# 结果如下:[3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]


  • 选择排序对冒泡排序进行了改进,保留了其基本的多趟比对思路,每趟都使当前最大项就位。
  • 但选择排序对交换进行了削减,相比起冒泡排序进行多次交换,每趟仅进行 1 次交换,记录最大项的所在位置,最后再跟本趟最后一项交换。
  • 选择排序的时间复杂度比冒泡排序稍优,比对次数不变,还是O(n²),但交换次数则减少为O(n)。


四、插入排序


插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。


算法原理:


  • 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
  • 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)


动态图演示如下:



算法实现:


definsert_sort(arr):
forindex_inrange(1, len(arr)):
position=index_current_value=arr[index_]    # 插入项# 比对 移动  直到找到第一个比它小的项whileposition>0andarr[position-1] >current_value:
arr[position] =arr[position-1]
position=position-1# 插入新项arr[position] =current_valuereturnarrs= [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
print(s)
print(insert_sort(s))
# 结果如下:[3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]


  • 插入排序的比对主要用来寻找 “新项” 的插入位置,最差情况是每趟都与子列表中所有项进行比对,总比对次数与冒泡排序相同,数量级仍是O(n²) 。
  • 最好情况,列表已经排好序的时候,每趟仅需 1 次比对,总次数是O(n)。
  • 由于移动操作仅包含1次赋值,是交换操作的1/3,所以插入排序性能会较好一些。


五、希尔排序


希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是不稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:


  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
  • 希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。


算法原理:


  • 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
  • 按增量序列个数 k,对序列进行 k 趟排序;
  • 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。


动图演示如下:



算法实现:


defshell_sort(nums):
n=len(nums)
gap=n//2# 定义增量# gap等于1的时候相当于最后一步是一插入排序whilegap>=1:
forjinrange(gap, n):
i=j# 增量的插入排序版本while (i-gap) >=0:
ifnums[i] <nums[i-gap]:
nums[i], nums[i-gap] =nums[i-gap], nums[i]
i-=gapelse:
breakgap//=2returnnumss= [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
print(shell_sort(s))
# 结果如下:# [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]


  • 粗看上去,谢尔排序以插入排序为基础,可能并不会比插入排序好,但由于每趟都使得列表更加接近有序,过程会减少很多原先需要的“无效”比对
  • 对希尔排序的详尽分析比较复杂,大致说是介于 O(n) 和 O(n²) 之间如果将间隔保持在2的k次方-1(1、3、5、7、15、31等等),谢尔排序的时间复杂度约为O(n^1.5)。


六、归并排序


归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:


  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;


和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。


算法原理:


  • 开辟内存空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  • 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  • 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  • 重复步骤 3 直到某一指针达到序列尾;
  • 将另一序列剩下的所有元素直接复制到合并序列尾。


动态图演示如下:


算法实现:


frommathimportfloordefmerge_sort(arr):
if(len(arr) <2):
returnarr# 二分middle=floor(len(arr) /2)
left, right=arr[0:middle], arr[middle:]
# 递归returnmerge(merge_sort(left), merge_sort(right))
defmerge(left, right):
result= []
# 分治whileleftandright:
ifleft[0] <=right[0]:
result.append(left.pop(0))
else:
result.append(right.pop(0))
whileleft:
result.append(left.pop(0))
whileright:
result.append(right.pop(0))
returnresults= [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
print(merge_sort(s))
# 结果如下:# [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]


七、快速排序


  • 快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n²) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
  • 快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
  • 快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。


快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好。查阅资料了解到:快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。


算法原理


  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;递归的最底部情形,是数列的大小是0或1,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。


动态图演示如下:



算法实现:


defquick_sort(arr, left=None, right=None):
left=0ifnotisinstance(left,(int, float)) elseleftright=len(arr) -1ifnotisinstance(right,(int, float)) elserightifleft<right:
partitionIndex=partition(arr, left, right)
quick_sort(arr, left, partitionIndex-1)
quick_sort(arr, partitionIndex+1, right)
returnarrdefpartition(arr, left, right):
pivot=leftindex=pivot+1i=indexwhilei<=right:
ifarr[i] <arr[pivot]:
swap(arr, i, index)
index+=1i+=1swap(arr, pivot, index-1)
returnindex-1defswap(arr, i, j):
arr[i], arr[j] =arr[j], arr[i]
s= [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
print(quick_sort(s))
# 结果如下:# [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]


八、堆排序


堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:


  • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

堆排序的平均时间复杂度为 Ο(nlogn)。


算法原理:


  • 将待排序序列构建成一个堆 H[0……n-1],根据(升序降序需求)选择大顶堆或小顶堆;
  • 把堆首(最大值)和堆尾互换;
  • 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
  • 重复步骤 2,直到堆的尺寸为1。


动态图演示如下:



算法实现:


frommathimportfloordefbuildMaxHeap(arr):
foriinrange(floor(len(arr)/2), -1, -1):
heapify(arr, i)
defheapify(arr, i):
left=2*i+1right=2*i+2largest=iifleft<arrLenandarr[left] >arr[largest]:
largest=leftifright<arrLenandarr[right] >arr[largest]:
largest=rightiflargest!=i:
swap(arr, i, largest)
heapify(arr, largest)
defswap(arr, i, j):
arr[i], arr[j] =arr[j], arr[i]
defheap_sort(arr):
globalarrLenarrLen=len(arr)
buildMaxHeap(arr)
foriinrange(len(arr)-1, 0, -1):
swap(arr, 0, i)
arrLen-=1heapify(arr, 0)
returnarrs= [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
print(heap_sort(s))
# 结果如下:# [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]


九、基数排序


基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异案例看大家发的:


  • 基数排序:根据键值的每位数字来分配桶;
  • 计数排序:每个桶只存储单一键值;
  • 桶排序:每个桶存储一定范围的数值;


基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。


算法原理:


  • 取得数组中的最大数,并获取其位数;
  • arr为原始数组,从最低位开始取每个位组成 radix 数组;
  • 对 radix 进行计数排序(利用计数排序适用于小范围数的特点);


动图演示如下:



算法实现:


defradix_sort(nums):
# 算n:为了计算最高位max_num=max(nums)
n=1whilemax_num>10**n:
n+=1forkinrange(n):
# 初始化0-9个桶来排序buckets= [[] foriinrange(10)]
forsubnuminnums:
buckets[int(subnum/ (10**k) %10)].append(subnum)
nums= [numforbucketinbucketsfornuminbucket ]
returnnumss= [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
print(radix_sort(s))
# 结果如下:# [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]


推荐阅读:


https://github.com/hustcc/JS-Sorting-Algorithm

https://blog.csdn.net/qq_36936730/article/details/104691911

https://www.cnblogs.com/huanghanyu/p/13267587.html

目录
相关文章
|
6天前
|
机器学习/深度学习 人工智能 算法
【宠物识别系统】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+图像识别
宠物识别系统,本系统使用Python作为主要开发语言,基于TensorFlow搭建卷积神经网络算法,并收集了37种常见的猫狗宠物种类数据集【'阿比西尼亚猫(Abyssinian)', '孟加拉猫(Bengal)', '暹罗猫(Birman)', '孟买猫(Bombay)', '英国短毛猫(British Shorthair)', '埃及猫(Egyptian Mau)', '缅因猫(Maine Coon)', '波斯猫(Persian)', '布偶猫(Ragdoll)', '俄罗斯蓝猫(Russian Blue)', '暹罗猫(Siamese)', '斯芬克斯猫(Sphynx)', '美国斗牛犬
58 29
【宠物识别系统】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+图像识别
|
12天前
|
存储 算法 搜索推荐
Python 中数据结构和算法的关系
数据结构是算法的载体,算法是对数据结构的操作和运用。它们共同构成了计算机程序的核心,对于提高程序的质量和性能具有至关重要的作用
|
24天前
|
机器学习/深度学习 人工智能 算法
基于Python深度学习的【垃圾识别系统】实现~TensorFlow+人工智能+算法网络
垃圾识别分类系统。本系统采用Python作为主要编程语言,通过收集了5种常见的垃圾数据集('塑料', '玻璃', '纸张', '纸板', '金属'),然后基于TensorFlow搭建卷积神经网络算法模型,通过对图像数据集进行多轮迭代训练,最后得到一个识别精度较高的模型文件。然后使用Django搭建Web网页端可视化操作界面,实现用户在网页端上传一张垃圾图片识别其名称。
72 0
基于Python深度学习的【垃圾识别系统】实现~TensorFlow+人工智能+算法网络
|
24天前
|
机器学习/深度学习 人工智能 算法
【手写数字识别】Python+深度学习+机器学习+人工智能+TensorFlow+算法模型
手写数字识别系统,使用Python作为主要开发语言,基于深度学习TensorFlow框架,搭建卷积神经网络算法。并通过对数据集进行训练,最后得到一个识别精度较高的模型。并基于Flask框架,开发网页端操作平台,实现用户上传一张图片识别其名称。
67 0
【手写数字识别】Python+深度学习+机器学习+人工智能+TensorFlow+算法模型
|
24天前
|
机器学习/深度学习 人工智能 算法
基于深度学习的【蔬菜识别】系统实现~Python+人工智能+TensorFlow+算法模型
蔬菜识别系统,本系统使用Python作为主要编程语言,通过收集了8种常见的蔬菜图像数据集('土豆', '大白菜', '大葱', '莲藕', '菠菜', '西红柿', '韭菜', '黄瓜'),然后基于TensorFlow搭建卷积神经网络算法模型,通过多轮迭代训练最后得到一个识别精度较高的模型文件。在使用Django开发web网页端操作界面,实现用户上传一张蔬菜图片识别其名称。
67 0
基于深度学习的【蔬菜识别】系统实现~Python+人工智能+TensorFlow+算法模型
|
28天前
|
算法 Python
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果;贪心算法在每一步选择局部最优解,追求全局最优;动态规划通过保存子问题的解,避免重复计算,确保全局最优。这三种算法各具特色,适用于不同类型的问题,合理选择能显著提升编程效率。
40 2
|
1月前
|
算法 测试技术 开发者
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗;代码审查通过检查源代码发现潜在问题,提高代码质量和团队协作效率。本文介绍了一些实用的技巧和工具,帮助开发者提升开发效率。
40 3
|
1月前
|
机器学习/深度学习 人工智能 算法
【车辆车型识别】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+算法模型
车辆车型识别,使用Python作为主要编程语言,通过收集多种车辆车型图像数据集,然后基于TensorFlow搭建卷积网络算法模型,并对数据集进行训练,最后得到一个识别精度较高的模型文件。再基于Django搭建web网页端操作界面,实现用户上传一张车辆图片识别其类型。
79 0
【车辆车型识别】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+算法模型
|
2月前
|
机器学习/深度学习 算法 Java
机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)
机器学习、基础算法、python常见面试题必知必答系列大全:(面试问题持续更新)
|
2月前
|
机器学习/深度学习 人工智能 算法
【玉米病害识别】Python+卷积神经网络算法+人工智能+深度学习+计算机课设项目+TensorFlow+模型训练
玉米病害识别系统,本系统使用Python作为主要开发语言,通过收集了8种常见的玉米叶部病害图片数据集('矮花叶病', '健康', '灰斑病一般', '灰斑病严重', '锈病一般', '锈病严重', '叶斑病一般', '叶斑病严重'),然后基于TensorFlow搭建卷积神经网络算法模型,通过对数据集进行多轮迭代训练,最后得到一个识别精度较高的模型文件。再使用Django搭建Web网页操作平台,实现用户上传一张玉米病害图片识别其名称。
71 0
【玉米病害识别】Python+卷积神经网络算法+人工智能+深度学习+计算机课设项目+TensorFlow+模型训练