[机器学习]排序算法(快速排序,插入排序,冒泡排序,归并排序,堆排序)

297 阅读8分钟

1/快速排序(quick_sort)

关键词: 基准pivot(首,中间,尾都可以), left, middle,right, 递归

分治思想, 通过递归把问题分解成更小的子问题, 然后合并子问题的解即可.

最后返回,return quick_sort(left)+middle+quick_sort(right)

以下是使用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)  # 递归合并结果

# 测试示例
if __name__ == "__main__":
    test_array = [3, 6, 8, 10, 1, 2, 1]
    print("原始数组:", test_array)
    sorted_array = quick_sort(test_array)
    print("排序结果:", sorted_array)

代码说明:

  1. 基准选择:选择数组中间元素作为基准值(也可选择首/尾元素)
  2. 分区过程
    • left:所有小于基准值的元素
    • middle:所有等于基准值的元素
    • right:所有大于基准值的元素
  3. 递归排序:对左右子数组递归调用快速排序
  4. 合并结果:返回 左子数组 + 基准值 + 右子数组

时间复杂度:

  • 平均情况:O(n log n)
  • 最坏情况:O(n²)(当数组已排序且选择首/尾元素作为基准时)
  • 空间复杂度:O(n)(因创建新列表,需要额外存储空间)

平均时间复杂度的推导过程

  1. 递归深度‌:在平均情况下,每次划分能够将数组大致均分为两部分。因此,递归树的深度约为log₂n层。
  2. 每层操作次数‌:每层递归需要进行n次比较和交换操作。因此,每层的操作次数为O(n)。
  3. 总时间复杂度‌:总时间复杂度为n乘以递归层数,即n × log n = O(n log n)。

测试输出:

原始数组: [3, 6, 8, 10, 1, 2, 1]
排序结果: [1, 1, 2, 3, 6, 8, 10]


2/插入排序(insert_sort)

关键词: 整理扑克牌, 从小到大排序

从第二张牌开始, 检查其大小, 是否可以插在前面的牌中.这就是插入排序.

原理说明

插入排序是一种简单直观的排序算法,其工作原理类似于整理扑克牌:

  1. 将数组分为已排序和未排序两部分:初始时已排序部分只包含第一个元素
  2. 逐个处理未排序元素:从未排序部分取出第一个元素
  3. 在已排序部分找到合适位置插入:将取出的元素与已排序部分的元素从后向前比较,找到合适的位置插入
  4. 重复直到完成:重复上述过程直到所有元素都被处理

算法特点

  • 时间复杂度
    • 最佳情况(已排序):O(n)
    • 平均情况:O(n²)
    • 最坏情况(逆序):O(n²)
  • 空间复杂度:O(1)(原地排序)
  • 稳定性:稳定排序(相同元素相对位置不变)
  • 适用场景:小规模数据或基本有序的数据集

Python 实现代码

def insertion_sort(arr):
    """
    插入排序实现
    :param arr: 待排序的列表
    :return: 原地排序(不返回新列表)
    """
    # 就像整理扑克牌一样, 从第二张牌开始整理, 即index=1
    # 从第二个元素开始(索引1),直到最后一个元素
    for i in range(1, len(arr)):
        key = arr[i]   # 当前要插入的元素
        j = i - 1      # 已排序部分的最后一个元素的索引
        
        # 在已排序部分中寻找插入位置(从后向前扫描)
        # 把key放在已排序部分中最合适的位置上
        while j >= 0 and key < arr[j]:
            arr[j + 1] = arr[j]  # 元素后移
            j -= 1
        
        # 把要插入的元素放在合适的位置
        arr[j + 1] = key  

# 测试示例
if __name__ == "__main__":
    # 测试数据
    test_cases = [
        [5, 2, 4, 6, 1, 3],
        [3, 1, 4, 1, 5, 9, 2, 6],
        [10, 9, 8, 7, 6, 5],
        [1],
        []  # 空数组测试
    ]
    
    for i, arr in enumerate(test_cases):
        print(f"测试用例 {i+1}:")
        print("排序前:", arr)
        insertion_sort(arr)
        print("排序后:", arr)
        print("-" * 30)

代码执行过程示例(以 [5, 2, 4, 6, 1, 3] 为例)

初始: [5, 2, 4, 6, 1, 3]
步骤1: [2, 5, 4, 6, 1, 3]  (插入2)
步骤2: [2, 4, 5, 6, 1, 3]  (插入4)
步骤3: [2, 4, 5, 6, 1, 3]  (插入6,位置不变)
步骤4: [1, 2, 4, 5, 6, 3]  (插入1)
步骤5: [1, 2, 3, 4, 5, 6]  (插入3)

输出结果

