十大算法简介
- 冒泡排序
- 选择排序
- 插入排序
- 希尔排序
- 归并排序
- 快速排序
- 堆排序
- 基数排序
- 桶排序(待定)
- 计数排序(待定)
稳定性
稳定:当a == b时,a、b的相对位置不变: a,b --> a,b;
不稳定:当a == b时,a、b的相对位置可能发生变化:a,b --> b,a。
稳定算法:冒泡排序、插入排序、归并排序、计数排序、桶排序、基数排序
不稳定算法:选择排序、希尔排序、快速排序、堆排序
是否需要额外空间
不需要:冒泡排序、选择排序、插入排序、希尔排序、快速排序、堆排序
需要:归并排序、计数排序、桶排序、基数排序
内排序/外排序
内排序:所有排序操作都在内存中完成;
外排序:排序通过磁盘和内存的数据传输进行,一般用于数据量较大,需要将数据放入磁盘中。
以上算法中,计数排序和桶排序属于外排序算法;其他八种算法,均为内排序算法 —— “八大经典排序算法”。
空间复杂度
冒泡排序、选择排序、插入排序、希尔排序、快速排序、堆排序这六种算法不需要额外空间,但快速排序采用了分治的思想,将问题规模通过分治法消减为log n次,增加了空间复杂度,因此:
冒泡排序、选择排序、插入排序、希尔排序、堆排序的空间复杂度均为O(1);
快速排序的空间复杂度为O(log n);
归并排序的空间复杂度为O(n);
计数排序的空间复杂度为O(k),"k"是指要排序的序列中最大值和最下值得差,既额外空间,它的空间复杂度与原问题规模无关;
桶排序、基数排序的时间复杂度为O(n+k),原问题规模n和桶的数量(额外空间)
1.冒泡排序
算法描述
1.遍历整个序列,比较系列中相邻位置的两个元素,如果元素顺序错误,则交换两个元素的位置;(一趟比较完后,最大的元素位于最右边(已排序))
2.重复步骤1,遍历除最右边外的剩下元素,依次将最大(次大)的元素移至右边相应位置(已排序序列的前面一个位置),直至所有元素排好;;
3.简言之,第一趟比较,将最大的数“冒”出来,放在最后边;然后依次“次大”、“第三大”....
过程演示
原始序列:5 2 47 36 15
第一趟排序:2 5 36 15 47
第二趟排序:2 5 15 36 47
第三趟排序:2 5 15 36 47
已经有序,结束
时间复杂度
冒泡排序最好情况下的时间复杂度为O(n),此时的序列本身就已经排序好;最坏情况下的时间复杂度为O(n2);平均时间复杂度为O(n2)。
代码
def bubbleSort(arr):
n = len(arr) # 计算数组长度
for i in range(n): # 遍历数组
for j in range(n-i-1):
if arr[j] > arr[j+1] : # 比较相邻的两个数
arr[j], arr[j+1] = arr[j+1], arr[j]
2.选择排序
算法描述
1.找出序列中的最小元素,与序列的起始位置的元素交换位置;
2.从剩余未排序元素中继续寻找最小元素,然后与未排序序列的第一个元素交换位置。以此类推,直到所有元素均排序完毕。
过程演示
原始序列:5 2 47 36 15
第一趟排序:2 5 47 36 15
第二趟排序:2 5 47 36 15
第三趟排序:2 5 15 36 47
第四趟排序:2 5 15 36 47
时间复杂度
选择排序在任何情况下的时间复杂度都为O(n2)。
代码
def selectSort(arr):
for i in range(len(arr)):
min_index = i # 记录最小元素的位置
for j in range(i + 1, len(arr)):
if arr[min_index] > arr[j]:
min_index = j
arr[i], arr[min_index] = arr[min_index], arr[i] # 将最小元素与序列第一个元素交换位置
3.插入排序
算法描述
1.将第一个数作为已经排序好的序列;
2.依次遍历未排序的元素,将每个元素,从已排序的序列最后往前比较,找到位置插入,,直到所有元素均排序完毕。
过程演示
原始序列:5 2 47 36 15
第一趟排序:2 5 47 36 15
第二趟排序:2 5 47 36 15
第三趟排序:2 5 36 47 36
第四趟排序:2 5 15 36 47
时间复杂度
插入排序最好情况下的时间复杂度为O(n),此时的序列本身就已经排序好;最坏情况下的时间复杂度为O(n2);平均时间复杂度为O(n2)。
代码
def insertSort(arr):
for i in range(1, len(arr)):
temp = arr[i] # 记录arr[i]的值
j = i - 1
while j >= 0 and temp < arr[j]:# 遍历arr[i]之前的序列(已经排序好)
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = temp
4.希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。
算法描述
1.先取一个正整数d1<n,把所有序号相隔d1的数组元素放一组,组内进行直接插入排序;
2.然后取d2<d1,重复上述分组和排序操作;
3.直至di=1,即所有记录放进一个组中排序为止。
过程演示
原始序列:5 2 47 36 15 8 (间隔为:di = d(1-i)//2)
第一趟排序:[5,36], [2,15], [47,8]
5 2 8 36 15 47
第二趟排序:[5,8,15],[2,36,47]
5 2 8 36 15 47
第三趟排序:[5,2,8,36,15,47]
2 5 8 15 36 47
时间复杂度
希尔排序最好情况下的时间复杂度为O(nlog n);最坏情况下的时间复杂度为O(nlog2n);平均时间复杂度为O(nlog2n)。
代码
def shellSort(arr):
n = len(arr)
d = n // 2
while d > 0:
for i in range(d, n):
temp = arr[i]
j = i
while j >= d and arr[j - d] > temp:
arr[j] = arr[j - d]
j -= d
arr[j] = temp
d = d // 2
5.归并排序
算法描述
1.分割:递归地把当前序列平均分割成两半;
2.集成:在保持元素顺序的同时,将上一步得到的子序列集成到一起(归并)。
过程演示
原始序列:5 2 47 36 15 8 9 6
第一趟排序:2 5 36 47 8 15 6 9
第二趟排序:2 5 36 47 6 8 9 15
第三趟排序:2 5 6 8 9 15 36 47
时间复杂度
归并排序在任何情况下的时间复杂度都为O(n long n)。
代码
def merge(arr, l, m, r):
n1 = m - l + 1
n2 = r - m
# 创建临时数组
L = [0] * (n1)
R = [0] * (n2)
for i in range(0, n1): # 拷贝数据到临时数组 arrays L[] 和 R[]
L[i] = arr[l + i]
for j in range(0, n2):
R[j] = arr[m + 1 + j]
i = 0 # 初始化第一个子数组的索引
j = 0 # 初始化第二个子数组的索引
k = l # 初始归并子数组的索引
while i < n1 and j < n2:
if L[i] <= R[j]:
arr[k] = L[i]
i += 1
else:
arr[k] = R[j]
j += 1
k += 1
while i < n1: # 拷贝 L[] 的保留元素
arr[k] = L[i]
i += 1
k += 1
while j < n2: # 拷贝 R[] 的保留元素
arr[k] = R[j]
j += 1
k += 1
def mergeSort(arr, l, r):
if l < r:
m = int((l + (r - 1)) / 2)
mergeSort(arr, l, m)
mergeSort(arr, m + 1, r)
merge(arr, l, m, r)
6.快速排序
算法描述
1.选取一个关键字(通常是第一个)作为枢轴,将序列中比枢轴小的移到枢轴的前边,比枢轴大的移动到枢轴的后边;
2.先将关键字与最后一个数(右边)比较,如果最后一个数比关键字大,与倒数第二个数比较...直到遇到比关键字小的数,交换两者位置;
3.再将关键字与第一个数(左边)比较,找到比关键大的数,交换两者位置;
4.重复步骤2和步骤3,遍历完整个序列,第一趟排序完成,此时关键字左边都是比关键字小的元素,右边是比关键字大的元素;
5.将上述关键字两边,作为两个新的子序列,重复步骤2和步骤3,直到子序列的长度为0或1,排序完成。
过程演示
原始序列:15 2 47 36 5 8 9 6
第一趟排序:6 2 9 8 5 15 36 47
说明:将 15 作为关键字,和最右边元素 6 进行比较,6比15小,交换位置:6 2 47 36 5 8 9 15;
再将15 与左边数字进行比较(6不用再比较了),2,不用交换,47比15大,交换:6 2 15 36 5 8 9 47;
再回到右边,与 9 比较,9比15小,交换:6 2 9 36 5 8 15 47;
再回到左边,与36比较,36比15大,交换:6 2 9 15 5 8 36 47;
再回到右边,与8比较,8比15小,交换:6 2 9 8 5 15 36 47;
第一趟排序完成。
第二趟排序:5 2 6 8 9 15 36 47
第三趟排序:2 5 6 8 9 15 36 47
时间复杂度
快速排序最好情况下的时间复杂度为O(nlog n);最坏情况下的时间复杂度为O(n2);平均时间复杂度为O(nlog n)。
注:快速排序最坏的情况是,原始序列为倒序,此时快速排序倒退为冒泡排序。
代码
def quickSort(arr, low, high): # 对从R[Low]到R[High]的关键字进行排序
temp = 0
i = low
j = high
if low < high:
temp = arr[low]
while i < j: # 每次循环完成了一趟排序,即数组中小于temp的关键字放在左边,大于temp的关键字放在右边;左边和右边的分界点就是temp的最终位置
while i < j and arr[j] >= temp: # 先从右往左扫描,找到第一个小于temp的关键字
j -= 1
if i < j: # 这个判断保证退出上面的while循环是因为R[j] < temp,而不是因为 i >= j退出循环的,此步非常重要切忌将其忽略
arr[i] = arr[j] # 放在temp左边
i += 1 # i右移一位
while i < j and arr[i] <= temp: # 从右往左扫描,找到一个大于temp的关键字
i += 1
if i < j:
arr[j] = arr[i] # 放在tem的左边
j -= 1 # j左移一位
arr[j] = temp # 将temp放在最终的位置上
quickSort(arr, low, i - 1) # 递归的对temp左边的关键字进行排序
quickSort(arr, i + 1, high) # 递归的对temp右边的关键字进行排序
7.堆排序
堆是一种完全二叉树,堆排序分为大根堆排序(正序)和小根堆排序(逆序):大根堆指父节点元素的值大于等于子节点的值,这样每次排序都可以找到未排序序列的最大值;小根堆指父节点元素的值小于等于子节点的值,这样每次排序都可以找到未排序序列的最小值;本文以大根堆为例。
算法描述
1.将序列排成一个完全二叉树,从树的第一个非叶子结点开始,从左至右,从上至下,对每个结点进行调整,直到得到一个大根堆;
对结点的调整方法:将当前结点(假设为A)的值与其孩子结点进行比较,如果存在大于A的值的孩子结点,则从中挑出最大的一个与A进行交换;
当A来到下一层的时候重复上述过程,直到A的孩子结点的值都小于A的值为止。
2.将步骤1中的大根堆,除去根节点(最大值)外的序列,按照步骤1重新排序,依次获得次大值,第三大....直到序列中的元素个数为1时,结束排序。
过程演示
原始序列:15 2 47 36 5 8 9 6
第一趟排序:47 15 36 6 5 8 9 2
第二趟排序:47 36 15 9 5 8 6 2
第三趟排序:47 36 15 9 5 8 6 2
第四趟排序:47 36 15 9 6 8 5 2
第五趟排序:47 36 15 9 8 6 5 2
第六趟排序:47 36 15 9 8 6 5 2
第七趟排序:47 36 15 9 8 6 5 2
时间复杂度
堆排序在任何情况下的时间复杂度都为O(n long n)。
代码
def heapify(arr, n, i):
largest = i
l = 2 * i + 1 # left = 2*i + 1
r = 2 * i + 2 # right = 2*i + 2
if l < n and arr[i] < arr[l]:
largest = l
if r < n and arr[largest] < arr[r]:
largest = r
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i] # 交换
heapify(arr, n, largest)
def heapSort(arr):
n = len(arr)
for i in range(n, -1, -1): # 创建堆
heapify(arr, n, i)
for i in range(n - 1, 0, -1) :
arr[i], arr[0] = arr[0], arr[i] # 交换
heapify(arr, i, 0)
8.基数排序
算法描述
1.首先根据个位数的数值,将序列中的数值,分配至编号0到9的桶中,再将桶中的数串联起来组成一个新序列:
2.将第一步中的新序列,根据十位数的数值,重新分配至编号0到9的桶中,再串联起来;
3.后面依次根据百位、千位....重新分配、串联,持续进行到最高位结束。
过程演示
原始序列:73 22 93 43 55 14 28 65 39 81
第一趟排序:81 22 73 93 43 14 55 65 28 39
0 1 2 3 4 5 6 7 8 9
81 22 73 14 55 28 39
93 65
43
第二趟排序:14 22 28 39 43 55 65 73 81 93
0 1 2 3 4 5 6 7 8 9
14 22 39 43 55 65 73 81 93
时间复杂度
基数排序在任何情况下的时间复杂度都为O(n * k)(k为需要桶的数量)。
import math
def radixSort(arr, radix=10): # radix为桶的数量
K = int(math.ceil(math.log(max(arr), radix))) # 用K位数可表示任意整数
bucket = [[] for i in range(radix)] # 开辟桶的数组
for i in range(1, K+1): # K次循环
for val in arr:
bucket[val%(radix**i)//(radix**(i-1))].append(val) # 析取整数第K位数字 (从低到高)
del arr[:]
for each in bucket:
arr.extend(each) # 桶合并
bucket = [[] for i in range(radix)]