插入排序算法

一、算法概述

插入排序是一种稳定的原地排序算法,属于插入类排序的范畴。其核心特性是"逐元素插入、逐步有序":将待排序序列分为"已排序区域"和"未排序区域",初始时已排序区域仅包含序列的第一个元素,未排序区域包含剩余所有元素;然后依次从末排序区域中取出一个元素,将其插入到已排序区域中对应的正确位置(保证已排序区域始终有序),重复该过程,直到未排序区域为空,整个序列即完成排序。

二、核心原理与排序流程

插入排序的核心逻辑简单直观,本质是"逐步扩大有序区域",通过将未排序元素插入到已排序区域的正确位置,实现序列的有序化。以下以升序排序为例,详细拆解其核心原理与完整排序流程,降序排序仅需调整插入逻辑即可。

1. 核心思想

插入排序的核心思想可概括为"分区域、逐插入",具体可拆解为两个核心步骤,形成循环闭环:

  • 初始化区域:将待排序序列的第一个元素作为已排序区域,剩余元素作为未排序区域;

  • 逐元素插入:从末排序区域中取出第一个元素(当前待插入元素),与已排序区域的元素从后向前依次比较,找到待插入元素的正确位置,将其插入该位置(插入过程中,已排序区域中大于待插入元素的元素需向后移动,为待插入元素腾出位置);

  • 循环终止:重复执行"逐元素插入"步骤,每插入一个元素,已排序区域的长度增加1,未排序区域的长度减少1;当未排序区域为空时,排序终止,此时已排序区域即为整个有序序列。

需要注意的是,插入排序的核心优势在于"移动代替交换":冒泡排序需要通过频繁的相邻元素交换实现排序,而插入排序仅需将元素向后移动,每插入一个元素最多移动一次(或多次连续移动),减少了交换操作的开销(一次交换包含三次赋值,一次移动仅包含一次赋值),因此在相同数据规模下,插入排序的实际运行效率优于冒泡排序。

2. 排序流程(示例演示)

为了更直观地理解插入排序的流程,我们以一个具体的无序序列 ​ [5,3,8,4,2] (升序排序)为例,详细演示每一步的排序过程:​

  1. 初始状态:已排序区域 ​ [5] (索引0),未排序区域 ​ [3,8,4,2] (索引1-4);取出未排序区域第一个元素3(待插入元素);​ 与已排序区域元素5比较:3<5,将5向后移动一位(序列变为 ​ [5,5,8,4,2] );​ 已排序区域遍历完毕,将3插入到最前端(索引0);​ 插入后:已排序区域 ​ [3,5] ,未排序区域 ​ [8,4,2] 。

​ 2. 第二轮插入:​取出未排序区域第一个元素8(待插入元素);​ 与已排序区域末尾元素5比较:8>5,无需移动元素,直接将8插入到已排序区域末尾(索引2); 插入后:已排序区域 ​ [3,5,8] ,未排序区域 ​ [4,2] 。

​ 3. 第三轮插入: 取出未排序区域第一个元素4(待插入元素);与已排序区域末尾元素8比较:4<8,将8向后移动一位(序列变为 ​ [3,5,8,8,2] );与已排序区域元素5比较:4<5,将5向后移动一位(序列变为 ​ [3,5,5,8,2] );与已排序区域元素3比较:4>3,停止比较,将4插入到3和5之间(索引2);插入后:已排序区域 ​ [3,4,5,8] ,未排序区域 ​ [2] 。

​ 4. 第四轮插入:​取出未排序区域唯一元素2(待插入元素);与已排序区域末尾元素8比较:2<8,将8向后移动一位;与已排序区域元素5比较:2<5,将5向后移动一位;与已排序区域元素4比较:2<4,将4向后移动一位;与已排序区域元素3比较:2<3,将3向后移动一位(序列变为 ​ [3,3,4,5,8] );​已排序区域遍历完毕,将2插入到最前端(索引0);插入后:已排序区域 ​ [2,3,4,5,8] ,未排序区域为空,排序终止。​

最终排序结果: ​ [2,3,4,5,8] 。通过该示例可见,插入排序的核心是"逐步扩大有序区域",每插入一个元素,都能保证已排序区域始终有序,且移动操作的开销远低于冒泡排序的交换操作。

三、算法实现(以Java为例)

插入排序的实现逻辑简单,分为基础实现和优化实现两种。基础实现适用于理解算法原理,优化实现(如折半插入排序)可减少比较次数,进一步提升算法效率。以下分别给出两种实现的代码示例,并附带详细注释,便于读者理解和调试。

1. 基础实现(直接插入排序)

基础实现严格遵循插入排序的核心流程,直接从后向前遍历已排序区域,找到待插入元素的正确位置并插入。这种实现逻辑简单,易于编码,适合初学者理解算法原理,在小规模数据排序场景中表现良好。

java 复制代码
/**
 * 插入排序基础实现(直接插入排序,升序排序)
 * @param arr 待排序数组
 */
public static void insertionSortBasic(int[] arr) {
    // 边界校验:数组为空或长度小于2,无需排序,直接返回
    if (arr == null || arr.length < 2) {
        return;
    }
    int n = arr.length;
    // 外层循环:控制未排序区域,从索引1开始(索引0为初始已排序区域)
    for (int i = 1; i < n; i++) {
        int current = arr[i]; // 取出当前待插入元素(未排序区域的第一个元素)
        int j = i - 1; // j指向已排序区域的末尾元素(初始为i-1)
        
        // 内层循环:从后向前遍历已排序区域,找到待插入元素的正确位置
        // 条件:j >= 0(未遍历完已排序区域)且 current < arr[j](待插入元素小于当前遍历元素)
        while (j >= 0 && current < arr[j]) {
            arr[j + 1] = arr[j]; // 将当前遍历元素向后移动一位,腾出位置
            j--; // 继续向前遍历
        }
        // 循环结束,j+1即为待插入元素的正确位置,插入当前元素
        arr[j + 1] = current;
    }
}

