十大经典排序算法
排序算法
排序算法的性能如下表所示:

以上算法,我们经常标定快速排序为最优,但是它在现在编程语言使用的排序算法面前,就是弟弟。如python使用的timsort。
堆排就是大根堆,小根堆,原理比较简单,这里不再进行赘述,可参考:https://www.runoob.com/w3cnote/heap-sort.html。
一、冒泡排序
冒泡排序:通俗理解就是一个泡不断的往上冒,对应到数组中就是在一个窗口长度为2的窗口中比较两个数,若前者大于后者,则交换(升序),不断循环直到没有需要交换的。这个滑动窗口需要不断地从头到尾进行滑动。

代码实现
def bubbling(bubbles: list, reverse=False) -> list:
"""
冒泡排序
:param bubbles: 待排序的数组
:param reverse: 是否逆序
:return: 排序后的列表
"""
end_node = len(bubbles)
while end_node > 1:
status = False
for i in range(end_node-1):
if bubbles[i] > bubbles[i+1] and not reverse:
bubbles[i], bubbles[i+1] = bubbles[i+1], bubbles[i]
status = True
elif bubbles[i] < bubbles[i+1] and reverse:
bubbles[i], bubbles[i + 1] = bubbles[i + 1], bubbles[i]
status = True
if not status:
break
end_node -= 1
return bubbles
这个算法我们这里只引入了两个变量,所以内存开销是\(O(1)\)。两层循环,理论上时间复杂度是\(O(n^2)\)。
这段代码我们加了是否逆序参数,提前停止状态,内层循环 ,若不考虑这些代码会简单很多。
二、选择排序
选择排序:每次从数组剩余元素中选出最小的元素(逆序选最大)替换剩余数组第一个元素,这种思路就是暴力搜索的思路。

代码实现
def select_sort(arr: list, reverse=False) -> list:
"""
选择排序
:param arr: 待排序数组
:param reverse: 是否逆序
:return: 排序后列表
"""
for i in range(len(arr)-1):
status = i
for j in range(i+1, len(arr)):
if arr[j] < arr[status] and not reverse:
status = j
elif arr[j] > arr[status] and reverse:
status = j
arr[i], arr[status] = arr[status], arr[i]
return arr
这个算法的空间开销是\(O(1)\),算法复杂度是\(O(n^2)\),而且一直都是。
三、插入排序
插入排序:循环数组,每个元素都向前插队,直到前一个元素比子集小(升序排列)。
插入排序即当前节点之前的序列是有序的,只需要将当前节点向前滑动插入,直到满足前一节点比它小即可

