插入排序(Insertion Sort)是一种直观且基础的排序算法,其核心思想源于日常生活中整理卡牌、排列书籍的逻辑 —— 将元素逐个插入到已排序序列的正确位置。

1、核心原理与步骤拆解

插入排序是一种原地、稳定的比较排序算法,其核心逻辑可概括为:

  1. 将待排序序列分为「已排序部分」和「未排序部分」(初始时,第一个元素为已排序部分,其余为未排序部分);

  2. 从末排序部分依次选取元素,将其与已排序部分的元素从后向前逐一比较;

  3. 找到该元素在已排序部分的正确位置并插入,同时将已排序部分中大于该元素的元素向后平移;

  4. 重复步骤 2-3,直到未排序部分为空,整个序列完成排序。

1.1 排序拆解

以数组 [8, 5, 3, 9, 1] 为例,直观展示插入排序的完整过程:

排序轮次

已排序部分

未排序部分

操作描述

初始状态

[8]

[5,3,9,1]

第一个元素 8 为初始已排序部分

第 1 轮

[5,8]

[3,9,1]

取未排序首元素 5,与 8 比较后插入到 8 左侧

第 2 轮

[3,5,8]

[9,1]

取未排序首元素 3,依次与 8、5 比较后插入到最左侧

第 3 轮

[3,5,8,9]

[1]

取未排序首元素 9,与 8 比较后直接接在 8 右侧

第 4 轮

[1,3,5,8,9]

取未排序首元素 1,依次与 9、8、5、3 比较后插入到最左侧

冒泡排序动画

1.2 伪代码

function insertionSort(arr):
    n = 数组长度
    // 从第2个元素开始(索引1),第一个元素默认已排序
    for i from 1 to n-1:
        current = arr[i]  // 当前待插入的元素
        j = i - 1        // 已排序部分的最后一个元素索引
        
        // 已排序部分从后向前比较,大于current的元素向后平移
        while j >= 0 and arr[j] > current:
            arr[j+1] = arr[j]  // 元素后移
            j = j - 1          // 向前移动索引
        
        // 找到正确位置,插入current
        arr[j+1] = current
    return arr

2、算法实现

2.1 Java

public class InsertionSort {
    // 基础插入排序(升序)
    public static void insertionSort(int[] arr) {
        // 边界判断:空数组或单元素数组无需排序
        if (arr == null || arr.length <= 1) {
            return;
        }
        
        int n = arr.length;
        // 从第2个元素开始遍历未排序部分
        for (int i = 1; i < n; i++) {
            int current = arr[i];  // 待插入元素
            int j = i - 1;         // 已排序部分的末尾索引
            
            // 已排序部分元素后移,为current腾出位置
            while (j >= 0 && arr[j] > current) {
                arr[j + 1] = arr[j];
                j--;
            }
            
            // 插入当前元素到正确位置
            arr[j + 1] = current;
        }
    }
    
    // 优化版:使用二分查找减少比较次数
    public static void optimizedInsertionSort(int[] arr) {
        if (arr == null || arr.length <= 1) {
            return;
        }
        
        int n = arr.length;
        for (int i = 1; i < n; i++) {
            int current = arr[i];
            // 二分查找当前元素在已排序部分的插入位置
            int left = 0, right = i - 1;
            while (left <= right) {
                int mid = left + (right - left) / 2;
                if (arr[mid] > current) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            }
            
            // 从插入位置开始,元素后移
            for (int j = i - 1; j >= left; j--) {
                arr[j + 1] = arr[j];
            }
            
            // 插入当前元素
            arr[left] = current;
        }
    }
}

2.2 Go

package main

// 基础插入排序(升序)
func insertionSort(arr []int) []int {
	// 边界判断:空数组或单元素数组无需排序
	if arr == nil || len(arr) <= 1 {
		// 返回副本避免修改原数组
		result := make([]int, len(arr))
		copy(result, arr)
		return result
	}

	// 创建数组副本,避免修改原数组
	result := make([]int, len(arr))
	copy(result, arr)
	n := len(result)

	// 从第2个元素开始遍历未排序部分
	for i := 1; i < n; i++ {
		current := result[i] // 待插入元素
		j := i - 1           // 已排序部分的末尾索引

		// 已排序部分元素后移,为current腾出位置
		for j >= 0 && result[j] > current {
			result[j+1] = result[j]
			j--
		}

		// 插入当前元素到正确位置
		result[j+1] = current
	}

	return result
}