2. 优化实现(折半插入排序)

基础实现的核心问题是:在已排序区域中查找待插入位置时,采用线性遍历(从后向前),比较次数较多。折半插入排序通过"折半查找"(二分查找)替代线性遍历,快速找到待插入元素的正确位置,减少比较次数,提升算法效率。

java 复制代码
/**
 * 插入排序优化实现(折半插入排序,升序排序)
 * 核心优化:用折半查找替代线性遍历,减少比较次数
 * @param arr 待排序数组
 */
public static void insertionSortBinary(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    int n = arr.length;
    for (int i = 1; i < n; i++) {
        int current = arr[i]; // 当前待插入元素
        int left = 0; // 已排序区域左边界
        int right = i - 1; // 已排序区域右边界
        
        // 折半查找:找到待插入元素的正确位置(插入到left位置)
        while (left <= right) {
            int mid = left + (right - left) / 2; // 计算中间位置,避免溢出
            if (current < arr[mid]) {
                right = mid - 1; // 待插入元素在左半区域
            } else {
                left = mid + 1; // 待插入元素在右半区域(包含等于的情况,保证稳定性)
            }
        }
        
        // 移动元素:将left到i-1位置的元素向后移动一位,腾出插入位置
        for (int j = i - 1; j >= left; j--) {
            arr[j + 1] = arr[j];
        }
        // 插入当前元素到正确位置left
        arr[left] = current;
    }
}

3. 进阶优化(希尔排序入门)

插入排序的进阶优化是希尔排序(Shell Sort),其核心思想是"分组插入排序":将待排序序列按照一定的步长(gap)分为多个子序列,对每个子序列分别执行插入排序;逐步缩小步长,重复分组和插入排序操作,直到步长为1,此时整个序列变为一个子序列,执行最后一次插入排序,完成排序。

java 复制代码
/**
 * 插入排序进阶优化(希尔排序,升序排序)
 * 核心思想:分组插入排序,逐步缩小步长,提升效率
 * @param arr 待排序数组
 */
public static void shellSort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    int n = arr.length;
    // 初始化步长gap,通常取n/2,逐步缩小为gap/2,直到gap=1
    for (int gap = n / 2; gap > 0; gap /= 2) {
        // 对每个分组执行插入排序
        for (int i = gap; i < n; i++) {
            int current = arr[i];
            int j = i - gap; // j指向当前分组中已排序区域的对应元素
            // 分组内插入排序逻辑,与直接插入排序类似
            while (j >= 0 && current < arr[j]) {
                arr[j + gap] = arr[j];
                j -= gap;
            }
            arr[j + gap] = current;
        }
    }
}

四、关键细节与常见问题

插入排序的逻辑看似简单,但在实际实现过程中,仍有一些关键细节需要注意,避免出现逻辑错误或效率低下的问题。以下总结几个核心注意点和常见问题,帮助读者规避误区。

1. 边界条件的校验

与冒泡排序、快速排序一致,插入排序也必须优先进行边界校验:当数组为空(null)或数组长度小于2时,无需进行排序操作,直接返回。这不仅能避免空指针异常、数组索引越界异常,还能减少无效的循环操作,提升算法效率。

2. 待插入元素的保存

在实现直接插入排序时,必须先将当前待插入元素(arr[i])保存到临时变量(current)中,再进行元素移动操作。若不保存,直接移动元素会覆盖arr[i]的值,导致待插入元素丢失,排序失败。这是插入排序实现中最常见的错误之一。

3. 稳定性的保持

插入排序的稳定性是其核心优势之一,保持稳定性的关键是:在折半查找时,当待插入元素与已排序区域元素相等时,将待插入元素插入到相等元素的右侧(即left = mid + 1);在直接插入排序时,仅当current < arr[j]时才移动元素,不处理current == arr[j]的情况。这样可保证相等元素的原始相对位置不发生改变,保留稳定性。

4. 折半插入排序的注意点

折半插入排序中,折半查找仅用于找到待插入位置,元素移动操作仍需通过循环实现,无法减少移动次数。因此,它的核心优势是减少比较次数,适合已排序区域较长的场景(如大规模接近有序的序列)。

相关推荐
bubiyoushang8881 小时前
基于遗传算法的LQR控制器最优设计算法
开发语言·算法·matlab
西门吹-禅1 小时前
【eclipse 升级】
java·ide·eclipse
每天要多喝水1 小时前
图论Day39:孤岛题目
算法·深度优先·图论
Seven971 小时前
剑指offer-78、求平⽅根
java
玄〤1 小时前
个人博客网站搭建day6--Spring Boot自定义RedisTemplate配置:优化序列化与Java8时间类型支持
java·spring boot·redis·后端·spring
知我Deja_Vu2 小时前
@Transactional 与 @Transactional(rollbackFor = Exception.class) 的区别详解
java·spring
兩尛2 小时前
648. 单词替换
算法
敲敲千反田2 小时前
CAS和AQS相关问题
java
廋到被风吹走2 小时前
稳定性保障:限流降级深度解析 —— Sentinel滑动窗口算法与令牌桶实现
运维·算法·sentinel