测试用例 1:
排序前: [5, 2, 4, 6, 1, 3]
排序后: [1, 2, 3, 4, 5, 6]
------------------------------
测试用例 2:
排序前: [3, 1, 4, 1, 5, 9, 2, 6]
排序后: [1, 1, 2, 3, 4, 5, 6, 9]
------------------------------
测试用例 3:
排序前: [10, 9, 8, 7, 6, 5]
排序后: [5, 6, 7, 8, 9, 10]
------------------------------
测试用例 4:
排序前: [1]
排序后: [1]
------------------------------
测试用例 5:
排序前: []
排序后: []
------------------------------

插入排序在小规模数据或基本有序的数据集上表现良好,是许多高级排序算法(如Timsort)的组成部分。




3/冒泡排序(Bubble Sort)

关键词: 2个for循环

原理

冒泡排序是一种简单的排序算法,通过重复遍历待排序列表,比较相邻元素并交换顺序错误的元素(升序排列时将较大元素后移)。每轮遍历会将当前未排序部分的最大元素“冒泡”到正确位置。遍历持续进行,直到所有元素有序。

核心步骤

  1. 从第一个元素开始,比较相邻的两个元素
  2. 如果顺序错误(前 > 后),交换它们
  3. 对每一对相邻元素重复上述操作,直到列表末尾
  4. 每轮遍历后,最大的元素会沉到末尾(因此下一轮无需比较已排序部分)
  5. 重复直到没有元素需要交换(列表已排序)

时间复杂度

  • 最优:O(n)(当列表已有序时)
  • 平均:O(n²)
  • 最差:O(n²)

空间复杂度:O(1)(原地排序)


Python 实现

def bubble_sort(arr):
    n = len(arr)
    # 遍历所有元素
    for i in range(n):
        swapped = False  # 优化:标记本轮是否有交换
        # 最后i个元素已有序,无需比较
        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

# 测试示例
if __name__ == "__main__":
    data = [64, 34, 25, 12, 22, 11, 90]
    print("排序前:", data)
    bubble_sort(data)
    print("排序后:", data)

输出

排序前: [64, 34, 25, 12, 22, 11, 90]
排序后: [11, 12, 22, 25, 34, 64, 90]

关键优化

  • swapped 标志位:若某轮遍历未发生交换,说明列表已有序,提前终止排序,减少不必要的比较。

可视化过程(以 [5, 1, 4, 2] 为例):

初始: [5, 1, 4, 2]
第一轮: 
  比较 5>1 → 交换:[1, 5, 4, 2]
  比较 5>4 → 交换:[1, 4, 5, 2]
  比较 5>2 → 交换:[1, 4, 2, 5]  # 5沉底
第二轮:
  比较 1<4 → 不交换
  比较 4>2 → 交换:[1, 2, 4, 5]  # 4沉底
第三轮:
  无交换 → 排序完成



4/归并排序(Merge Sort)

什么是归并排序?

归并排序是一种高效的分治算法(Divide and Conquer),它将一个大问题分解为多个小问题, 然后解决小问题,再将结果合并。

核心操作是合并两个有序序列,时间复杂度稳定为 O(n log n),是稳定排序算法。

核心原理(三步走)

  1. 分(Divide)
    将数组递归地对半拆分,直到每个子数组只剩1个元素(天然有序)

  2. 治(Conquer)
    递归排序子数组(当数组长度为1时自动完成排序)

  3. 合(Merge)
    将两个已排序的子数组合并成一个有序数组:

    • 比较两个子数组的头部元素
    • 取较小值放入结果数组
    • 重复直到所有元素合并完成

直观理解

想象整理一副扑克牌:

  1. 把整副牌分成两半 → 再分半 → 直到每堆只剩1张牌(分)
  2. 两两比较相邻的牌堆,按顺序合并(合)
  3. 重复合并过程直到所有牌合并成有序的一副牌

Python 代码实现

def merge_sort(arr):
    # 递归终止条件:数组长度≤1时已有序
    if len(arr) <= 1:
        return arr
    
    # 分:将数组对半拆分
    mid = len(arr) // 2
    left_arr = merge_sort(arr[:mid])  # 递归排序左半部
    right_arr = merge_sort(arr[mid:]) # 递归排序右半部
    
    # 合:合并两个有序数组
    return merge(left_arr, right_arr)

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

# 测试示例
if __name__ == "__main__":
    arr = [3, 7, 6, 1, 9, 5, 4, 2]
    print("原始数组:", arr)
    sorted_arr = merge_sort(arr)
    print("排序结果:", sorted_arr)

🚀 输出示例:

原始数组: [3, 7, 6, 1, 9, 5, 4, 2]
排序结果: [1, 2, 3, 4, 5, 6, 7, 9]

关键特性

特性说明
时间复杂度始终 O(n log n) - 高效稳定
空间复杂度O(n) - 需要额外存储空间
稳定性✅ 是(相等元素顺序不变)
适用场景大数据量、链表排序、外部排序
🌟 学习要点
  • 分治思想:大问题 → 小问题 → 合并解
  • 双指针技巧:合并时的核心操作
  • 递归实现:简洁但需注意栈深度限制(大数据量可改迭代实现)

归并排序是理解分治算法的经典案例,也是许多高效排序算法(如TimSort)的基础组件。