用Python感受十大排序算法,首先我们对这十个排序进行分类
1 | 十大排序算法: |
非线性排序
冒泡排序
最经典的排序算法
算法思想
将数据中的每个数与相邻的数进行比较并交换,大数往上冒,小数向下沉。将每个数遍历排序一遍
算法步骤
- 取第一个数,依次与下一个数进行排序,然后与小于(或大于)自己的数交换位置直到最后一个数
- 如果未发生位置交换,说明数据有序,排序结束。如果发生了位置交换重复上一步
算法分析
时间复杂度:
- 当排序为正序时,只需要排序一次,比较n-1次,时间复杂度为
O(n)
,这是最优情况 - 当排序为逆序时,需要进行n-1次遍历,每次比较n-i次,时间复杂度为
O(n^2)
,这是最差情况
算法代码
1 | # 冒泡排序 |
快速排序
也称为分区交换排序,它采用了分治思想,是冒泡排序的改良版。冒泡排序只能比较和交换两个数据,但是快速排序可以交换和比较两个分区,其效率更高
算法思想
选取一个基准值,将待排序数据分为左(小于基准值)和右(大于基准值)两个区间,然后对两个分区的数据进行相同的操作,最终得到一组有序数据
算法步骤
- 选取待排序数据的第一个数值作为分区标准
- 遍历数组,将小于标准的放在左边,大于标准的放在右边,则中间为标准数
- 对标准数左右两个子序列分别进行(1)和(2)的操作
- 当左右序列的长度均小于等于1时,排序完成
算法分析
时间复杂度
- 如果选取的标准数位待排序数组的中位数,即每次划分后的左右子序列长度基本一致,时间复杂度为
O(n*log_2(n))
,为最好情况 - 如果待排序数组是逆序,且第一趟选取的标准数位待排序数组的最大值,经过n-1次比较后,得到一个n-1个元素的左子序列;然后第二趟选取的标准数仍然是最大值,…依次类推,最终的时间复杂度为
O(n^2)
,这是最坏的情况
算法代码
学习版:
1 | # 快速排序 |
实用版:
1 | def quick_sort(array): |
直接插入排序
直接插入法是直接排序的典型方法,毫无技术可言
算法思想
将排序数组中的数据逐一插入已排序的子序列中,从而得到一个完整的有序数组
算法步骤
- 将数组的第一个数据看成一个有序的的子序列
- 从第二个数据开始,依次与前面的有序子序列进行比较,若待插入数据array[i]大于或等于array[i-1],则原位插入;若待插入数据array[i]小于数据array[i-1],则将array[i]临时存在临时变量temp中,将有序序列中大于temp的所有数据都后移一位,然后将temp插入对应的位置
- 重复步骤(2)
算法分析
时间复杂度:
- 当数组为正序排列时,比较次数为n-1,移位次数为0,均为最小值,因此时间复杂度为
O(n)
- 当数组为逆序排列时,每次比较都需要做一次位移,此时时间复杂度为
O(n^2)
,是最差情况
空间复杂度:
O(1)
算法代码
1 | # 直接插入排序 |
希尔排序
即shell排序,也叫缩小增量排序,通过将原始列表分解为多个子列表来改进插入排序
算法排序
shell排序摒弃了直接插入排序逐一比较的区别,而是对指定距离(增量d)的元素进行比较,并不断把增量减小到1,直到排序完成
算法步骤
- 选取增量
d = n/2
- 将排序数组分成d个子序列,然后分别进行直接插入排序
- 将增量取半,即
d = d/2 = n/2/2
- 重复步骤(2)和(3)
- 对整个待排序数组进行直接插入排序
算法分析
似乎希尔排序并不比直接插入排序快多少,但是我们可以注意到它的特点:
- 当d较大时,分组多,每组记录少,因此直接插入排序快
- 当d教小时,分组少,每组记录多,但是此时各组记录已经为正序,因此排序比正常的直接插入要快
实际上希尔排序相较于直接排序,主要是优化了待排序数组的初始顺序,使其达到或接近于直接插入排序的最优情况
时间复杂度:
- 最好的情况下为
O(n^1.3)
- 最差的情况仍然为
O(n^2)
算法代码
1 | # 希尔排序 |
简单选择排序
选择排序的思想和简单,每次从待排序数据中选择最小的一个放在最前面,直到将所有数据都遍历完
算法思想
遍历待排序数组并选出其中的最小数据元素,并于第一个元素交换位置,第二小的元素和第二个元素交换位置,直到剩下最后一个数据为最大数据,排序结束
算法步骤
- 将第一个位置上的元素依次与后面的元素进行比较,若前者大,则交换二者位置
- 重复步骤(1),比较第二个位置上的元素,然后比较第三个位置上的元素…直到比到倒数第二个元素
算法分析
这个算法被视作在排序算法中最差劲的算法
时间复杂度:
无论时最好的情况还是最差的情况,都需要与所有数据进行遍历,因此时间复杂度始终为
O(n^2)
算法代码
1 | # 简单选择排序 |
堆排序(这个和数据结构相关,我不是太理解)
简单排序只顾着,一次又一次的重新比对,却不对每一次的比较结果进行分析保存,所以导致效率低下,而堆排序加以改进
算法思想
首先需要直到什么是堆,堆是用数组实现的已标号的完全二叉树。接着需要引入小顶堆和大顶堆的思想:
- 如果父节点的键值均不小于子节点,则为大顶堆:
arr[i]>=arr[2*i+1] && arr[i]>=arr[2*i+2]
- 如果父节点的键值均不大于子节点,则为小顶堆:
arr[i]<=arr[2*i+1] && arr[i]<=arr[2*i+2]
也就是说,堆排序实际上就是一次又一次的将待排序数组构造成一个大顶堆,又一次次的将大顶堆的根节点(最大值)和最末尾的元素互换位置,并末尾的元素隔离,直到整个序列变得有序
算法步骤
- 将待排序数组构建成一个大顶堆(如果降序排列,则使用小顶堆)。为了构建大顶堆,我们需要从从最后一个非叶子节点开始先从上到下,再从左往右进行调整。最后一个非叶子节点的计算公式为
arr.length//2-1
- 将堆顶元素与末尾元素交换,使最大元素沉至数组末尾
- 重复步骤(1)和(2),直到整个序列都变得很有序。重新调整数组结构,使其满足大顶堆的结构
算法分析
时间复杂度:
- 最好的情况是正序,只需要进行
O(n*log_2(n)
复杂度的比较操作,而不需要移动 - 最坏的情况时间复杂度还是
O(n*log_2(n)
- 从上面可以看出待排序数据的原始分布情况堆堆排序的效率影响较少
算法代码
1 | # 堆排序 |
归并排序
归并排序是包含归并思想的排序方法,它是分治法的一个应用。分治法即“分而治之”
算法思想
先将原数组分为子序列,一生二,二生四,四生无穷,然后使每个子序列有序,再将两个有序子序列合并为一个有序序列,最终合一
算法步骤
- 将排序数组一分为二,再将两个子序列一分为二,生成两个新的待排序数组
- 重复步骤(1),直到待排序数组的长度为1
- 按原路径将长度为1的两个数合并为一个有序序列,然后一直向前合并,最终得到一个完整的有序序列
算法分析
归并排序十分高效,它的分与合的结构都是完成二叉树,而分和合的二叉树深度均为log_2(n)
,而每次分合的时间复杂度为
O(n)
,故
时间复杂度:
始终为 O(n*log_2(n))
,不论情况好坏
算法代码
1 | # 归并排序 |
线性时间非比较类排序
计数排序
一种非基于比较的排序算法,通过辅助数组来确定各元素的为最终位置。因为在排序过程中不存在元素之间的比较和交换操作,当待排序数组为整数且数组内数据范围较小时,其优势十分明显
算法思想
对待排序数组中的每一个元素分别进行计数,确定整个数组中小于当前元素的个数,计数完成后便可以按照各计数值将各元素直接存放在已排序的数组中
算法步骤
- 根据待排序序列中的最大元素和最小元素确定计数数组c的空间大小
- 统计待排序序列中的每个元素i出现的次数并存入c[i-1]
- 对c中的所有值进行累加,后者为其前面所有的值的总和
- 将每个元素放入已排序序列的第c[i]项,每放入一个元素,c[i]便减1
- 所有的元素都放入或者c[i]均为0之后,排序结束
算法分析
我们可以观察到计数排序的弊端,当最大值与最小值间的差值过大,会使开销增大,性能变差.其次当出现浮点数或其他形式时,也会有问题
时间复杂度:
第一个循环用于记录每个元素出现的次数,复杂度为
O(n)
第二个循环用于对计数数组进行累加,复杂度为
O(k),k为申请空间的大小(即差值范围),第三个循环用于反向填充已排序的数列,复杂度为O(n)
.总结可得,时间复杂度为
O(n+k)
空间复杂度:
由于我们额外申请了一个大小为k的计数数组和一个与待排序数组同样大小的排序空间,所以空间复杂度为
O(n+k)
算法代码
1 | # 计数排序 |
桶排序
这是计数排序的改进版本
算法思想
根据元素的特性将集合拆分成为多个值域,我们称之为桶,将同一值域的元素存放在同一个桶内排序使其处于有序状态.如果每个桶是有序的,则由这些桶按顺序构成的集合也必定是有序的
算法步骤
- 根据待排序序列中元素的分布特征和差值范围,确定映射函数与申请桶的个数
- 遍历序列,将每个记录放到对应的桶中
- 选取排序算法,对不为空的桶内部进行排序
- 把已排序的桶中的元素放回已经清空的原序列中
算法分析
我们可以注意到,桶排序和快速排序与计数排序的相似性。为了加深理解,我们在此进行区分:
- 快速排序是对两个分区的排序,而桶排序是多个分区
- 快速排序是原地排序方式,即在数组本身内进行排序,而桶排序则是在申请的额外操作空间进行排序
- 快速排序中的每个区间仍然是快速排序,而桶排序可以自主选择桶内的排序方式
- 计数排序的申请空间的跨度是从最小元素到最大元素,受待排序序列的范围的影响很大,而桶排序弱化了这种影响,减少了元素大小不连续时计数排序所存在的空间浪费
这里对于桶排序要抓住两个关键点:元素值域的划分和排序算法的选择
时间复杂度:
映射过程的排序复杂度为
O(n)
桶内排序算法的时间复杂度则和选择的排序复杂度相关
空间复杂度:
由于桶排序需要申请额外的空间进行分桶操作,所以空间复杂度为
O(m+n)
算法代码
1 | # 桶排序 |
基数排序
基数排序是桶排序的扩展,因此又称之为”桶子法”,它是通过键值的部分信息,将要排序的元素分配至”桶”中,以达到排序作用
算法思想
将各元素按位数切割成不同的数字,然后分别根据每个位数的比较结果进行排序
算法步骤
- 确定待排序序列的最大位数
- 将所有待排序元素统一为最大数位长度(左补0)
- 从最低位到最高位分别进行排序
- 当最高位排序完成后,原数组变成有序数列
算法分析
时间复杂度:
每一轮排序的时间复杂度为
O(n)
,而轮数由最大值的位数d决定,因此时间复杂度为
O(d*n)
空间复杂度:
需要额外申请空间k,因此空间复杂度为 O(n+k)
算法代码
1 | # 基数排序 |