一、简单选择排序
🔍 1. 核心思想
简单选择排序的核心是 “选择 + 交换”,基于贪心思想:每一轮从待排序区间中找到最小(或最大)元素,将其与待排序区间的第一个元素交换位置;每完成一轮,待排序区间长度减 1,直到待排序区间为空。整个过程通过 “找最小值→交换位置→缩小待排序区间” 三步循环实现,属于选择类排序。
📋 2. 核心执行流程
以测试用例数组 [49, 38, 65, 97, 76, 13, 27, 49] 为例:
-
初始待排序区间:
[49, 38, 65, 97, 76, 13, 27, 49](索引 0~7)-
找到最小值 13(索引 5),与索引 0 的 49 交换 →
[13, 38, 65, 97, 76, 49, 27, 49]
-
-
待排序区间:
[38, 65, 97, 76, 49, 27, 49](索引 1~7)-
找到最小值 27(索引 6),与索引 1 的 38 交换 →
[13, 27, 65, 97, 76, 49, 38, 49]
-
-
待排序区间:
[65, 97, 76, 49, 38, 49](索引 2~7)-
找到最小值 38(索引 6),与索引 2 的 65 交换 →
[13, 27, 38, 97, 76, 49, 65, 49]
-
-
重复上述步骤,依次缩小待排序区间,直到最后一轮待排序区间仅含 1 个元素(天然有序)。
-
最终有序数组:
[13, 27, 38, 49, 49, 65, 76, 97]
💻 3. Python 代码实现
python
运行
def selection_sort(arr):
"""
简单选择排序(升序)
:param arr: 待排序数组(列表)
:return: 排序后的数组(不修改原数组)
"""
arr_copy = arr.copy()
n = len(arr_copy)
# 外层循环:控制待排序区间的起始位置(0 ~ n-2)
for i in range(n - 1):
min_idx = i # 初始化最小值索引为待排序区间第一个元素
# 内层循环:遍历待排序区间,找到最小值索引
for j in range(i + 1, n):
if arr_copy[j] < arr_copy[min_idx]:
min_idx = j
# 交换最小值与待排序区间第一个元素
arr_copy[i], arr_copy[min_idx] = arr_copy[min_idx], arr_copy[i]
return arr_copy
# 优化版:二元选择排序(同时找最小/最大值,减少循环次数)
def selection_sort_optimized(arr):
arr_copy = arr.copy()
n = len(arr_copy)
left = 0
right = n - 1
while left < right:
min_idx = left
max_idx = right
# 一次遍历同时找最小、最大值索引
for j in range(left, right + 1):
if arr_copy[j] < arr_copy[min_idx]:
min_idx = j
if arr_copy[j] > arr_copy[max_idx]:
max_idx = j
# 交换最小值到左边界
arr_copy[left], arr_copy[min_idx] = arr_copy[min_idx], arr_copy[left]
# 若最大值索引是左边界(已被交换),更新最大值索引
if max_idx == left:
max_idx = min_idx
# 交换最大值到右边界
arr_copy[right], arr_copy[max_idx] = arr_copy[max_idx], arr_copy[right]
# 缩小待排序区间
left += 1
right -= 1
return arr_copy
# 测试用例
if __name__ == "__main__":
test_arr = [49, 38, 65, 97, 76, 13, 27, 49]
res1 = selection_sort(test_arr)
res2 = selection_sort_optimized(test_arr)
print("原始数组:", test_arr)
print("基础版排序后:", res1) # [13, 27, 38, 49, 49, 65, 76, 97]
print("优化版排序后:", res2) # [13, 27, 38, 49, 49, 65, 76, 97]
🔍 4. 代码核心逻辑解析
-
基础版核心:
-
外层循环
i:控制待排序区间的起始位置(0 到 n-2,因为最后 1 个元素无需排序); -
内层循环
j:遍历待排序区间(i+1 到 n-1),找到最小值的索引min_idx; -
交换操作:将最小值与待排序区间第一个元素(arr [i])交换,完成一轮排序。
-
-
优化版核心(二元选择):
-
同时找最小值和最大值,一次循环处理两个元素,循环次数减少约一半;
-
需注意:若最大值索引恰好是左边界(已被最小值交换),需更新最大值索引。
-
📝 5. 算法特点与优化策略
优点:
-
逻辑简单易懂,实现成本低;
-
交换次数少(最多 n-1 次),优于冒泡排序。
缺点:
-
不稳定排序(例如
[2, 1, 2],第一个 2 会被交换到末尾,破坏相对位置); -
时间复杂度固定 O (n²),无论数组是否有序,效率都低。
优化策略:
-
二元选择排序(同时找最小 / 最大值);
-
记录最小值索引时,跳过已确认有序的区间;
-
小规模数据场景下可结合插入排序使用。
二、堆排序
🔍 1. 核心思想
堆排序基于堆(完全二叉树) 数据结构,核心是 “构建堆→交换堆顶→调整堆” 三步:
-
大顶堆定义:每个父节点值 ≥ 子节点值,堆顶是全局最大值;
-
构建大顶堆:将无序数组转换为大顶堆结构,使堆顶为最大值;
-
交换堆顶与末尾:将最大值 “固定” 在数组末尾,缩小堆的范围;
-
调整堆结构:对剩余元素重新调整为大顶堆,重复交换和调整,直到堆大小为 1。
📋 2. 核心执行流程
以测试用例数组 [49, 38, 65, 97, 76, 13, 27, 49] 为例:
-
构建大顶堆:数组调整为
[97, 76, 65, 49, 38, 13, 27, 49](堆顶 97 是最大值); -
交换堆顶(97)与数组最后一个元素(49)→
[49, 76, 65, 49, 38, 13, 27, 97],堆大小减为 7; -
调整前 7 个元素为大顶堆 →
[76, 49, 65, 49, 38, 13, 27]; -
交换堆顶(76)与第 7 个元素(27)→
[27, 49, 65, 49, 38, 13, 76, 97],堆大小减为 6; -
重复调整、交换,直到堆大小为 1,最终得到有序数组
[13, 27, 38, 49, 49, 65, 76, 97]。
💻 3. Python 代码实现
python
运行
def heap_sort(arr):
"""
堆排序(升序,基于大顶堆)
:param arr: 待排序数组(列表)
:return: 排序后的数组(不修改原数组)
"""
arr_copy = arr.copy()
n = len(arr_copy)
def sift_down(arr, start, end):
"""
堆调整函数(下沉):将以start为根的子树调整为大顶堆
:param start: 子树根节点索引
:param end: 堆的最后一个元素索引
"""
root = start
while True:
# 左子节点索引
child = 2 * root + 1
# 子节点超出堆范围则退出
if child > end:
break
# 选择左右子节点中较大的那个
if child + 1 <= end and arr[child] < arr[child + 1]:
child += 1
# 若根节点 < 子节点,交换并继续下沉
if arr[root] < arr[child]:
arr[root], arr[child] = arr[child], arr[root]
root = child
else:
break
# 1. 构建大顶堆(从最后一个非叶子节点开始调整)
# 最后一个非叶子节点索引:n//2 - 1
for i in range(n // 2 - 1, -1, -1):
sift_down(arr_copy, i, n - 1)
# 2. 交换堆顶与末尾,调整堆结构
for end in range(n - 1, 0, -1):
arr_copy[0], arr_copy[end] = arr_copy[end], arr_copy[0]
sift_down(arr_copy, 0, end - 1)
return arr_copy
# 测试用例
if __name__ == "__main__":
test_arr = [49, 38, 65, 97, 76, 13, 27, 49]
result = heap_sort(test_arr)
print("原始数组:", test_arr)
print("堆排序后:", result) # [13, 27, 38, 49, 49, 65, 76, 97]
🔍 4. 代码核心逻辑解析
-
堆调整函数
sift_down:-
作用:将以
start为根的子树调整为大顶堆; -
逻辑:找到根节点的左右子节点,选择较大的子节点,若根节点小于该子节点则交换,然后递归调整交换后的子树。
-
-
构建大顶堆:
-
从最后一个非叶子节点(索引
n//2 - 1)开始,向前遍历并调用sift_down,确保每个子树都是大顶堆。
-
-
排序核心:
-
循环交换堆顶(最大值)与当前堆的最后一个元素,将最大值 “固定” 在末尾;
-
对剩余元素重新调用
sift_down调整为大顶堆,直到堆大小为 1。
-
📝 5. 算法特点与优化策略
优点:
-
时间复杂度稳定 O (nlogn)(构建堆 O (n) + 调整堆 O (nlogn)),最坏情况也优于简单选择排序;
-
原地排序(空间复杂度 O (1)),内存占用少;
-
适合大规模数据排序。
缺点:
-
不稳定排序(交换堆顶与末尾时,相等元素相对位置可能改变);
-
对小规模数据,效率不如插入 / 选择排序(堆调整的开销大于直接比较);
-
缓存不友好(访问元素非连续,命中率低)。
优化策略:
-
小顶堆优化:若需降序排序,可构建小顶堆;
-
结合插入排序:对小规模子堆(如长度 < 10)改用插入排序,减少堆调整开销;
-
斐波那契堆:理论上优化调整效率,但实现复杂,实际应用少。
三、归并排序
🔍 1. 核心思想
归并排序基于分治思想,核心是 “先分后合”:
-
拆分:将数组递归拆分为两个等长的子数组,直到子数组长度为 1(天然有序);
-
合并:将两个有序子数组合并为一个有序数组,递归向上合并,最终得到完整的有序数组。
核心特点是 “合并” 过程(双指针遍历两个有序子数组),是稳定排序的典型代表。
📋 2. 核心执行流程
以测试用例数组 [49, 38, 65, 97, 76, 13, 27, 49] 为例:
-
拆分阶段:
-
原数组 →
[49,38,65,97]+[76,13,27,49]; -
继续拆分 →
[49,38]+[65,97]+[76,13]+[27,49]; -
最终拆分 →
[49]+[38]+[65]+[97]+[76]+[13]+[27]+[49](子数组长度为 1)。
-
-
合并阶段:
-
合并
[49]&[38]→[38,49];合并[65]&[97]→[65,97];合并[76]&[13]→[13,76];合并[27]&[49]→[27,49]; -
合并
[38,49]&[65,97]→[38,49,65,97];合并[13,76]&[27,49]→[13,27,49,76]; -
最终合并 →
[38,49,65,97]&[13,27,49,76]→[13,27,38,49,49,65,76,97]。
-
💻 3. Python 代码实现
python
运行
def merge_sort(arr):
"""
归并排序(递归实现,升序)
:param arr: 待排序数组(列表)
:return: 排序后的数组(不修改原数组)
"""
# 基线条件:子数组长度≤1时直接返回
if len(arr) <= 1:
return arr.copy()
# 1. 拆分:找到中间索引,拆分为左右子数组
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
# 2. 合并:双指针合并两个有序子数组
def merge(left_arr, right_arr):
merged = []
i = j = 0
# 双指针遍历,取较小值加入结果
while i < len(left_arr) and j < len(right_arr):
if left_arr[i] <= right_arr[j]:
merged.append(left_arr[i])
i += 1
else:
merged.append(right_arr[j])
j += 1
# 追加剩余元素(左/右子数组可能有未遍历完的元素)
merged.extend(left_arr[i:])
merged.extend(right_arr[j:])
return merged
return merge(left, right)
# 测试用例
if __name__ == "__main__":
test_arr = [49, 38, 65, 97, 76, 13, 27, 49]
result = merge_sort(test_arr)
print("原始数组:", test_arr)
print("归并排序后:", result) # [13, 27, 38, 49, 49, 65, 76, 97]
🔍 4. 代码核心逻辑解析
-
拆分逻辑:
-
基线条件:子数组长度≤1 时直接返回(天然有序);
-
中间索引
mid:将数组拆分为arr[:mid](左)和arr[mid:](右),递归排序左右子数组。
-
-
合并函数
merge:-
双指针
i/j:分别指向左右子数组的起始位置; -
循环比较:取
left[i]和right[j]中的较小值加入结果数组,移动对应指针; -
追加剩余元素:循环结束后,左 / 右子数组可能有未遍历的元素,直接追加到结果末尾。
-
📝 5. 算法特点与优化策略
优点:
-
稳定排序(合并时相等元素保留原顺序);
-
时间复杂度稳定 O (nlogn)(拆分 O (logn) + 合并 O (n)),最坏情况仍高效;
-
适合外排序(数据无法全部加载到内存时)。
缺点:
-
空间复杂度高(O (n),需要额外数组存储合并结果);
-
递归实现有栈开销(可改用迭代实现)。
优化策略:
-
原地归并:优化合并过程,无需额外数组(实现较复杂);
-
小规模优化:子数组长度 < 15 时,改用插入排序(减少递归和合并开销);
-
多路归并:拆分时分为多个子数组,减少合并次数。
四、基数排序
🔍 1. 核心思想
基数排序属于分配式排序,核心是按 “位” 排序(个位→十位→百位…),基于 LSD(最低位优先)策略:
-
分配:根据当前位的数字(0~9),将元素放入对应编号的桶中;
-
收集:按桶的顺序(0 到 9)取出元素,形成新数组;
-
逐位处理:从最低位到最高位重复 “分配→收集”,最终数组整体有序。
仅适用于可按位拆分的类型(整数、字符串等)。
📋 2. 核心执行流程
以测试用例数组 [49, 38, 65, 97, 76, 13, 27, 49] 为例(LSD 策略):
-
初始数组:
[49, 38, 65, 97, 76, 13, 27, 49] -
按个位分配 & 收集:
-
个位数字:9、8、5、7、6、3、7、9 → 分配到桶 3 (13)、桶 5 (65)、桶 6 (76)、桶 7 (97,27)、桶 8 (38)、桶 9 (49,49);
-
收集后:
[13, 65, 76, 97, 27, 38, 49, 49]。
-
-
按十位分配 & 收集:
-
十位数字:1、6、7、9、2、3、4、4 → 分配到桶 1 (13)、桶 2 (27)、桶 3 (38)、桶 4 (49,49)、桶 6 (65)、桶 7 (76)、桶 9 (97);
-
收集后:
[13, 27, 38, 49, 49, 65, 76, 97](已有序)。
-
💻 3. Python 代码实现
python
运行
def radix_sort(arr):
"""
基数排序(LSD策略,仅处理非负整数)
:param arr: 待排序数组(列表)
:return: 排序后的数组(不修改原数组)
"""
if not arr:
return []
arr_copy = arr.copy()
# 找到最大数,确定需要处理的位数
max_num = max(arr_copy)
digit = 1 # 当前处理的位数(个位=1,十位=10,百位=100...)
while max_num // digit > 0:
# 初始化10个桶(0~9)
buckets = [[] for _ in range(10)]
# 1. 分配:按当前位数字放入对应桶
for num in arr_copy:
# 获取当前位的数字
current_digit = (num // digit) % 10
buckets[current_digit].append(num)
# 2. 收集:按桶顺序合并元素
arr_copy = []
for bucket in buckets:
arr_copy.extend(bucket)
# 处理下一位
digit *= 10
return arr_copy
# 扩展:处理负数的基数排序
def radix_sort_with_negative(arr):
if not arr:
return []
# 分离正负数,分别排序后合并
negatives = [-x for x in arr if x < 0]
positives = [x for x in arr if x >= 0]
# 对负数的绝对值排序后反转,恢复负数顺序
sorted_negatives = [-x for x in reversed(radix_sort(negatives))]
sorted_positives = radix_sort(positives)
return sorted_negatives + sorted_positives
# 测试用例
if __name__ == "__main__":
test_arr = [49, 38, 65, 97, 76, 13, 27, 49]
result1 = radix_sort(test_arr)
print("非负基数排序后:", result1) # [13, 27, 38, 49, 49, 65, 76, 97]
test_arr2 = [49, -38, 65, -97, 76, 13, -27, 49]
result2 = radix_sort_with_negative(test_arr2)
print("支持负数排序后:", result2) # [-97, -38, -27, 13, 49, 49, 65, 76]
🔍 4. 代码核心逻辑解析
-
核心步骤:
-
找最大数:确定需要处理的位数(如 97 是两位数,需处理个位和十位);
-
逐位处理:从个位(digit=1)开始,到最高位结束;
-
分配:按当前位数字将元素放入 0~9 号桶;
-
收集:按桶的顺序(0 到 9)合并元素,完成当前位的排序。
-
-
处理负数:
-
分离正负数,将负数转为绝对值排序;
-
排序后反转负数列表(恢复升序),再与正数数组合并。
-
📝 5. 算法特点与优化策略
优点:
-
稳定排序(分配收集过程保留元素相对位置);
-
时间复杂度低:O (d*(n+r)),d 为位数,r 为基数(通常 r=10),大规模整数排序效率高;
-
无需比较元素大小,仅通过 “分配 - 收集” 实现排序。
缺点:
-
适用范围窄:仅支持整数 / 字符串等可按位拆分的类型;
-
空间复杂度高(O (n+r),需要额外桶存储元素);
-
处理多位数时,位数越多效率越低。
优化策略:
-
用计数排序替代桶排序:减少桶的空间开销,直接通过计数统计当前位的数字分布;
-
MSD 策略:从最高位开始排序,适合字符串 / 字典序排序;
-
基数优化:对二进制数据,用 2^8(字节)作为基数,减少处理位数。
总结
关键点回顾:
-
简单选择排序适合小数据量、对稳定性无要求的场景,核心是 “找最小值 + 交换”;
-
堆排序是原地 O (nlogn) 排序,适合大规模数据,但不稳定;
-
归并排序是稳定 O (nlogn) 排序,缺点是空间开销大;
-
基数排序无需比较元素,适合整数 / 字符串排序,需注意位数和负数处理