排序算法分类及实现
一、排序算法分类
可以从多个维度对排序算法进行分类,最常见的是按 时间复杂度 、空间复杂度 和 稳定性 来划分,但更本质的分类是按 工作原理。
1. 按时间复杂度分类
- O(n²) :冒泡排序、选择排序、插入排序
- O(n log n) :快速排序、归并排序、堆排序
- O(n) :计数排序、桶排序、基数排序(线性阶,但有前提条件)
2. 按空间复杂度分类
- 原地排序(O(1)):冒泡、选择、插入、希尔、堆排序、快速排序(平均递归栈O(log n))
- 非原地排序(O(n) 或更多):归并排序、计数排序、桶排序、基数排序
3. 按稳定性分类
- 稳定排序:相等元素的相对顺序在排序后保持不变(冒泡、插入、归并、计数、桶、基数)
- 不稳定排序:可能改变相等元素的相对顺序(选择、快速、堆、希尔)
4. 按工作原理分类(核心分类)
| 类别 | 代表算法 | 核心思想 |
|---|---|---|
| 比较排序 | 冒泡、选择、插入、归并、快速、堆 | 通过比较元素大小决定顺序,理论下限O(n log n) |
| 非比较排序 | 计数、基数、桶 | 利用数据本身的特性,不直接比较,可突破O(n log n)下限 |
| 交换排序 | 冒泡、快速 | 通过交换元素完成排序 |
| 插入排序 | 插入、希尔 | 构建有序序列,逐个插入 |
| 选择排序 | 简单选择、堆排序 | 每次选择最小(大)元素放到合适位置 |
| 分治排序 | 快速、归并 | 将问题分解为子问题,分别解决后合并 |
二、经典算法实现(Python示例)
1. 冒泡排序
核心思想 :像气泡一样,较大的元素会逐渐"浮"到顶端。
工作过程:
- 从左到右比较相邻元素,如果顺序不对就交换
- 每一轮将当前未排序部分的最大元素"冒泡"到正确位置
- 优化:如果某一轮没有发生交换,说明已排序完成
形象比喻 :
像水中的气泡,轻的气泡(小值)往上浮,重的气泡(大值)往下沉,但算法实际上是把重的气泡(大值)推到右边。
关键点:
- 稳定排序
- 每次确定一个元素的最终位置
- 冒泡排序代码实现 - 稳定
python
def bubble_sort(arr):
n = len(arr)
for i in range(n):
swapped = False
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped:
break
return arr
2. 选择排序
核心思想 :不断选择剩余元素中的最小值,放到已排序序列的末尾。
工作过程:
- 将数组分为已排序区和未排序区
- 在未排序区中找到最小元素
- 将其与未排序区的第一个元素交换,扩大已排序区
形象比喻 :
打扑克牌时,你把手里的牌摊开,每次都找出最小的那张放到左边,直到全部有序。
关键点:
- 不稳定(因为交换可能破坏相等元素的相对顺序)
- 无论数据如何,比较次数固定
- 选择排序 - 不稳定
python
def selection_sort(arr):
n = len(arr)
for i in range(n):
min_idx = i
for j in range(i + 1, n):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr
3. 插入排序
核心思想 :像整理扑克牌一样,将每个新元素插入到已排序序列的适当位置。
工作过程:
- 将数组分为已排序区和未排序区
- 从未排序区取第一个元素
- 在已排序区中从后向前扫描,找到合适位置插入
形象比喻 :
打扑克牌时,你一张张摸牌,每摸到一张新牌,就把它插入到手中已排序牌堆的合适位置。
关键点:
- 稳定排序
- 对部分有序数据效率很高(接近O(n))
- 插入排序 - 稳定
python
def insertion_sort(arr):
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and key < arr[j]:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
4. 快速排序
核心思想 :分而治之,选择一个"基准"元素,将数组分成两部分:小于基准和大于基准。
工作过程:
- 选择基准元素(pivot)
- 分区操作:将所有小于基准的元素移到左边,大于基准的移到右边
- 递归地对左右两个子数组进行快速排序
形象比喻 :
像整理书籍时,你随便选一本书作为参考,把比它薄的书放左边,比它厚的放右边,然后对左右两边重复这个过程。
关键点:
- 不稳定排序
- 平均性能最好(O(n log n))
- 最坏情况(已排序数组)是O(n²)
- 快速排序 - 不稳定(递归实现)
python
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
5. 归并排序
核心思想 :分而治之,先将数组不断拆分成最小单元,然后两两合并有序序列。
工作过程:
- 递归地将数组分成两半,直到每个子数组只有一个元素
- 合并两个有序子数组,形成新的有序数组
- 重复合并直到整个数组有序
形象比喻 :
合并两叠已经排好序的卡片:你比较两叠最上面的卡片,总是取较小的那张放到结果堆,直到所有卡片合并。
关键点:
- 稳定排序
- 需要额外存储空间(O(n))
- 时间复杂度稳定为O(n log n)
- 归并排序 - 稳定
python
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
6. 堆排序
核心思想 :利用堆(一种完全二叉树)数据结构进行排序。
工作过程:
- 建立最大堆(父节点值 ≥ 子节点值)
- 堆顶元素(最大值)与最后一个元素交换
- 缩小堆范围,重新调整堆
- 重复直到堆大小为1
形象比喻 :
像锦标赛:所有选手先进行预赛(建堆),冠军(最大值)确定后,让他退役(放到数组末尾),然后从剩下的选手中再决出冠军。
关键点:
- 不稳定排序
- 原地排序(O(1)额外空间)
- 时间复杂度稳定为O(n log n)
- 堆排序 - 不稳定
python
def heapify(arr, n, i):
largest = i
l = 2 * i + 1
r = 2 * i + 2
if l < n and arr[l] > arr[largest]:
largest = l
if r < n and arr[r] > arr[largest]:
largest = r
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest)
def heap_sort(arr):
n = len(arr)
for i in range(n // 2 - 1, -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)
return arr
7. 计数排序
核心思想 :不是比较元素大小,而是统计每个值出现的次数。
工作过程:
- 统计每个元素出现的次数
- 计算每个元素在排序后数组中的位置
- 按统计结果将元素放到正确位置
形象比喻 :
像投票计数:有10个候选人(值范围),统计每人的得票数,然后按得票数多少排出名次。
关键点:
- 稳定排序(通过反向填充实现)
- 只适用于整数且值范围不大的情况
- 时间复杂度O(n+k),k是值范围
- 计数排序 - 稳定(非比较,适合整数小范围)
python
def counting_sort(arr):
if not arr:
return []
max_val = max(arr)
count = [0] * (max_val + 1)
for num in arr:
count[num] += 1
for i in range(1, len(count)):
count[i] += count[i - 1]
output = [0] * len(arr)
for num in reversed(arr): # 反向遍历保证稳定性
output[count[num] - 1] = num
count[num] -= 1
return output
8. 基数排序
核心思想 :按位排序,从最低位到最高位依次排序。
工作过程:
- 从最低位开始,使用稳定排序算法(通常用计数排序)按该位排序
- 逐步向高位推进
- 当最高位排序完成后,整个数组就有序了
形象比喻 :
整理学生档案时,先按班级(个位)排序,再按年级(十位)排序,最后按学院(百位)排序。
关键点:
- 稳定排序
- 适用于整数、字符串等可分解为键的数据
- 时间复杂度O(nk),k是最大位数
LSD(最低位优先)基数排序 - 适用于整数
python
def radix_sort(arr):
"""
基数排序(LSD - 最低位优先)
适用于正整数,负数需要额外处理
"""
if not arr:
return arr
# 找到最大数确定最大位数
max_num = max(arr)
# 从个位开始,按每一位进行计数排序
exp = 1 # 1, 10, 100, ...
while max_num // exp > 0:
# 使用计数排序对当前位进行排序
counting_sort_by_digit(arr, exp)
exp *= 10
return arr
def counting_sort_by_digit(arr, exp):
"""
对数组arr按照某一位(exp决定)进行计数排序
"""
n = len(arr)
output = [0] * n # 输出数组
count = [0] * 10 # 0-9的数字计数
# 统计当前位上每个数字的出现次数
for i in range(n):
digit = (arr[i] // exp) % 10
count[digit] += 1
# 将count[i]更新为实际位置
for i in range(1, 10):
count[i] += count[i - 1]
# 反向填充保证稳定性
for i in range(n - 1, -1, -1):
digit = (arr[i] // exp) % 10
output[count[digit] - 1] = arr[i]
count[digit] -= 1
# 将排序结果复制回原数组
for i in range(n):
arr[i] = output[i]
# 测试基数排序
arr = [170, 45, 75, 90, 802, 24, 2, 66]
print("基数排序前:", arr)
print("基数排序后:", radix_sort(arr.copy()))
支持负数的基数排序
python
def radix_sort_with_negative(arr):
"""
支持负数的基数排序
"""
if not arr:
return arr
# 分离正数和负数
positives = [x for x in arr if x >= 0]
negatives = [-x for x in arr if x < 0] # 转换为正数处理
# 分别排序
if positives:
radix_sort(positives)
if negatives:
radix_sort(negatives)
negatives = negatives[::-1] # 反转,因为负数是反向的
negatives = [-x for x in negatives] # 转换回负数
# 合并结果:负数 + 正数
return negatives + positives
# 测试包含负数的基数排序
arr_with_neg = [-5, -100, 0, 123, -45, 67, -3, 99]
print("包含负数的数组:", arr_with_neg)
print("基数排序后:", radix_sort_with_negative(arr_with_neg.copy()))
9. 桶排序
核心思想 :将数据分到有限数量的桶里,每个桶单独排序,最后合并。
工作过程:
- 设置固定数量的空桶
- 将数据分配到对应的桶中
- 对每个非空桶进行排序
- 按顺序连接所有桶
形象比喻 :
邮局分拣信件:先按邮政编码范围分到不同格子里,然后对每个格子里的信件再按详细地址排序。
关键点:
- 稳定排序(取决于桶内使用的排序算法)
- 适用于数据均匀分布的情况
- 时间复杂度O(n+k),k是桶的数量
基本桶排序
python
def bucket_sort(arr, bucket_size=5):
"""
桶排序
bucket_size: 每个桶的大小范围,默认5
"""
if len(arr) == 0:
return arr
# 找出最大值和最小值
min_val = min(arr)
max_val = max(arr)
# 计算桶的数量
bucket_count = (max_val - min_val) // bucket_size + 1
buckets = [[] for _ in range(bucket_count)]
# 将数据分配到各个桶中
for num in arr:
# 计算当前元素应该放入哪个桶
bucket_index = (num - min_val) // bucket_size
buckets[bucket_index].append(num)
# 对每个桶进行排序(这里使用内置排序,也可以用其他排序算法)
sorted_arr = []
for bucket in buckets:
if bucket:
# 可以使用任何排序算法,这里用内置的sorted
bucket.sort() # 或 sorted(bucket)
sorted_arr.extend(bucket)
return sorted_arr
# 测试桶排序
arr = [0.42, 0.32, 0.23, 0.52, 0.25, 0.47, 0.51]
print("桶排序前:", arr)
print("桶排序后:", bucket_sort(arr.copy()))
三、算法比较总结表
| 算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 | 教学、小数据 |
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 | 教学、小数据 |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 | 小数据、基本有序 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 | 通用、数据随机 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 | 大数据、需稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 | 大数据、内存有限 |
| 计数排序 | O(n + k) | O(n + k) | O(n + k) | 稳定 | 整数小范围 |
| 基数排序 | O(nk) | O(nk) | O(n + k) | 稳定 | 整数、字符串 |
注:
k为计数排序中的值域范围或基数排序中的位数- 快速排序最坏情况可通过随机化或三数取中法避免
四、如何选择排序算法?
- 数据量小:插入排序(简单且对部分有序高效)
- 数据随机、要求快:快速排序(平均性能好)
- 要求稳定、大数据:归并排序
- 内存紧张:堆排序
- 已知数据范围小、整数:计数排序
- 链表结构:归并排序(适合链表)
- 基本有序:插入排序或冒泡排序