Loading......

文章背景图

✨ 八大排序算法详解 ②

2026-01-13
6
-
- 分钟

一、简单选择排序

🔍 1. 核心思想

简单选择排序的核心是 “选择 + 交换”,基于贪心思想:每一轮从待排序区间中找到最小(或最大)元素,将其与待排序区间的第一个元素交换位置;每完成一轮,待排序区间长度减 1,直到待排序区间为空。整个过程通过 “找最小值→交换位置→缩小待排序区间” 三步循环实现,属于选择类排序。

📋 2. 核心执行流程

以测试用例数组 [49, 38, 65, 97, 76, 13, 27, 49] 为例:

  1. 初始待排序区间:[49, 38, 65, 97, 76, 13, 27, 49](索引 0~7)

    • 找到最小值 13(索引 5),与索引 0 的 49 交换 → [13, 38, 65, 97, 76, 49, 27, 49]

  2. 待排序区间:[38, 65, 97, 76, 49, 27, 49](索引 1~7)

    • 找到最小值 27(索引 6),与索引 1 的 38 交换 → [13, 27, 65, 97, 76, 49, 38, 49]

  3. 待排序区间:[65, 97, 76, 49, 38, 49](索引 2~7)

    • 找到最小值 38(索引 6),与索引 2 的 65 交换 → [13, 27, 38, 97, 76, 49, 65, 49]

  4. 重复上述步骤,依次缩小待排序区间,直到最后一轮待排序区间仅含 1 个元素(天然有序)。

  5. 最终有序数组:[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. 代码核心逻辑解析

  1. 基础版核心:

    • 外层循环 i:控制待排序区间的起始位置(0 到 n-2,因为最后 1 个元素无需排序);

    • 内层循环 j:遍历待排序区间(i+1 到 n-1),找到最小值的索引 min_idx

    • 交换操作:将最小值与待排序区间第一个元素(arr [i])交换,完成一轮排序。

  2. 优化版核心(二元选择):

    • 同时找最小值和最大值,一次循环处理两个元素,循环次数减少约一半;

    • 需注意:若最大值索引恰好是左边界(已被最小值交换),需更新最大值索引。

📝 5. 算法特点与优化策略

优点

  • 逻辑简单易懂,实现成本低;

  • 交换次数少(最多 n-1 次),优于冒泡排序。

缺点

  • 不稳定排序(例如 [2, 1, 2],第一个 2 会被交换到末尾,破坏相对位置);

  • 时间复杂度固定 O (n²),无论数组是否有序,效率都低。

优化策略

  • 二元选择排序(同时找最小 / 最大值);

  • 记录最小值索引时,跳过已确认有序的区间;

  • 小规模数据场景下可结合插入排序使用。


二、堆排序

🔍 1. 核心思想

堆排序基于堆(完全二叉树) 数据结构,核心是 “构建堆→交换堆顶→调整堆” 三步:

  • 大顶堆定义:每个父节点值 ≥ 子节点值,堆顶是全局最大值;

  • 构建大顶堆:将无序数组转换为大顶堆结构,使堆顶为最大值;

  • 交换堆顶与末尾:将最大值 “固定” 在数组末尾,缩小堆的范围;

  • 调整堆结构:对剩余元素重新调整为大顶堆,重复交换和调整,直到堆大小为 1。

📋 2. 核心执行流程

以测试用例数组 [49, 38, 65, 97, 76, 13, 27, 49] 为例:

  1. 构建大顶堆:数组调整为 [97, 76, 65, 49, 38, 13, 27, 49](堆顶 97 是最大值);

  2. 交换堆顶(97)与数组最后一个元素(49)→ [49, 76, 65, 49, 38, 13, 27, 97],堆大小减为 7;

  3. 调整前 7 个元素为大顶堆 → [76, 49, 65, 49, 38, 13, 27]

  4. 交换堆顶(76)与第 7 个元素(27)→ [27, 49, 65, 49, 38, 13, 76, 97],堆大小减为 6;

  5. 重复调整、交换,直到堆大小为 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. 代码核心逻辑解析

  1. 堆调整函数 sift_down

    • 作用:将以 start 为根的子树调整为大顶堆;

    • 逻辑:找到根节点的左右子节点,选择较大的子节点,若根节点小于该子节点则交换,然后递归调整交换后的子树。

  2. 构建大顶堆:

    • 从最后一个非叶子节点(索引 n//2 - 1)开始,向前遍历并调用 sift_down,确保每个子树都是大顶堆。

  3. 排序核心:

    • 循环交换堆顶(最大值)与当前堆的最后一个元素,将最大值 “固定” 在末尾;

    • 对剩余元素重新调用 sift_down 调整为大顶堆,直到堆大小为 1。

📝 5. 算法特点与优化策略

优点

  • 时间复杂度稳定 O (nlogn)(构建堆 O (n) + 调整堆 O (nlogn)),最坏情况也优于简单选择排序;

  • 原地排序(空间复杂度 O (1)),内存占用少;

  • 适合大规模数据排序。

缺点

  • 不稳定排序(交换堆顶与末尾时,相等元素相对位置可能改变);

  • 对小规模数据,效率不如插入 / 选择排序(堆调整的开销大于直接比较);

  • 缓存不友好(访问元素非连续,命中率低)。

优化策略

  • 小顶堆优化:若需降序排序,可构建小顶堆;

  • 结合插入排序:对小规模子堆(如长度 < 10)改用插入排序,减少堆调整开销;

  • 斐波那契堆:理论上优化调整效率,但实现复杂,实际应用少。


三、归并排序

🔍 1. 核心思想

归并排序基于分治思想,核心是 “先分后合”:

  • 拆分:将数组递归拆分为两个等长的子数组,直到子数组长度为 1(天然有序);

  • 合并:将两个有序子数组合并为一个有序数组,递归向上合并,最终得到完整的有序数组。

    核心特点是 “合并” 过程(双指针遍历两个有序子数组),是稳定排序的典型代表。

📋 2. 核心执行流程

以测试用例数组 [49, 38, 65, 97, 76, 13, 27, 49] 为例:

  1. 拆分阶段:

    • 原数组 → [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)。

  2. 合并阶段:

    • 合并 [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. 拆分逻辑:

    • 基线条件:子数组长度≤1 时直接返回(天然有序);

    • 中间索引 mid:将数组拆分为 arr[:mid](左)和 arr[mid:](右),递归排序左右子数组。

  2. 合并函数 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 策略):

  1. 初始数组:[49, 38, 65, 97, 76, 13, 27, 49]

  2. 个位分配 & 收集:

    • 个位数字: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]

  3. 十位分配 & 收集:

    • 十位数字: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. 代码核心逻辑解析

  1. 核心步骤:

    • 找最大数:确定需要处理的位数(如 97 是两位数,需处理个位和十位);

    • 逐位处理:从个位(digit=1)开始,到最高位结束;

    • 分配:按当前位数字将元素放入 0~9 号桶;

    • 收集:按桶的顺序(0 到 9)合并元素,完成当前位的排序。

  2. 处理负数:

    • 分离正负数,将负数转为绝对值排序;

    • 排序后反转负数列表(恢复升序),再与正数数组合并。

📝 5. 算法特点与优化策略

优点

  • 稳定排序(分配收集过程保留元素相对位置);

  • 时间复杂度低:O (d*(n+r)),d 为位数,r 为基数(通常 r=10),大规模整数排序效率高;

  • 无需比较元素大小,仅通过 “分配 - 收集” 实现排序。

缺点

  • 适用范围窄:仅支持整数 / 字符串等可按位拆分的类型;

  • 空间复杂度高(O (n+r),需要额外桶存储元素);

  • 处理多位数时,位数越多效率越低。

优化策略

  • 用计数排序替代桶排序:减少桶的空间开销,直接通过计数统计当前位的数字分布;

  • MSD 策略:从最高位开始排序,适合字符串 / 字典序排序;

  • 基数优化:对二进制数据,用 2^8(字节)作为基数,减少处理位数。


总结

排序算法

核心思想

时间复杂度(平均 / 最坏)

稳定性

空间复杂度

简单选择排序

选最小值 + 交换

O(n²)/O(n²)

不稳定

O(1)

堆排序

大顶堆 + 交换堆顶

O(nlogn)/O(nlogn)

不稳定

O(1)

归并排序

分治 + 合并有序子数组

O(nlogn)/O(nlogn)

稳定

O(n)

基数排序

按位分配 - 收集(LSD)

O(d*(n+r))/O(d*(n+r))

稳定

O(n+r)

关键点回顾

  1. 简单选择排序适合小数据量、对稳定性无要求的场景,核心是 “找最小值 + 交换”;

  2. 堆排序是原地 O (nlogn) 排序,适合大规模数据,但不稳定;

  3. 归并排序是稳定 O (nlogn) 排序,缺点是空间开销大;

  4. 基数排序无需比较元素,适合整数 / 字符串排序,需注意位数和负数处理

评论交流