1. 原理解析
插入排序的过程,就像我们平时打扑克牌时理牌的过程一样。 我们将数组分为两部分:已排序部分(起初只有第1个元素)和未排序部分(剩下的元素)。 每次从未排序部分拿出第一张“新牌”,在已排序部分中从后往前扫描。只要前面的“老牌”比“新牌”大,就把“老牌”往后挪一个位置,腾出空位。直到找到比“新牌”小或相等的“老牌”,或者比到了最前面,就把“新牌”插进那个空位里。
2. 使用场景
- 数据量较小:对于小规模数据(如 ),插入排序的常数开销很小,速度很快。很多高级排序算法(如快速排序、归并排序)在数据量切分到很小时,底层都会切换为插入排序。
- 数组基本有序:如果数组已经基本排好序,插入排序的内层循环几乎不需要移动元素,时间复杂度会退化到接近 ,效率极高。
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. 复杂度分析
- 时间复杂度:
- 最好情况:(数组已经升序,每次只比较一次就不挪动了)
- 最坏/平均情况:(数组降序或乱序,每次都要挪动大量元素)
- 空间复杂度:。原地排序,只借助了常数级别的额外空间(保存新抓的牌)。
- 稳定性:稳定。遇到值相同的元素时,我们不会进行交换或挪动(条件是
arr[j] > currentCard而不是>=),所以相同元素的相对顺序保持不变。