名词解释:
-
n:数据规模
-
k:"桶"的个数
-
In-place:占用常数内存,不占用额外内存
-
Out-place:占用额外内存
-
稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同
关于时间复杂度:
-
平方阶 (O(n²)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。
-
线性对数阶 (O(nlogn)) 排序 快速排序、堆排序和归并排序。
-
线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
关于稳定性:
-
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。(冒插归基)
-
不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。(选快希堆)
一、冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。这个算法的名字由来是因为越小的元素会经由交换慢慢 "浮" 到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名 "冒泡排序"。
算法原理:
-
比较相邻的元素,如果第一个比第二个大,就交换他们两个。
-
对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
-
针对所有的元素重复以上的步骤,除了最后一个。
-
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
动图演示过程如下:
算法实现:
def bubble_sort(nums):
for i in range(len(nums) - 1):
flag = True # 每一趟置flag为True
for j in range(0, len(nums) - i - 1):
if nums[j] > nums[j + 1]:
nums[j], nums[j + 1] = nums[j + 1], nums[j]
flag = False # 有交换 flag置为False
# flag为True 说明到这趟已经没有交换 提前跳出循环 提高算法效率
if flag:
break
return nums
s = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
print(bubble_sort(s))
# 结果如下:
# [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
详细步骤如下:
[3, 38, 44, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
[3, 38, 5, 44, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
[3, 38, 5, 44, 15, 47, 36, 26, 27, 2, 46, 4, 19, 50, 48]
[3, 38, 5, 44, 15, 36, 47, 26, 27, 2, 46, 4, 19, 50, 48]
[3, 38, 5, 44, 15, 36, 26, 47, 27, 2, 46, 4, 19, 50, 48]
[3, 38, 5, 44, 15, 36, 26, 27, 47, 2, 46, 4, 19, 50, 48]
[3, 38, 5, 44, 15, 36, 26, 27, 2, 47, 46, 4, 19, 50, 48]
[3, 38, 5, 44, 15, 36, 26, 27, 2, 46, 47, 4, 19, 50, 48]
[3, 38, 5, 44, 15, 36, 26, 27, 2, 46, 4, 47, 19, 50, 48]
[3, 38, 5, 44, 15, 36, 26, 27, 2, 46, 4, 19, 47, 50, 48]
[3, 38, 5, 44, 15, 36, 26, 27, 2, 46, 4, 19, 47, 48, 50]
[3, 5, 38, 44, 15, 36, 26, 27, 2, 46, 4, 19, 47, 48, 50]
[3, 5, 38, 15, 44, 36, 26, 27, 2, 46, 4, 19, 47, 48, 50]
[3, 5, 38, 15, 36, 44, 26, 27, 2, 46, 4, 19, 47, 48, 50]
[3, 5, 38, 15, 36, 26, 44, 27, 2, 46, 4, 19, 47, 48, 50]
[3, 5, 38, 15, 36, 26, 27, 44, 2, 46, 4, 19, 47, 48, 50]
[3, 5, 38, 15, 36, 26, 27, 2, 44, 46, 4, 19, 47, 48, 50]
[3, 5, 38, 15, 36, 26, 27, 2, 44, 4, 46, 19, 47, 48, 50]
[3, 5, 38, 15, 36, 26, 27, 2, 44, 4, 19, 46, 47, 48, 50]
[3, 5, 15, 38, 36, 26, 27, 2, 44, 4, 19, 46, 47, 48, 50]
[3, 5, 15, 36, 38, 26, 27, 2, 44, 4, 19, 46, 47, 48, 50]
[3, 5, 15, 36, 26, 38, 27, 2, 44, 4, 19, 46, 47, 48, 50]
[3, 5, 15, 36, 26, 27, 38, 2, 44, 4, 19, 46, 47, 48, 50]
[3, 5, 15, 36, 26, 27, 2, 38, 44, 4, 19, 46, 47, 48, 50]
[3, 5, 15, 36, 26, 27, 2, 38, 4, 44, 19, 46, 47, 48, 50]
[3, 5, 15, 36, 26, 27, 2, 38, 4, 19, 44, 46, 47, 48, 50]
[3, 5, 15, 26, 36, 27, 2, 38, 4, 19, 44, 46, 47, 48, 50]
[3, 5, 15, 26, 27, 36, 2, 38, 4, 19, 44, 46, 47, 48, 50]
[3, 5, 15, 26, 27, 2, 36, 38, 4, 19, 44, 46, 47, 48, 50]
[3, 5, 15, 26, 27, 2, 36, 4, 38, 19, 44, 46, 47, 48, 50]
[3, 5, 15, 26, 27, 2, 36, 4, 19, 38, 44, 46, 47, 48, 50]
[3, 5, 15, 26, 2, 27, 36, 4, 19, 38, 44, 46, 47, 48, 50]
[3, 5, 15, 26, 2, 27, 4, 36, 19, 38, 44, 46, 47, 48, 50]
[3, 5, 15, 26, 2, 27, 4, 19, 36, 38, 44, 46, 47, 48, 50]
[3, 5, 15, 2, 26, 27, 4, 19, 36, 38, 44, 46, 47, 48, 50]
[3, 5, 15, 2, 26, 4, 27, 19, 36, 38, 44, 46, 47, 48, 50]
[3, 5, 15, 2, 26, 4, 19, 27, 36, 38, 44, 46, 47, 48, 50]
[3, 5, 2, 15, 26, 4, 19, 27, 36, 38, 44, 46, 47, 48, 50]
[3, 5, 2, 15, 4, 26, 19, 27, 36, 38, 44, 46, 47, 48, 50]
[3, 5, 2, 15, 4, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
[3, 2, 5, 15, 4, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
[3, 2, 5, 4, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
[2, 3, 5, 4, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
二、选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
算法原理:
-
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
-
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
-
重复第二步,直到所有元素均排序完毕。
算法实现:
def select_sort(nums):
for i in range(len(nums)-1):
min_index = i
# i+1是开始从i之后的元素找最小值,并用minindex标记它的索引
for j in range(i+1, len(nums)):
if nums[j] < nums[min_index]:
min_index = j
# 如果和最开始的标记的最小元素不等,就交换两个元素
if min_index != i:
nums[i], nums[min_index] = nums[min_index], nums[i]
return nums
s = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
print(select_sort(s))
# 结果如下:
# [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
详细步骤如下:
三、插入排序
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
算法原理:
-
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
-
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)
动态图演示如下:
算法实现:
def insert_ort(arr):
for i in range(len(arr)):
preIndex = i-1
current = arr[i]
while preIndex >= 0 and arr[preIndex] > current:
arr[preIndex + 1] = arr[preIndex]
preIndex -= 1
arr[preIndex + 1] = current
return arr
s = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
print(insert_sort(s))
# 结果如下:
# [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
详细步骤如下:
四、希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是不稳定排序算法。希尔排序是基于插入排序的以下两点性质而提出改进方法的:
-
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
-
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
算法原理:
-
选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
-
按增量序列个数 k,对序列进行 k 趟排序;
-
每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
动图演示如下:
算法实现:
def shell_sort(nums):
n = len(nums)
gap = n // 2 # 定义增量
# gap等于1的时候相当于最后一步是一插入排序
while gap >= 1:
for j in range(gap, n):
i = j
# 增量的插入排序版本
while (i-gap) >= 0:
if nums[i] < nums[i-gap]:
nums[i], nums[i-gap] = nums[i-gap], nums[i]
i -= gap
else:
break
gap //= 2
return nums
s = [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]
详细步骤如下:
五、归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:
-
自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
-
自下而上的迭代;
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。
算法原理:
-
开辟内存空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
-
设定两个指针,最初位置分别为两个已经排序序列的起始位置;
-
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
-
重复步骤 3 直到某一指针达到序列尾;
-
将另一序列剩下的所有元素直接复制到合并序列尾。
动态图演示如下:
算法实现:
from math import floor
def merge_sort(arr):
if(len(arr) < 2):
return arr
# 二分
middle = floor(len(arr) / 2)
left, right = arr[0:middle], arr[middle:]
# 递归
return merge(merge_sort(left), merge_sort(right))
def merge(left, right):
result = []
# 分治
while left and right:
if left[0] <= right[0]:
result.append(left.pop(0))
else:
result.append(right.pop(0))
while left:
result.append(left.pop(0))
while right:
result.append(right.pop(0))
return result
s = [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)中,它至少会把一个元素摆到它最后的位置去。
动态图演示如下:
算法实现:
def quick_sort(arr, left=None, right=None):
left = 0 if not isinstance(left,(int, float)) else left
right = len(arr) - 1 if not isinstance(right,(int, float)) else right
if left < right:
partitionIndex = partition(arr, left, right)
quick_sort(arr, left, partitionIndex - 1)
quick_sort(arr, partitionIndex + 1, right)
return arr
def partition(arr, left, right):
pivot = left
index = pivot+1
i = index
while i <= right:
if arr[i] < arr[pivot]:
swap(arr, i, index)
index += 1
i+=1
swap(arr, pivot, index - 1)
return index - 1
def swap(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。
动图演示如下:
算法实现:
from math import floor
def buildMaxHeap(arr):
for i in range(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 heap_sort(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
s = [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 进行计数排序(利用计数排序适用于小范围数的特点);
动图演示如下:
算法实现:
def radix_sort(nums):
# 算n:为了计算最高位
max_num = max(nums)
n = 1
while max_num > 10 ** n:
n += 1
for k in range(n):
# 初始化0-9个桶来排序
buckets = [[] for i in range(10)]
for subnum in nums:
buckets[int(subnum / (10 ** k) % 10)].append(subnum)
nums = [num for bucket in buckets for num in bucket]
return nums
s = [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]