八大排序之插入排序+希尔排序

一、插入排序(基础必懂)

1. 核心思想(打扑克理牌法)

把数组一刀切成两部分

  • 左边:已排序区(初始只有第一个元素)
  • 右边:未排序区(剩下所有元素)

核心动作 :每次从未排序区的第一个元素 拿出来,从后往前和已排序区的元素逐个比较,找到它应该在的位置,插入进去。直到未排序区为空。

2. 完整算法步骤

  1. 从下标i=1开始遍历数组(因为第一个元素默认已排序)
  2. 取出当前元素temp = arr[i](保存要插入的元素,防止被覆盖)
  3. 从已排序区的末尾j=i-1开始,从后往前 比较:
    • 如果arr[j] > temp:把arr[j]向后移动一位(arr[j+1] = arr[j]),j--
    • 如果arr[j] <= temp:找到插入位置,跳出循环
  4. temp插入到j+1的位置
  5. 重复步骤 2-4,直到i遍历完整个数组

3. 结合示例数组的逐元素详细过程

示例数组:[8, 9, 1, 7, 2, 3, 5, 4, 6, 0](长度 10,共 9 趟)

我挑 ** 最复杂的第 9 趟(处理最后一个元素 0)** 给你拆解每一步操作:

  • 此时已排序区:[1, 2, 3, 4, 5, 6, 7, 8, 9]
  • 未排序区第一个元素:0(下标i=9
  • 步骤 1:保存temp = 0
  • 步骤 2:j=8(已排序区最后一个元素下标)
    • 比较arr[8]=9 > 0 → 9 后移到下标 9 → 数组变为[1,2,3,4,5,6,7,8,9,9]j=7
    • 比较arr[7]=8 > 0 → 8 后移到下标 8 → 数组变为[1,2,3,4,5,6,7,8,8,9]j=6
    • 比较arr[6]=7 > 0 → 7 后移到下标 7 → 数组变为[1,2,3,4,5,6,7,7,8,9]j=5
    • ...(以此类推,所有比 0 大的元素都向后移一位)
    • 最终j=-1(已排序区所有元素都比 0 大)
  • 步骤 3:把temp=0插入到j+1=0的位置
  • 最终数组:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

4. 关键特性

  • 时间复杂度:最好 O (n)(数组已经有序),最坏 O (n²)(数组完全逆序),平均 O (n²)
  • 空间复杂度:O (1)(原地排序,只需要一个临时变量)
  • 稳定性稳定排序(相等元素的相对顺序不变)
  • 优点 :实现简单,对基本有序的数组效率极高
  • 缺点:对逆序数组效率极低(每个元素都要移动到最前面)
cpp 复制代码
void insertSort(int* arr, int n)
{
	for (int i = 1; i < n; i++)
	{
		int temp = arr[i];
		int j = i - 1;
		for ( ;j >= 0; j--)
		{
			if (arr[j] > temp)
			{
				arr[j + 1] = arr[j];
			}
			else
			{
				
				break;
			}
			
		}
		arr[j + 1] = temp;
	}
}

二、希尔排序(插入排序的 "超级改良版")

1. 为什么需要希尔排序?

插入排序有一个致命弱点:如果数组是完全逆序的,每个元素都要移动到最前面,时间复杂度直接拉满到 O (n²)。

希尔排序的天才想法:先让数组 "基本有序",再用插入排序收尾。这样最后一步插入排序的时间复杂度就会接近最好情况 O (n)。

2. 核心思想(分组预排序法)

  1. 分组 :选择一个增量 gap ,把整个数组分成gap个小组(下标相差 gap 的元素为一组
  2. 组内插排:对每个小组分别进行插入排序
  3. 缩小增量:把 gap 减半(或按其他规则缩小),重复步骤 1-2
  4. 最终插排 :当gap=1时,整个数组变成一个小组,此时就是普通插入排序

3. 增量序列的选择

最常用、最简单的是希尔原始增量序列

  • 初始gap = n/2(向下取整)
  • 每次gap = gap/2
  • 直到gap=1

(面试常考:还有其他更优的增量序列,比如 Knuth 序列:gap = 3*gap + 1,但原始增量最容易理解和实现)

4. 结合示例数组的逐 gap 详细过程

示例数组:[8, 9, 1, 7, 2, 3, 5, 4, 6, 0](长度 n=10)增量序列:gap=5 → gap=2 → gap=1


第 1 趟:gap=5(分 5 个小组,每组 2 个元素)

分组规则:下标差 5 的元素为一组

  • 第 1 组:下标 0 和 5 → 元素83
  • 第 2 组:下标 1 和 6 → 元素95
  • 第 3 组:下标 2 和 7 → 元素14
  • 第 4 组:下标 3 和 8 → 元素76
  • 第 5 组:下标 4 和 9 → 元素20

每组内部插入排序

  • 第 1 组:3 < 8 → 交换 → 变为[3, 8]
  • 第 2 组:5 < 9 → 交换 → 变为[5, 9]
  • 第 3 组:4 > 1 → 不变 → 还是[1, 4]
  • 第 4 组:6 < 7 → 交换 → 变为[6, 7]
  • 第 5 组:0 < 2 → 交换 → 变为[0, 2]

第 1 趟结束后的数组[3, 5, 1, 6, 0, 8, 9, 4, 7, 2]


第 2 趟:gap=2(分 2 个小组,每组 5 个元素)

分组规则:下标差 2 的元素为一组

  • 第 1 组(偶数下标):下标 0,2,4,6,8 → 元素3, 1, 0, 9, 7
  • 第 2 组(奇数下标):下标 1,3,5,7,9 → 元素5, 6, 8, 4, 2

第 1 组内部插入排序(逐个处理组内第 2 到第 5 个元素):

  • 处理1(下标 2):比3小 → 插入到最前 → 组变为[1, 3, 0, 9, 7]
  • 处理0(下标 4):比1,3都小 → 插入到最前 → 组变为[0, 1, 3, 9, 7]
  • 处理9(下标 6):比3大 → 不变 → 组还是[0, 1, 3, 9, 7]
  • 处理7(下标 8):比9小 → 插入到9前面 → 组变为[0, 1, 3, 7, 9]

第 2 组内部插入排序

  • 处理6(下标 3):比5大 → 不变 → 组变为[5, 6, 8, 4, 2]
  • 处理8(下标 5):比6大 → 不变 → 组还是[5, 6, 8, 4, 2]
  • 处理4(下标 7):比5,6,8都小 → 插入到最前 → 组变为[4, 5, 6, 8, 2]
  • 处理2(下标 9):比4,5,6,8都小 → 插入到最前 → 组变为[2, 4, 5, 6, 8]

第 2 趟结束后的数组 (把两个组的元素按原下标顺序放回):[0, 2, 1, 4, 3, 5, 7, 6, 9, 8]


第 3 趟:gap=1(整个数组一个小组,普通插入排序)

此时数组已经基本有序,插入排序只需要很少的比较和移动就能完成:

  • 处理1(下标 2):插入到02之间 → [0,1,2,4,3,5,7,6,9,8]
  • 处理3(下标 4):插入到24之间 → [0,1,2,3,4,5,7,6,9,8]
  • 处理6(下标 7):插入到57之间 → [0,1,2,3,4,5,6,7,9,8]
  • 处理8(下标 9):插入到79之间 → [0,1,2,3,4,5,6,7,8,9]

最终排序完成

5. 关键特性

  • 时间复杂度:最好 O (n),最坏 O (n²)(原始增量),平均 O (n^1.3)(远优于插入排序)
  • 空间复杂度:O (1)(原地排序)
  • 稳定性不稳定排序(分组预排序会打乱相等元素的相对顺序)
  • 优点:对中等规模数组效率很高,比插入排序快得多
  • 缺点:实现比插入排序复杂,增量序列的选择会影响效率
cpp 复制代码
void ShellSort(int* arr, int len)
{
	for (int gap = len / 2; gap > 0; gap /= 2)
	{
		for (int i = gap; i < len; i++)
		{
			int temp = arr[i];
			int j = i - gap;
			for (; j >= 0 && arr[j] > temp; j -= gap)
			{
				arr[j+gap] = arr[j];
			}
			arr[j + gap] = temp;
		}
	}
}

三、面试必考点对比

表格

特性 插入排序 希尔排序
核心思想 逐个插入已排序区 分组预排序 + 最终插排
时间复杂度(平均) O(n²) O(n^1.3)
稳定性 稳定 不稳定
适用场景 小规模、基本有序数组 中等规模数组
实现难度 简单 中等
相关推荐
kishu_iOS&AI2 小时前
机器学习 —— 逻辑回归(混淆矩阵)
人工智能·算法·机器学习·逻辑回归
W23035765732 小时前
经典算法:打家劫舍(动态规划 + 回溯求最优解)C++ 超详细解析
c++·算法·动态规划
Dev7z2 小时前
基于改进小波阈值的sEMG信号降噪与手势识别系统设计与实现
算法·手势识别·改进小波阈值·semg·信号降噪
灵感__idea9 小时前
Hello 算法:贪心的世界
前端·javascript·算法
澈20710 小时前
深入浅出C++滑动窗口算法:原理、实现与实战应用详解
数据结构·c++·算法
ambition2024211 小时前
从暴力搜索到理论最优:一道任务调度问题的完整算法演进历程
c语言·数据结构·c++·算法·贪心算法·深度优先
cmpxr_11 小时前
【C】原码和补码以及环形坐标取模算法
c语言·开发语言·算法
qiqsevenqiqiqiqi11 小时前
前缀和差分
算法·图论
代码旅人ing11 小时前
链表算法刷题指南
数据结构·算法·链表