引入
排序算法是算法学习中最基础的一部分,更是面试的常见考题。
掌握各种常用排序算法的特点,能够熟练地手撕,才能在面试中表现的更好。
本文列举:冒泡排序、选择排序、插入排序、归并排序、快速排序、希尔排序、堆排序、基数排序,共八大常用排序算法的方法特点,均使用Python实现。
1. 冒泡排序
1.1 基本特点
冒泡排序进行n轮(n为序列长度-1)冒泡
每轮冒泡将最小(大)的元素交换到待排序序列的最后(最前面)
若当前轮次没有进行任何交换则序列已经有序
注意:
- 最小还是最大要看比较条件是小于还是大于
- 最后还是最前要看比较是从前往后还是从后往前
1.2 算法分析
- 稳定性:稳定
- 时间复杂度 :最佳: ,最差:, 平均:
- 空间复杂度 :
1.3 代码实现
def bubble_sort(arr):
for i in range(len(arr)):
flag = False # 指示本轮是否改变了顺序
for j in range(len(arr)-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
flag = True
if not flag: # 未改变顺序,已有序
return
2. 选择排序
2.1 基本特点
选择排序是一种简单直观的排序算法。
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
以此类推,直到所有元素均排序完毕。
2.2 算法分析
- 稳定性:不稳定
- 时间复杂度 :最佳: ,最差:, 平均:
- 空间复杂度 :
2.3 代码实现
def selection_sort(arr):
for i in range(len(arr)):
m = i
for j in range(i+1, len(arr)): # 找出未排序序列中最小的元素
if arr[j]< arr[m]:
m = j
if m!=i:
arr[i], arr[m] = arr[m], arr[i] # 将其交换到对应的位置
3. 插入排序
3.1 基本特点
插入排序的排序方式有点类似于扑克牌整理手牌的方式
(但有一些不同,插入排序在寻找插入位置的同时就在一步步挪动元素来腾空间)
对于每个未排序的元素,先在已排序序列中从后向前扫描并挪动元素腾出位置,直到该元素移动到正确的位置(原理上是找到位置再插入,但操作上更高效)
3.2 算法分析
- 稳定性:稳定
- 时间复杂度 :最佳:O(n) ,最差:O(n2), 平均:O(n2)
- 空间复杂度 :O(1)
3.3 代码实现
def insertion_sort(arr):
for i in range(1, len(arr)): # 将第一个元素视为有序序列
cur = arr[i]
j = i-1
while j>=0 and cur <arr[j]:
arr[j+1] = arr[j] # 后移
j-=1
arr[j+1] = cur
3.4 优化:折半插入排序
与原先的直接插入排序差别在于:
先使用折半查找出元素的待插入位置,然后再统一挪动插入位置之后的所有元素
def binary_insertion_sort(arr):
for i in range(1, len(arr)): # 将第一个元素视为有序序列
l, r = 0, i-1 # 位置i对应当前要插入的元素
while (l <= r):
mid = (l+r)//2
if arr[i] < arr[mid]:
r = mid-1 # 在左子表中
else:
l = mid+1 # 在右子表中
# 遇到这种不好定范围的,建议在纸上画特例
j = i-1
cur = arr[i]
while j > r:
arr[j+1] = arr[j] # 后移
j -= 1
arr[j+1] = cur # 插入
4. 希尔排序
4.1 基本特点
因为直接插入排序适用于基本有序、数据的排序表,基于此希尔排序是对直接插入排序的一种优化,又称作缩小增量排序。
希尔排序的基本思想是将相隔某个“增量”的记录组成一个子表,对子表分别进行直接插入排序。
当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序。
常见的增量取dk=len(arr)//2,每轮排序后将增量缩小为一半。
直到增量为1时,相当于对全体记录进行直接插入排序,从而完成排序。
4.2 算法分析
- 稳定性:稳定
- 时间复杂度 :最佳:, 最差: 平均:
- 空间复杂度 :
4.3 代码实现
def shell_sort(arr):
dk = len(arr)//2
while (dk >= 1):
for i in range(dk, len(arr)): # 分轮次
cur = arr[i] # 对每轮的当前元素进行直接插入排序
j = i - dk # 注意间隔为dk
while j>=0 and cur < arr[j]:
arr[j+dk] = arr[j]
j -= dk
arr[j+dk] = cur
dk //= 2
5. 归并排序
5.1 基本特点
归并排序,“归并”的含义是将两个或两个以上的有序表组合成一个新的有序表
该算法是分治法 的一个非常典型的应用,利用递归来实现
先进行分解并排序,然后进行合并(合并时还需要调整位置以保证仍然有序)
5.2 算法分析
- 稳定性:稳定
- 时间复杂度 :最佳:, 最差:, 平均:
- 空间复杂度 :
5.3 代码实现
def merge_sort(arr):
def merge(arr1, arr2):
res = []
i, j = 0, 0
l1, l2 = len(arr1), len(arr2)
while i < l1 and j < l2:
if arr1[i] <= arr2[j]:
res.append(arr1[i])
i += 1
else:
res.append(arr2[j])
j += 1
# 还有有一个数组未完全合并
if i < l1:
res += arr1[i:]
if j < l2:
res += arr2[j:]
return res
if len(arr) <= 1:return arr
mid = len(arr)//2
# 拆分成左右两部分,递归进行分解、排序、合并
return merge(merge_sort(arr[:mid]), merge_sort(arr[mid:]))
6. 快速排序
6.1 基本特点
快速排序的基本思想也是基于分治法的
每次在待排序表中任取一个元素作为基准(通常取首元素)
然后通过一轮排序将排序表划分为独立的两个部分,得到基准的最终位置:
- 小于基准的部分
- 大于等于基准的部分(基准位于该部分)
对两部分递归地重复多轮操作,直到两部分都只有一个元素或空(终止条件)
特点是每轮能将基准元素放到其最终的位置上
6.2 算法分析
- 稳定性 :不稳定
- 时间复杂度 :最佳:, 最差:,平均:
- 空间复杂度 :
6.3 代码实现
def quick_sort(arr):
def partition(l, r):
p = arr[l] # 使用左端为基准(所以下方从右端开始搜索)
while l < r:
# 使用双指针交替移动到左右端
# 从右往左搜索小于基准的(注意左右端不能都搜索相等的)
while l < r and arr[r] >= p:
r -= 1
arr[l] = arr[r] # 将其移动到左端
# 从左往右搜索大于等于基准的(注意左右端不能都搜索相等的)
while l < r and arr[l] < p:
l += 1
arr[r] = arr[l] # 移动到右端
# 直到l==r时停止,此时的位置即基准的最终位置
arr[l] = p
return l
def quick_sort_between(l, r):
if l >= r: # 元素数量只有1个或者0个时递归结束
return
m = partition(l, r) # arr[m] 作为划分标准
quick_sort_between(l, m - 1)
quick_sort_between(m + 1, r)
quick_sort_between(0, len(arr)-1)
7. 堆排序
7.1 基本特点
堆排序是指利用堆来设计的一种排序算法。
堆是一个近似完全二叉树的结构,子结点的值总是小于(或者大于)它的父节点,分为小根堆和大根堆。
堆排序的核心步骤:
- 将数组调整为大顶堆,该过程即建堆
- 将堆顶元素与最后一个元素进行交换(即输出堆顶元素到序列尾),然后对其他元素再次建堆
- 重复上面的处理流程,直到堆中仅剩下一个元素,此时序列已有序。
此排序方式可以解决找出最大(小)的n个数的问题,适合数据量大时使用。
7.2 算法分析
- 稳定性 :不稳定
- 时间复杂度 :最佳:, 最差:, 平均:
- 空间复杂度 :
7.3 代码实现
def heap_sort(arr):
def build_min_heap(root, end):
while True:
child = 2 * root + 1
if child > end: # 无左子节点
return
# 若右子节点存在,则找出左右中更小的
if (child+1 <= end) and (arr[child+1] < arr[child]):
child += 1
if arr[child] < arr[root]:
arr[child], arr[root] = arr[root], arr[child] # 交换两者顺序
root = child # 继续向下调整
else:
return
n = len(arr)
first_root = n // 2 - 1 # 找到最后一个非叶节点(注意用//)
for r in range(first_root, -1, -1): # 由后向前遍历所有的根节点,建堆调整
build_min_heap(r, n - 1)
# 调整完成后,输出到序列尾,然后再次调整堆
for end in range(n - 1, 0, -1):
arr[0], arr[end] = arr[end], arr[0]
build_min_heap(0, end - 1)
8. 基数排序
8.1 基本特点
基数排序是非比较的排序算法
将所有待比较数值统一为同样的数位长度(补零)
按照低位先排序,分别放入10个队列中,然后采用先进先出的原则进行收集;再按照高位排序,然后再收集;依次类推,直到最高位,最终得到排好序的数列
对于整数可以使用上述方式,其他数据类型需要进行调整(找出特征值、优先级)
8.2 算法分析
- 稳定性 :稳定
- 时间复杂度 :最佳: 最差: 平均:
- 空间复杂度 :
8.3 代码实现
def radix_sort(arr):
# 找出最大数的位数
max_len = len(str(max(arr)))
for i in range(max_len): # i代表当前位
buckets = [[] for _ in range(10)] # 初始化0-9十个桶
for num in arr:
radix = int(num/(10**i) % 10) # int目的是将位数不够的数结果转为0
buckets[radix].append(num) # 放入对应的桶
j = 0
for k in range(10): # 按顺序对桶进行收集
for num in buckets[k]:
arr[j] = num
j += 1
return arr