python的十大经典排序算法
十大经典排序算法
该文章参照菜鸟教程,也有自己的部分理解。
一、简介
排序算法是《数据结构与算法》中最基本的算法之一。
排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。用一张图概括:


关于时间复杂度:
- 平方阶 (O(n2)) 排序 各类简单排序:
- 直接插入、直接选择和冒泡排序。
- 线性对数阶 (O(nlog2n)) 排序:
- 快速排序、堆排序和归并排序。
- O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。:
- 希尔排序
- 线性阶 (O(n)) 排序:
- 基数排序,此外还有桶、箱排序。
关于稳定性:
- 稳定的排序算法:
- 冒泡排序、插入排序、归并排序和基数排序。
- 不是稳定的排序算法:
- 选择排序、快速排序、希尔排序、堆排序。
名词解释:
- n:数据规模
- k:"桶"的个数
- In-place:占用常数内存,不占用额外内存
- Out-place:占用额外内存
- 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
二、题目
题目如下,非常简单:
给定一个数组,返回该数组排序后的形式
输入:
[5,2,3,1,4]
输出:
[1,2,3,4,5]
三、python自带的sort
在python中,对于列表有自带的sort函数,也有内置函数sorted。我们可以直接使用他:
# 方法一、使用python自带的排序方法
def sort1(arr):
return sorted(arr)
if __name__ == '__main__':
arr = [2, 5, 7, 5, 3, 22, 2]
sort1(arr)
print(arr)
四、冒泡排序
冒泡排序(Bubble Sort)是排序算法中最为经典的。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
1. 算法步骤
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
2. 动图演示