// 优化版:使用二分查找减少比较次数
func optimizedInsertionSort(arr []int) []int {
	if arr == nil || len(arr) <= 1 {
		result := make([]int, len(arr))
		copy(result, arr)
		return result
	}

	result := make([]int, len(arr))
	copy(result, arr)
	n := len(result)

	for i := 1; i < n; i++ {
		current := result[i]
		left, right := 0, i-1

		// 二分查找插入位置
		for left <= right {
			mid := left + (right-left)/2
			if result[mid] > current {
				right = mid - 1
			} else {
				left = mid + 1
			}
		}

		// 元素后移
		for j := i; j > left; j-- {
			result[j] = result[j-1]
		}

		// 插入当前元素
		result[left] = current
	}

	return result
}

3、关键优化策略

插入排序的时间复杂度主要来自「比较次数」和「元素移动次数」,针对这两点可进行以下优化:

3.1 二分查找优化(减少比较次数)

核心思路:已排序部分是有序序列,可通过二分查找快速定位待插入元素的位置,将比较次数从 O (n) 降至 O (log n);

局限性:仅减少比较次数,元素移动次数仍为 O (n²),整体时间复杂度仍为 O (n²),但实际运行效率有明显提升(尤其数据量较大时)。

3.2 希尔排序(减少元素移动次数)

核心思路:将原数组按一定步长(如 n/2、n/4…1)划分为多个子数组,对每个子数组执行插入排序;随着步长减小,子数组规模增大,最终步长为 1 时完成全局排序;

优势:通过「预排序」减少后续插入时的元素移动距离,时间复杂度可优化至 O (n log n)~O (n²) 之间,是插入排序的重要改进版本。

3.3 链表优化(避免元素移动)

核心思路:若数据存储在链表中,插入元素时无需移动其他元素,只需调整指针指向;

局限性:链表无法随机访问,比较次数仍为 O (n²),仅适合数据频繁插入但查询较少的场景。

4、复杂度与特性分析

评估维度

结果

说明

时间复杂度(平均)

O(n²)

两层循环:外层遍历未排序元素(O (n)),内层比较 + 移动元素(平均 O (n))

时间复杂度(最好)

O(n)

数据已完全有序时,内层循环无需移动元素,仅需 O (n) 遍历

时间复杂度(最坏)

O(n²)

数据完全逆序时,每个元素需移动至已排序部分的最左侧

空间复杂度

O(1)

原地排序,仅需常数级临时变量(基础版)

稳定性

稳定

当元素相等时,不会改变其相对顺序(如 [2, 2, 1] 排序后仍保持两个 2 的顺序)

适应性

自适应

对接近有序的数据(如仅少数元素无序),性能接近 O (n)

5、与其他基础排序算法的对比

算法

平均时间复杂度

空间复杂度

稳定性

适用场景

核心优势

插入排序

O(n²)

O(1)

稳定

小规模数据、接近有序数据

实现简单、对有序数据友好

冒泡排序

O(n²)

O(1)

稳定

教学演示、数据量极小场景

逻辑直观、易于理解

选择排序

O(n²)

O(1)

不稳定

交换成本高的场景(如元素体积大)

交换次数少(每轮最多 1 次)

关键结论

  • 当数据规模较小时(n ≤ 1000),插入排序与 冒泡排序 、 选择排序 性能差距不大,但代码实现更简洁;

  • 当数据接近有序时(如日志数据、增量更新数据),插入排序性能远超 冒泡排序 和 选择排序 ,甚至接近 O (n);

  • 选择排序 虽交换次数少,但不稳定,且对数据有序性不敏感,整体场景适应性弱于插入排序。

6、实际应用场景

尽管插入排序时间复杂度为 O (n²),但在以下场景中仍有不可替代的价值:

  1. 小规模数据排序:如嵌入式系统、物联网设备等资源受限场景,数据量通常较小(n ≤ 100),插入排序的简单实现(低代码量、低内存占用)成为优势。

  2. 接近有序数据处理:如数据库查询结果的二次排序(查询结果已按某字段排序,需按另一字段微调)、日志系统的增量数据排序(新日志仅需插入到已有有序日志中)。

  3. 高级排序算法的子步骤:如快速排序、归并排序在处理小规模子数组时(通常 n ≤ 16),会切换为插入排序 —— 因为此时 O (n²) 算法的「常数项优势」(少递归、少函数调用)超过 O (n log n) 算法的复杂度优势。

  4. 链表数据结构排序:链表无法随机访问, 选择排序 、快速排序等依赖随机访问的算法性能下降,而插入排序仅需遍历 + 指针调整,更适合链表排序。