【一文了解】八大排序-插入排序、希尔排序

目录

1.插入排序

1.1.核心逻辑

1.2.适用场景

1.3.复杂度

1.4.稳定性

1.5.举例(升序为例)

1)核心动作拆解

2)实例分析(5,2,4,6,1

1.6.代码实现

2.希尔排序

2.1.核心逻辑

2.2.适用场景

2.3.复杂度

2.4.稳定性

2.5.举例(升序为例)

1)核心动作拆解

2)实例分析(12,34,54,2,3

2.6.代码实现

3.测试

3.1.完整代码

3.2.测试结果

4.总结


本篇文章来分享一下八大排序中的插入排序与希尔排序。

1.插入排序

1.1.核心逻辑

将元素逐个插入"已排序部分"的正确位置(类似整理手牌:拿一张,插入前面合适位置)

1.2.适用场景

小规模数据、部分有序数据

1.3.复杂度

1)时间复杂度:O(n²)(最坏/平均),O(n)(最优)

●最坏情况:O(n²)(完全逆序,每次插入需移动已排序部分所有元素)。

●平均情况:O(n²)(随机数据,插入位置平均在已排序部分中间,移动次数平均为n²/4)。

●最优情况:O(n)(完全有序,无需移动元素,仅需n-1次比较)。

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

1.4.稳定性

稳定(相同元素插入到已存在元素后方,不改变相对位置)

1.5.举例(升序为例)

1)核心动作拆解

分区思想:始终将数组分为"已排序部分"(左)和"未排序部分"(右),初始已排序部分只有第一个元素。

插入逻辑

从"未排序部分"取第一个元素作为"待插入元素"。

从"已排序部分"的末尾开始,向前逐个比较:

若"已排序元素"大于"待插入元素",则将"已排序元素"后移一位(腾出位置)。

若"已排序元素"小于或等于"待插入元素",则停止比较,将"待插入元素"插入到当前位置的后一位。

终止条件:"未排序部分"的所有元素都被插入到"已排序部分",此时整个数组有序。

2)实例分析(5,2,4,6,1

●初始状态

已排序部分:5(默认数组第一个元素有序)。

未排序部分:2, 4, 6, 1(需要逐个处理的元素)。

●插入第 1 个未排序元素 2

比较 2 和已排序部分的最后一个元素 5:2 < 5(逆序),需将 5 后移一位(腾出位置 → 已排序部分变为 5, 5

将 2 插入到空位(已排序部分的开头),结果:2, 5

本轮结果:

已排序部分:2, 5

未排序部分:4, 6, 1

●插入第 2 个未排序元素 4

比较 4 和已排序部分的最后一个元素 5:4 < 5(逆序),将 5 后移一位 → 已排序部分临时变为 2, 5, 5

比较 4 和前一个元素 2:4 > 2(正序),无需继续后移,找到插入位置。

将 4 插入到 2 和 5 之间,结果:2, 4, 5

本轮结果:

已排序部分:2, 4, 5

未排序部分:6, 1

●插入第 3 个未排序元素 6

比较 6 和已排序部分的最后一个元素 5:6 > 5(正序),无需移动任何元素。

直接将 6 插入到已排序部分的末尾,结果:2, 4, 5, 6

本轮结果:

已排序部分:2, 4, 5, 6

未排序部分:1

●插入最后一个未排序元素 1

比较 1 和 6:1 < 6 → 6 后移 → 临时 2, 4, 5, 6, 6

比较 1 和 5:1 < 5 → 5 后移 → 临时 2, 4, 5, 5, 6

比较 1 和 4:1 < 4 → 4 后移 → 临时 2, 4, 4, 5, 6

比较 1 和 2:1 < 2 → 2 后移 → 临时 2, 2, 4, 5, 6

已到已排序部分的开头,将 1 插入到最前面,结果:1, 2, 4, 5, 6

●最终结果:1, 2, 4, 5, 6

1.6.代码实现

cs 复制代码
/// <summary>
/// 插入排序
/// </summary>
/// <param name="arr">待排序数组(会直接修改原数组)</param>
/// <param name="isAscending">排序方向:true=升序,false=降序</param>
public static void InsertionSort<T>(T[] arr, bool isAscending = true) where T : IComparable<T>
{
    if (arr == null || arr.Length <= 1) return;

    int n = arr.Length;
    //外层循环:从第2个元素开始(第1个元素默认已序)
    for (int i = 1; i < n; i++)
    {
        T current = arr[i];//待插入的"当前元素"(暂存,避免后续移位覆盖)
        int j = i - 1;     //已排序部分的末尾索引

        //内层循环:将"已排序部分"中比current大/小的元素后移,腾出插入位置
        while (j >= 0)
        {
            int compareResult = arr[j].CompareTo(current);
            bool needSwap = isAscending ? (compareResult > 0) : (compareResult < 0);

            if (needSwap)
            {
                arr[j + 1] = arr[j];//元素后移
                j--;
            }
            else
            {
                break;//找到插入位置,退出循环
            }
        }

        //将current插入到正确位置(j+1:因循环结束时j已多减1)
        arr[j + 1] = current;
    }
}

2.希尔排序

2.1.核心逻辑

按"增量"将数组分组,对每组执行插入排序;逐步减小增量至1(此时数组接近有序,插入排序效率高)

2.2.适用场景

中等规模数据(n<10^5)、对稳定性无要求的场景

2.3.复杂度

1)时间复杂度:O(n^1.3)(平均,取决于增量选择)、O(n²)(最坏,增量为1时退化为插入排序,但实际表现仍优于直接插入排序),无最优情况O(n),因为需要多轮分组排序,即使数组有序,仍需处理增量过程。

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

2.4.稳定性

不稳定(分组排序时,相同元素可能被分到不同组,交换后破坏相对位置)

2.5.举例(升序为例)

1)核心动作拆解

