插入排序 (Insertion Sort)

0 阅读3分钟

1. 原理解析

插入排序的过程,就像我们平时打扑克牌时理牌的过程一样。 我们将数组分为两部分:已排序部分(起初只有第1个元素)和未排序部分(剩下的元素)。 每次从未排序部分拿出第一张“新牌”,在已排序部分中从后往前扫描。只要前面的“老牌”比“新牌”大,就把“老牌”往后挪一个位置,腾出空位。直到找到比“新牌”小或相等的“老牌”,或者比到了最前面,就把“新牌”插进那个空位里。

2. 使用场景

  • 数据量较小:对于小规模数据(如 N<50N < 50),插入排序的常数开销很小,速度很快。很多高级排序算法(如快速排序、归并排序)在数据量切分到很小时,底层都会切换为插入排序。
  • 数组基本有序:如果数组已经基本排好序,插入排序的内层循环几乎不需要移动元素,时间复杂度会退化到接近 O(N)O(N),效率极高。

3. 代码实战

Java 版本

public class InsertionSort {
    public static void insertionSort(int[] arr) {
        if (arr == null || arr.length <= 1) return;

        // 外层循环:从第2张牌(索引为1)开始抓牌,直到最后一张
        for (int i = 1; i < arr.length; i++) {
            int currentCard = arr[i]; // 右手刚抓到的新牌
            int j = i - 1;            // 左手最右边那张牌的索引

            // 内层循环:新牌和左手里的牌从右往左依次比较
            // 如果左手里的牌比新牌大,就把左手的牌往右边挪一个空位
            while (j >= 0 && arr[j] > currentCard) {
                arr[j + 1] = arr[j];  // 往后挪一位,腾出空间
                j--;                  // 继续和前面一张牌比较
            }

            // 退出 while 循环时,说明找到了比新牌小(或相等)的牌,或者已经比到头了(j < 0)
            // 此时 j+1 就是新牌应该插入的正确位置
            arr[j + 1] = currentCard;
        }
    }
}

Python 版本

def insertion_sort(arr):
    if not arr or len(arr) <= 1:
        return
    
    # 外层循环:从第2张牌(索引为1)开始抓牌
    for i in range(1, len(arr)):
        current_card = arr[i]  # 右手刚抓到的新牌
        j = i - 1              # 左手最右边那张牌的索引
        
        # 内层循环:如果左手里的牌比新牌大,就往右挪
        while j >= 0 and arr[j] > current_card:
            arr[j + 1] = arr[j] # 往后挪一位
            j -= 1              # 继续往前看
            
        # 找到正确位置,把新牌插进去
        arr[j + 1] = current_card

4. 核心难点/易错点详解

  • 内层循环的越界问题:在 while (j >= 0 ...) 中,必须保证 j >= 0 在前,利用短路求值防止数组越界。因为当你一直比到最左边(即 j 减到 -1 时),说明新牌是当前最小的,不能再去取 arr[-1]
  • 插入位置的确定:很多初学者容易搞错最后插入的位置究竟是 j 还是 j+1
    • 生活例子:假设你手里有一张牌 5,新抓了一张牌 3。你对比发现 5 > 3,于是把 5 往右挪到了原本 3 的位置(也就是 j+1 的位置)。这时候你手里的目光 j 继续往前看,发现前面没牌了(j 变成了 -1)。退出循环时,那个空出来的坑其实是在 -1 + 1 = 0 的位置。所以永远是插在 j + 1 的位置!

5. 复杂度分析

  • 时间复杂度
    • 最好情况:O(N)O(N)(数组已经升序,每次只比较一次就不挪动了)
    • 最坏/平均情况:O(N2)O(N^2)(数组降序或乱序,每次都要挪动大量元素)
  • 空间复杂度:O(1)O(1)。原地排序,只借助了常数级别的额外空间(保存新抓的牌)。
  • 稳定性:稳定。遇到值相同的元素时,我们不会进行交换或挪动(条件是 arr[j] > currentCard 而不是 >=),所以相同元素的相对顺序保持不变。