3. 比较
冒牌排序的时间复杂度为O(n2)O(n^2)O(n2)。如果你的数组已经是一个排序好的数组,那他只需要从第一个开始判断,然后一直判断到第最后一个,这样重复n次。而如果你的数组刚刚好就是一个已经排序好的反序数组时,他就要从第一个开始然后每两个之间都需要进行交换位置,这样n次。但这样有一个好处就是,不管你的数组是很小的一串还是巨大的一串,时间复杂度都基本不变。
4. 核心
冒泡排序的核心在第二层,就是控制第二层从第一个元素开始到长度的第n-i次结束。
5. 代码展示
def sort2(arr):
for i in range(len(arr) - 1):
for j in range(len(arr) - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
五、选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。而他顾名思义就是选出最当前数组中最小的一个元素,然后把他放入第i个元素。
1. 算法步骤
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
2. 动图演示

3. 核心
选择排序的核心在第二层,他控制循环从当前第i+1个元素到最后一个元素。(需要标记min_index)
4. 代码展示
def sort3(arr):
for i in range(len(arr) - 1):
min_index = i
for j in range(i + 1, len(arr)):
if arr[j] < arr[min_index]:
min_index = j
if min_index != i:
arr[i], arr[min_index] = arr[miin_index], arr[i]
return arr
def sort3(arr):
# 外层循环,控制整个循环体
for i in range(len(arr) - 1):
min_index = i # 将当前的i记为默认的最小值下标
# 内层循环,查找i下标后面的最小值
for j in range(i + 1, len(arr)):
if arr[j] < arr[min_index]: # 如果下标为j的值小于下标是min_index的值,则把j存入min_index
min_index = j
# 如果min_index不等于i,说明后面存在比当前下标更小的值,否则就说明当前下标的值是最小值
if min_index != i:
arr[i], arr[min_index] = arr[min_index], arr[i]
return arr
六、插入排序
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
1. 算法步骤
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
2. 动图演示

3. 核心
选择排序的核心还是在第二层,就是需要控制从第i个元素到第一个元素进行逆序查找。
4. 代码展示
def sort4(arr):
for i in range(len(arr)):
for j in range(i, 0, -1):
if arr[j] < arr[j - 1]:
arr[j], arr[j - 1] = arr[j - 1], arr[j]
return arr
def sort4(arr):
# 外层循环控制整体循环
for i in range(len(arr)):
# 内层,从当前i一直往前判断
for j in range(i, 0, -1):
# 如果当前j小于前面的,则将两个数进行交换
if arr[j] < arr[j - 1]:
arr[j], arr[j - 1] = arr[j - 1], arr[j]
return arr
七、希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;
1. 基本思路
给定一个间隔,然后在列表上根据间隔的个数先排列,然后循环直到该间隔全部排完。等该间隔完了以后缩小间隔继续循环
细化:假设初始间隔为4,该指针从该间隔开始到数组末尾进行判断,如果该指针所指向的值小于该指针的大小减去间隔*n的值,则将两个位置进行交换。重复上诉操做,然后间隔减一变成3,继续上面的操作,直到间隔为0完成排序
2. 算法步骤
选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
按增量序列个数 k,对序列进行 k 趟排序;
每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
3. 动图演示

4. 核心
希尔排序的核心在第三层循环,如果下标j的值大于temp,则说明需要换位置,则把j+gap下标的值变成下标为j的值,下标j的位置空出来,进行下一轮判断,而接下来j需要减去gap来进入下一层循环。当跳出循环后,我们此时的j是当前空这的位置的前gap值,因此需要把temp加入的时候,需要重新加上gap。(需要设定gap)
5. 代码展示
def sort5(arr):
gap = 4
while gap > 0:
for i in range(gap, len(arr)):
temp = arr[i]
j = i - gap
while j > 0 and arr[j] > temp:
arr[j + gap] = arr[j]
j -= gap
arr[j + gap] = temp
gap -= 1
return arr
def sort5(arr):
gap = 4 # 设定间隔
while gap > 0:
# 从gap开始到结尾
for i in range(gap, len(arr)):
temp = arr[i] # 记录当前下标的值
j = i - gap # 将j设置为前一个需要查找的值下标
while j > 0 and arr[j] > temp: # 如果j大于0,并且下表j的值大于temp
arr[j + gap] = arr[j] # 把后面的值变成前面的值
j -= gap # j设置为下一个查找的值的下标
arr[j + gap] = temp # 应为跳出循环时,j是被剪过的,需要添加回去
gap -= 1 # gap自剪1,进入下一层循环
return arr
八、归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
- 自下而上的迭代;
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
1. 基本思路
先递归分解数组,再合并数组。
细化:【1.拆分数组】传入数组,获取该数组的中间位置(向下取整或者整除2),然后重复上诉操作,直至传入函数的数组达到了长度为1。【2-1.创建数组】因为每个传入的数组都被分为两个部分(前半部分和后半部分),给定两个指针,分别指向每个数组的第一个元素,然后创建一个新的数组用来存放这两个数组合并后的数组。【2-2.合并数组】对两个指针的值进行判断,那个值小,就将哪个值放入新数组,该指针往右移动一个位置。重复上述操作直至某个指针达到该指针所在列表的长度。【2-3.压入多余数组】因为会存在一个数组已经全部压入新数组了但是另一个并没有放入,所以要判断哪个数组还有剩余,把这些元素全部压入
2. 算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
3. 动图演示

4. 核心
归并排序的核心在递归之后,设置左指针和右指针,然后通过做右指针判断左右列表的值的大小,将小的存入开辟的空间。当一方放置完成之后,将另一方所有的值直接压入
5. 代码展示
def sort6(arr):
if len(arr) < 2:
return arr
mid = len(arr) // 2
left_li = sort6(arr[:mid])
right_li = sort6(arr[mid:])
pointer_left = 0
pointer_right = 0
result = []
while pointer_left < len(left_li) and pointer_right < len(right_li):
if left_li[pointer_left] < right_li[pointer_right]:
result.append(left_li[pointer_left])
pointer_left += 1
else:
result.append(right_li[pointer_right])
pointer_right += 1
if pointer_left < len(left_li):
result += left_li[pointer_left:]
if right_pointer < len(right_li):
result += right_li[pointer_right:]
return result
def sort6(arr):
# 判断传入的数组长度是否小于1,如果是返回该数组
if len(arr) < 2:
return arr
mid = len(arr) // 2 # 获取中间值
# left_li,right_li:采用归并排序后形成的新的有序的列表
left_li = sort6(arr[:mid])
right_li = sort6(arr[mid:])
# 下面是将两个数组合并成一个新的数组
left_pointer = 0
right_pointer = 0
result = []
# 如果左指针小于左列表长度并且右指针小于右列表的长度
while left_pointer < len(left_li) and right_pointer < len(right_li):
# 如果左列表下的左指针所指向的值小于右边的,则把该值放入新列表
if left_li[left_pointer] < right_li[right_pointer]:
result.append(left_li[left_pointer])
left_pointer += 1
else:
# 不然就把右边的值放入新列表指针加一
result.append(right_li[right_pointer])
right_pointer += 1
# 当结束上层循环说明,某个指针已经达到了该列表的长度,另一个列表还有多余的值需要全部传入新数组
if left_pointer < len(left_li):
# 也可以使用extend方法而不能使用append。
# extend是将传入的列表挨个值放入列表,但是append是将整个放入
result += left_li[left_pointer:]
if right_pointer < len(right_li):
result += right_li[right_pointer:]
return result
九、快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好:快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
1. 基本思路
选择中间值,分出左半边和右半边,然后循环该操作,直到传入的值符合结束要求。
细化:对于传入的数组,有开始指针和结束指针,然后开始指针设为中间值,从结束指针开始往前移动,如果该值小于中间值,则把结束指针上的值与开始指针进行位置交换,然后由开始指针往前移动,如果值大于中间值,则与结束指针的值进行交换,重复上述操作,直到开始指针和结束指针的值相等,此时数组分为中间值,中间值左边部分,中间值右边部分。然后把这些部分再次递归调用函数,直到传入的参数的开始指针大于等于结束指针时,返回函数。
2. 算法步骤
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
3. 动图演示

4. 核心
快速排序的核心在于,选定基准之后,先用高的值与基值进行判断,如果小于基值,则把高值放到低值,然后用低值进行判断,如果高于基值,则放到高值。一直重复循环上述步骤,直到low不小于high为止。
5. 代码展示
def sort7(arr, first, last):
if fitst >= last:
return
low = first
high = last
mid_val = arr[low]
while low < high:
while low < high and arr[high] > mid_val:
high -= 1
arr[low] = arr[high]
while low < high and arr[low] < mid_val:
low += 1
arr[high] = arr[low]
arr[low] = mid_val
sort7(arr, first, low - 1)
sort7(arr, low + 1, high)
return arr
def sort7(arr, first, last):
# 判断该函数是否结束
if first >= last:
return
mid_val = arr[first]
low = first
high = last
while low < high:
# 如果开始指针小于结束指针并且结束指针指向的值大于等于中间值
while low < high and arr[high] >= mid_val:
# 结束指针往左移动一位
high -= 1
# 当上层循环结束时,说明开始指针等于结束指针或者结束指针小于中间值
# 则需要将结束指针指向的值放到开始指针指向的位置
arr[low] = arr[high]
# 接下来由开始指针进行判断,如果开始指针小于结束指针并且开始指针指向的值小于中间值
while low < high and arr[low] < mid_val:
# 开始指针往右移动一位
low += 1
# 当上层循环结束时,说明开始指针等于结束指针或者开始指针大于中间值
# 则需要将开始指针指向的值放到结束指针指向的位置
arr[high] = arr[low]
# 如果上层循环结束,说明开始指针和结束指针指向同一个位置,则把中间值放到开始指针所指位置(也可以是结束指针)
arr[low] = mid_val
# 结束以上的工作,说明该数组在first到end之间,已经分出了三部分(即中间值,中间值的左半部分和中间值的右半部分),接下来即可将左半部分和右半部分放入函数进行重新递归。
sort7(arr, first, low - 1)
sort7(arr, low + 1, last)
return arr
以上是在python中经常用到的排序方式,也是各大书籍zho
十、堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
堆排序的平均时间复杂度为 Ο(nlogn)。
1. 算法步骤
- 创建一个堆 H[0……n-1];
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2,直到堆的尺寸为 1。
2. 动图演示


3. 代码实现
def buildMaxHeap(arr):
import math
for i in range(math.floor(len(arr)/2),-1,-1):
heapify(arr,i)
def heapify(arr, i):
left = 2*i+1
right = 2*i+2
largest = i
if left < arrLen and arr[left] > arr[largest]:
largest = left
if right < arrLen and arr[right] > arr[largest]:
largest = right
if largest != i:
swap(arr, i, largest)
heapify(arr, largest)
def swap(arr, i, j):
arr[i], arr[j] = arr[j], arr[i]
def heapSort(arr):
global arrLen
arrLen = len(arr)
buildMaxHeap(arr)
for i in range(len(arr)-1,0,-1):
swap(arr,0,i)
arrLen -=1
heapify(arr, 0)
return arr
十一、计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
1. 特征
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
通俗地理解,例如有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因
2. 算法步骤
- (1)找出待排序的数组中最大和最小的元素
- (2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项
- (3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- (4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
3. 动图演示

4. 代码展示
def countingSort(arr, maxValue):
bucketLen = maxValue+1
bucket = [0]*bucketLen
sortedIndex =0
arrLen = len(arr)
for i in range(arrLen):
if not bucket[arr[i]]:
bucket[arr[i]]=0
bucket[arr[i]]+=1
for j in range(bucketLen):
while bucket[j]>0:
arr[sortedIndex] = j
sortedIndex+=1
bucket[j]-=1
return arr
十二、桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
1. 什么时候最快
当输入的数据可以均匀的分配到每一个桶中。
2. 什么时候最慢
当输入的数据被分配到了同一个桶中。
3. 示意图
元素分布在桶中:

然后,元素在每个桶中排序:

4. 代码展示
def bucket_sort_simplify(arr, max_num):
"""
简化版
"""
buf = {i: [] for i in range(int(max_num)+1)} # 不能使用[[]]*(max+1),这样新建的空间中各个[]是共享内存的
arr_len = len(arr)
for i in range(arr_len):
num = arr[i]
buf[int(num)].append(num) # 将相应范围内的数据加入到[]中
arr = []
for i in range(len(buf)):
if buf[i]:
arr.extend(sorted(buf[i])) # 这里还需要对一个范围内的数据进行排序,然后再进行输出
return arr
十三、基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
1. 基数排序 vs 计数排序 vs 桶排序
基数排序有两种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;
2. LSD 基数排序动图演示

3. 代码实现
def radix_sort(data):
if not data:
return []
max_num = max(data) # 获取当前数列中最大值
max_digit = len(str(abs(max_num))) # 获取最大的位数
dev = 1 # 第几位数,个位数为1,十位数为10···
mod = 10 # 求余数的除法
for i in range(max_digit):
radix_queue = [list() for k in range(mod * 2)] # 考虑到负数,我们用两倍队列
for j in range(len(data)):
radix = int(((data[j] % mod) / dev) + mod)
radix_queue[radix].append(data[j])
pos = 0
for queue in radix_queue:
for val in queue:
data[pos] = val
pos += 1
dev *= 10
mod *= 10
return data
更多推荐
所有评论(0)