核心思路:**"分组优化插入排序",**插入排序的效率在数组"基本有序"时极高(接近O(n)),但对完全无序的数组效率很低(O(n²))。希尔排序的优化逻辑是:

●先让数组"基本有序":通过"较大间隔"将数组分成多个子数组,对每个子数组执行插入排序(此时子数组长度短,插入排序效率高)。

●逐步缩小间隔:每轮排序后减小间隔,重复分组排序,使数组越来越接近有序。

●最后用间隔1排序:当间隔缩小到1时,数组已基本有序,此时执行一次插入排序即可完成最终排序(此时插入排序效率接近O(n))。

间隔(又称"增量")是希尔排序的核心参数,决定了如何分组:

●初始间隔通常取数组长度的一半(gap=n/2),后续每轮减半(gap=gap/2),直到gap=1(这是最经典的间隔选择方式,也可使用其他间隔序列如斐波那契数列)。

●分组规则:间隔为gap时,数组被分为gap个子数组,每个子数组包含"下标相差gap的元素"。例如,gap=2时,数组a0,a1,a2,a3,a4,a5被分为两个子数组:a0,a2,a4a1,a3,a5

2)实例分析(12,34,54,2,3

使用经典间隔:gap=2→gap=1。

●初始数组

12, 34, 54, 2, 3(长度 n=5)

●第 1 轮:间隔 gap=2(n/2=2,向下取整)

○分组:按 gap=2 分为 2 个子数组:

子数组 1:12, 54, 3(下标 0, 2, 4)

子数组 2:34, 2(下标 1, 3)

○ 对每个子数组执行插入排序:

子数组 1 排序:12, 54, 3

初始已排序部分:12,未排序元素:54、3。

插入 54:54 > 12(正序),直接放末尾 → 已排序部分:12, 54

插入 3:3 < 54 → 54 后移 → 临时 12, 54, 54; 3 < 12 → 12 后移 → 临时12, 12, 54 插入3到开头 → 已排序部分:3, 12, 54

子数组 2 排序:34, 2

初始已排序部分:34,未排序元素:2。

插入 2:2 < 34 → 34 后移 → 临时 34, 34 插入 2 到开头 → 已排序部分:2, 34

本轮结果:3, 2, 12, 34, 54(数组已更接近有序)

●第 2 轮:间隔 gap=1(gap/2=1)

○分组:gap=1 时,整个数组为一个子数组 3, 2, 12, 34, 54

○对每个子数组执行插入排序:

初始已排序部分:3,未排序元素:2、12、34、54。

插入 2:2 < 3 → 3 后移 → 临时 3, 3, 12, 34, 54,插入 2 到开头 → 已排序部分:2, 3

插入 12:12 > 3(正序),直接放末尾 → 已排序部分:2, 3, 12

插入 34:34 > 12(正序),直接放末尾 → 已排序部分:2, 3, 12, 34

插入 54:54 > 34(正序),直接放末尾 → 已排序部分:2, 3, 12, 34, 54

●最终结果:2, 3, 12, 34, 54

2.6.代码实现

cs 复制代码
/// <summary>
/// 希尔排序
/// </summary>
/// <param name="arr">待排序数组(会直接修改原数组)</param>
/// <param name="isAscending">排序方向:true=升序,false=降序</param>
public static void ShellSort<T>(T[] arr, bool isAscending = true) where T : IComparable<T>
{
    if (arr == null || arr.Length <= 1) return;

    int n = arr.Length;
    //增量初始值:数组长度的一半,后续每次减半(经典增量选择,简单高效)
    for (int gap = n / 2; gap > 0; gap /= 2)
    {
        //对每个"增量组"执行插入排序(i从gap开始,遍历所有组的元素)
        for (int i = gap; i < n; i++)
        {
            T current = arr[i];//待插入的当前元素
            int j = i;

            //组内元素比较:按增量gap向前遍历,比current大/小则移位
            while (j >= gap)
            {
                int compareResult = arr[j - gap].CompareTo(current);
                bool needSwap = isAscending ? (compareResult > 0) : (compareResult < 0);

                if (needSwap)
                {
                    arr[j] = arr[j - gap];//组内元素按增量移位
                    j -= gap;
                }
                else
                {
                    break;
                }
            }

            //插入当前元素到组内正确位置
            arr[j] = current;
        }
    }
}

3.测试

3.1.完整代码

cs 复制代码
using System;
using System.Collections.Generic;
using UnityEngine;

public class SortTest : MonoBehaviour
{
    private void Start()
    {
        int[] arr3 = GenerateArray(10);
        PrintArray(arr3);
        InsertionSort(arr3);
        PrintArray(arr3, "插入排序后,数组内容:");

        int[] arr4 = GenerateArray(10);
        PrintArray(arr4);
        ShellSort(arr4);
        PrintArray(arr4, "希尔排序后,数组内容:");
    }

    private int[] GenerateArray(int count, int minValue = 0, int maxValue = 100)
    {
        List<int> arr = new List<int>();
        for (int i = 0; i < count; i++)
        {
            int value = UnityEngine.Random.Range(minValue, maxValue);
            arr.Add(value);
        }
        return arr.ToArray();
    }
    /// <summary>
    /// 插入排序
    /// </summary>
    /// <param name="arr">待排序数组(会直接修改原数组)</param>
    /// <param name="isAscending">排序方向:true=升序,false=降序</param>
    public static void InsertionSort<T>(T[] arr, bool isAscending = true) where T : IComparable<T>
    {
        if (arr == null || arr.Length <= 1) return;

        int n = arr.Length;
        //外层循环:从第2个元素开始(第1个元素默认已序)
        for (int i = 1; i < n; i++)
        {
            T current = arr[i];//待插入的"当前元素"(暂存,避免后续移位覆盖)
            int j = i - 1;     //已排序部分的末尾索引

            //内层循环:将"已排序部分"中比current大/小的元素后移,腾出插入位置
            while (j >= 0)
            {
                int compareResult = arr[j].CompareTo(current);
                bool needSwap = isAscending ? (compareResult > 0) : (compareResult < 0);

                if (needSwap)
                {
                    arr[j + 1] = arr[j];//元素后移
                    j--;
                }
                else
                {
                    break;//找到插入位置,退出循环
                }
            }

            //将current插入到正确位置(j+1:因循环结束时j已多减1)
            arr[j + 1] = current;
        }
    }
    /// <summary>
    /// 希尔排序
    /// </summary>
    /// <param name="arr">待排序数组(会直接修改原数组)</param>
    /// <param name="isAscending">排序方向:true=升序,false=降序</param>
    public static void ShellSort<T>(T[] arr, bool isAscending = true) where T : IComparable<T>
    {
        if (arr == null || arr.Length <= 1) return;

        int n = arr.Length;
        //增量初始值:数组长度的一半,后续每次减半(经典增量选择,简单高效)
        for (int gap = n / 2; gap > 0; gap /= 2)
        {
            //对每个"增量组"执行插入排序(i从gap开始,遍历所有组的元素)
            for (int i = gap; i < n; i++)
            {
                T current = arr[i];//待插入的当前元素
                int j = i;

                //组内元素比较:按增量gap向前遍历,比current大/小则移位
                while (j >= gap)
                {
                    int compareResult = arr[j - gap].CompareTo(current);
                    bool needSwap = isAscending ? (compareResult > 0) : (compareResult < 0);

                    if (needSwap)
                    {
                        arr[j] = arr[j - gap];//组内元素按增量移位
                        j -= gap;
                    }
                    else
                    {
                        break;
                    }
                }

                //插入当前元素到组内正确位置
                arr[j] = current;
            }
        }
    }
    /// <summary>
    /// 打印数组内容
    /// </summary>
    /// <param name="arr"></param>
    public static void PrintArray<T>(T[] arr, string prefix = "数组内容:") where T : IComparable<T>
    {
        if (arr == null)
        {
            Debug.Log($"{prefix} null");
            return;
        }
        Debug.Log($"{prefix} [{string.Join(", ", arr)}]");
    }
}

3.2.测试结果

4.总结

插入排序与希尔排序同属"插入类排序",核心逻辑均基于"将元素插入已排序部分",但希尔排序通过"分组插入"优化了插入排序的效率。小数据/有序数据用插入排序(简单、稳定、高效);中大数据/无序数据用希尔排序(效率高、空间省)。二者共同覆盖了"小规模到中大规模"的排序需求,且都无需额外空间,适合内存有限的场景

|----------|-------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| 对比维度 | 插入排序(InsertionSort) | 希尔排序(ShellSort) |
| 核心逻辑 | 将元素逐个插入"已排序部分"的正确位置 | 按"增量"将数组分组,对每组执行插入排序;逐步减小增量至1(此时数组接近有序,插入排序效率高) |
| 时间复杂度 | O(n²)(最坏/平均),O(n)(最优) | O(n^1.3)(平均)、O(n²)(最坏) |
| 空间复杂度 | O(1)(原地排序,仅需1个临时变量存储当前待插入元素) | O(1)(原地排序,仅需临时变量存储待插入元素和增量) |
| 稳定性 | 稳定(插入时相同值元素不会被交换,保留原相对顺序) | 不稳定(分组插入时,相同值元素可能被分到不同子数组,插入后相对顺序被打乱) |
| 适用场景 | 1.小规模数据(n<1000); 2.接近完全有序的数据:可触发O(n)最优复杂度,效率远超选择/冒泡排序; 3.需稳定排序的场景(如排序含相同值的结构化数据); 4.数据流式处理(边接收数据边插入排序,实时维护有序序列)。 | 1.中大规模数据(n=1000~100000):效率远高于插入排序(O(n²)→O(n^(3/2))),且实现比快速排序简单; 2.对稳定性无要求的场景(如纯数值排序,无需保留相同值的原顺序); 3.硬件资源有限的场景(空间复杂度O(1),无额外内存消耗)。 |

想要了解冒泡排序和选择排序,可以参考【一文了解】八大排序-冒泡排序、选择排序

好了,本次的分享到这里就结束啦,希望对你有所帮助~

相关推荐
鱼鱼不愚与2 小时前
《原来如此 | 第01期:为什么导航软件能预测红绿灯倒计时?》
算法
唐青枫3 小时前
别只会反射:C#.NET Emit 动态生成代码实战详解
c#·.net
咕白m6256 小时前
.NET 环境下 Word 超链接批量提取方案
c#·.net
复杂网络6 小时前
论最小 Agent 计算机的形态
算法
用户91721561902117 小时前
C# 通信协议增量解析:用状态机处理半包和粘包
c#
kisshyshy1 天前
🍦 雪糕、食堂、火车厢:三幅漫画吃透栈、队列与链表
javascript·算法
小码编匠1 天前
C# 工控上位机必备:数据转换工具类与十个核心模块
后端·c#·.net
猿人谷1 天前
不只是 CPU 阈值:STAR 如何用 GAT + Transformer 做容器级自动扩缩容?
人工智能·算法
复杂网络1 天前
Stable Diffusion 视觉大模型微调技术深度调研
算法
复杂网络1 天前
基于 Stable Diffusion 架构的视觉大模型代表性工作与原理深度解析
算法