【一文了解】八大排序-插入排序、希尔排序
目录
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.代码实现
/// <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,a4]和[a1,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.代码实现
/// <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.完整代码
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),无额外内存消耗)。 |
想要了解冒泡排序和选择排序,可以参考【一文了解】八大排序-冒泡排序、选择排序
好了,本次的分享到这里就结束啦,希望对你有所帮助~