代码实现
def insert_sort(arr: list, reverse=False) -> list:
"""
插入排序
:param arr: 待排序数组
:param reverse: 是否逆序
:return: 排序后列表
"""
for i in range(len(arr)-1):
for j in range(i, -1, -1):
if arr[j] < arr[j+1] and reverse:
arr[j], arr[j+1] = arr[j+1], arr[j]
elif arr[j] > arr[j+1] and not reverse:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
这里没有引入其他变量,空间复杂度为\(O(1)\),时间复杂度为:\(O(n^2)\)。
四、希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
希尔排序的步骤:
- 计算待排序列的长度\(n\);
- 生成递减增量序列:\(gap = \left\{ n/2, n/4, \cdots,1 \right\}\)
- 根据增量序列元素作为步长进行取值,拆分为\(gap[i]\)个子序列,对子序列在原数组上进行排序;
- 循环处理到\(gap[-1]\)时,即对整个序列排序,但是实际上只需要两两比较就好。
希尔排序为什么是有效的?(证明参考于知乎:https://zhuanlan.zhihu.com/p/137355984)
定义: 我们规定如果序列\(nums\)满足如下性质:
其中\(k\)为整数,\(n\)为序列长度,则称序列nums为\(k\)有序。
引理: 若序列\(X=\left\{x_{1},x_{2},\cdots,x_{m+r}\right\}\),\(Y=\left\{y_{1},y_{2},\cdots,y_{n+r}\right\}\),满足:
则分别对\(X,Y\)升序排列后,仍有上述关系!
证明: 对\(X,Y\)升序排列后,有:
根据已知条件,我们知道一定在\(X\)原始序列中存在一个数使得\(x_{ir} \leqslant y_{n+i},(1\leqslant i\leqslant r)\),即\(y_{j}\)至少大于\(j\)个\(X\)中的数。
所以,\(x_{k} \leqslant y_{n+k},\quad(1 \leqslant k \leqslant r)\).引理得证\(\blacksquare\)
证明序列nums是\(k\)有序的,则排序后的nums是\(h(h<k)\)有序的,则新的nums还是\(k\)有序的!
证明: nums是\(k\)有序的,则:
我们知道上面每个子序列都是有序的!下面我们对这个序列进行关于\(h\)的拆分:
注意\((5)\)就是下一次迭代我们需要进行排序的子序列!
我们先取出局部序列:
此时,有\(k\)有序有:
根据引理,再次排序后,这部分还是满足 这个关系,而这个关系又可以推出\(k\)有序。
所以希尔排序可以排序得到证明,即第一次可以获得\(\left \lfloor \frac{n}{2} \right \rfloor\)个有序数,再一次排序后,在保持\(\left \lfloor \frac{n}{2} \right \rfloor\)有序的前提下,又实现了\(\left \lfloor \frac{n}{4} \right \rfloor\)有序,这样一直进行下去,数组的排序也就可以完成了。
代码实现
def hill(arr: list, reverse=False) -> list:
"""
希尔排序
:param arr: 待排序数组
:param reverse: 是否逆序
:return: 排序后列表
"""
n = len(arr) // 2
while n >= 1:
for g in range(n):
for i in range(g, len(arr)-1, n):
for j in range(i, g-1, -1):
if arr[j] < arr[j + 1] and reverse:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
elif arr[j] > arr[j + 1] and not reverse:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
pass
n = n//2
return arr
五、归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

图片来源于:https://www.cnblogs.com/chengxiao/p/6194356.html
代码实现
def merge_sort(arr: list) -> list:
"""归并排序"""
res = []
n = 1
while n <= len(arr):
n *= 2
for i in range(0, len(arr), n):
a = i
b = i+(1+n)//2
while a < min(i+(1+n)//2, len(arr)) or b < min(i+n, len(arr)):
if a >= min(i+(1+n)//2, len(arr)):
res.append(arr[b])
b += 1
elif b >= min(i+n, len(arr)):
res.append(arr[a])
a += 1
elif arr[a] < arr[b]:
res.append(arr[a])
a += 1
else:
res.append(arr[b])
b += 1
arr[i:i+len(res)] = res
res = []
return arr
六、快速排序
快速排序是一个优秀的算法,主要步骤为:
-
选择一个数作为基准;
-
基于基准向后遍历,若某个数据\(arr[j]\)小于基准\(arr[i]\)就将就将\(arr[j]\)保存,然后\(arr[i+1]\)赋值给\(a[j]\),\(arr[i]\)向后挪一位,\(arr[i]\)使用保存的\(arr[j]\)。
注意我这里和传统的解法不一样,有些许差异,但是本质一样
-
重复上面的步骤,直至基准遍历到倒数第二个位置。至此,所有元素比它大的都在其后面,比它小的都在它前面,基于此得到有序数组。
菜鸟上的图示(和我的步骤有差异):

代码实现
def quick_sort(arr: list) -> list:
i = 0
while i < len(arr)-1:
for j in range(i+1, len(arr)):
if arr[i] > arr[j]:
temp = arr[j]
arr[j] = arr[i+1]
arr[i+1] = arr[i]
arr[i] = temp
i += 1
return arr
此算法比菜鸟教程的实现要优一些!但是和python自带的sort(timsort)差距甚远。
七、计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
算法的步骤如下:
- 找出待排序的数组中最大和最小的元素
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

寻找最小最大值计算量为\(O(n)\),计数的计算复杂度为\(O(n)\),还原有序数组需要\(O(k)\),\(k\)可能会比较大,这样排序的优势可能不如预期。
代码实现略.
八、桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 映射函数能尽量将输入的\(n\)个数据均匀的分配到\(k\)个桶中
原理和计数排序是一样的,只是这里有点分治的意思。
代码实现略.
九、基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
基数排序 vs 计数排序 vs 桶排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值
这个排序算法可以理解为基数排序的特殊版本,即有一个明确的分桶函数。

十、timsort
timsort算法主要使用了:插入排序、归并排序。
算法步骤:
- 根据数组长度划分称多个子数组,数组个数有专门的计算方式,划分要求尽量均衡;
- 对每个子组进行插入排序;
- 每两个子组进行归并排序;
- 新生成的子组再进行归并排序,直到结束。
关于算法的讲解参考:
完!