插入排序算法

1.直接插入排序

时间复杂度: O(n²)

插入排序是一种简单直观的排序算法,它的工作原理类似于我们打牌时整理手牌的过程:

  • 初始时,左手是空的

  • 每次从桌上拿起一张牌,将它插入到左手已排好序的牌中的正确位置

  • 重复这个过程,直到所有牌都插入到左手中

c 复制代码
void InsertSort(int* arr, int num)
{
    for (int i = 0; i < num - 1; i++)        // 外层循环:控制未排序元素
    {
        int end = i;                          // end 指向已排序区间的最后一个元素
        int tmp = arr[end + 1];               // tmp 保存待插入的元素(未排序区间的第一个)
        
        while (end >= 0)                      // 内层循环:在已排序区间中找到 tmp 的位置
        {
            if (tmp < arr[end])               // 如果待插入元素比当前元素小
            {
                arr[end + 1] = arr[end];      // 将当前元素向后移动一位
                end--;                         // 继续向前比较
            }
            else
            {
                break;                         // 找到正确位置,退出循环
            }
            arr[end + 1] = tmp;                // 将 tmp 插入到正确位置
        }
    }
}

变量解释:

  • i:指向已排序区间的最后一个元素的下标(即已排序区间长度为 i+1)

  • end:用于在已排序区间中从后向前遍历的指针

  • tmp:保存当前要插入的元素(即 arr[i+1])

执行流程:

  1. 外层循环从 i=0 到 i=num-2,表示每次处理一个未排序元素
  2. 将 arr[i+1] 保存到 tmp(这个元素是待插入的)
  3. 内层循环从 end=i 开始,从后向前遍历已排序区间
  4. 如果 tmp 比当前元素小,就把当前元素向后移动一位
  5. 如果找到合适位置(tmp >= arr[end]),就跳出循环
  6. 最后将 tmp 插入到正确位置(end+1)

那么其实这个是比较好理解的,我们来看一下动图来帮助我们更好的理解:

那么我们再来注意一丢丢的小细节:

为什么 tmp 要提前保存?

c 复制代码
int tmp = arr[end + 1];

如果不提前保存,当 arr[end+1] = arr[end] 执行后,arr[end+1] 的值就被覆盖了,我们就丢失了待插入元素的值

2.希尔排序

希尔排序简介:

希尔排序是插入排序的改进版,也称"缩小增量排序"。它的核心思想是:让数组先变得"基本有序",最后再进行一次插入排序。

为什么需要希尔排序?

  • 插入排序在数据基本有序时效率很高(接近 O(n))

  • 但数据完全无序时,插入排序需要大量移动元素(O(n²))

希尔排序通过引入"增量"的概念,先将数组分成若干子序列分别排序,使整个数组逐渐接近有序,最后用一次插入排序完成。

但实际上,希尔排序的时间复杂度很难计算,我们可以认为是O(n^1.3)。

那么我们怎么来实现希尔排序呢?

我们想,插入排序是不是有点慢呀,如果我们再进行插入排序之前先来预排序,就是先使数组里的元素尽可能的有序,那么再进行插入排序的话,那么这个效率是不是就大大提高了呀。

我们想,那怎么进行预排序呢,预排序是指把这个数组分为好几个小组,先对这几个小组排序,那么我们先来实现第一部分代码:

c 复制代码
for (int i = 0; i < num - gap; i += gap)
{
	int end = i;
	int tmp = arr[end + gap];
	while (end >= 0)
	{
		if (tmp < arr[end])
		{
			arr[end + gap] = arr[end];
			end -= gap;
		}
		else
		{
			break;
		}
		arr[end + gap] = tmp;
	}

我们看,这段代码的作用是什么呢?是不是就是对小组内元素进行排序的一个过程呀,或许有人可能会疑惑,这里为什么是i < num - gap呢?答案如下,当i等于num-gap的时候,此时tmp是不是就越界了呀,这样是不是不行,所以i < num - gap

或许这时候有老铁发现了,这段代码跟上面的插入排序是不是就是把gap的值换为1了呀,没错,就是这样的,当把1换成gap时,此时就是一组的预排序。

那么我们这个数组一共分为几个小组呢,是不是分成gap组呀,也就是我们对于每个小组去使它一组一组的进行排序:

c 复制代码
for (int j = 0; j < gap; j++)
{
	for (int i = j; i < num - gap; i += gap)
	{
		int end = i;
		int tmp = arr[end + gap];
		while (end >= 0)
		{
			if (tmp < arr[end])
			{
				arr[end + gap] = arr[end];
				end -= gap;
			}
			else
			{
				break;
			}
			arr[end + gap] = tmp;
		}
	}
}

此时你看,是不是就把这几组都预排序完了呀,那我们预排序完了是不是该进行插入排序了呀,也就是如下:

c 复制代码
//4层循环,一组一组排
void ShellSort(int* arr, int num)
{
	int gap = num;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		//gap>1时为预排序
		//gap==1时为插入排序
		for (int j = 0; j < gap; j++)
		{
			for (int i = j; i < num - gap; i += gap)
			{
				int end = i;
				int tmp = arr[end + gap];
				while (end >= 0)
				{
					if (tmp < arr[end])
					{
						arr[end + gap] = arr[end];
						end -= gap;
					}
					else
					{
						break;
					}
					arr[end + gap] = tmp;
				}
			}
		}
	}
}

当gap>1时为预排序gap==1时为插入排序对不对,然后这里可能会有小伙伴疑惑为什么这里是gap = gap / 3 + 1;,其实这个不是统一规定的,只是这样会更好一点点。

代码中使用 gap = gap / 3 + 1 作为增量序列,常见序列还有:

  • gap = gap / 2(简单但效率略低)

  • gap = gap / 3 + 1(Knuth 序列变种)

  • 其他更复杂的序列(如 Hibbard、Sedgewick)

那为什么 +1呢?

  • 保证最后一次循环 gap 一定为 1,从而进行一次完整的插入排序。

  • 如果不加1,gap 可能变成 0,导致死循环或排序不完整。

到此,希尔排序完结了,那可能还会有老铁说,这里4层循环,看的我脑袋都大了,实际上在这里我们可以进行小小的优化一下,使之变成3层循环:

c 复制代码
//3层循环,一起排
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		// +1保证最后一个gap一定是1
		// gap > 1时是预排序
		// gap == 1时是插入排序
		gap = gap / 3 + 1;

		for (size_t i = 0; i < n - gap; ++i)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

与第一种方法的区别:

  • 第一种方法:按分组分别处理(先处理完一组,再处理下一组)

  • 第二种方法:交替处理所有分组(每次只处理一个元素,然后 i++)

两种方法本质上等价,只是遍历顺序不同。第二种更简洁,且实际效率略高(减少了外层 j 循环)。

相关推荐
程序员小崔日记1 小时前
一道KMP统考真题彻底讲透:nextval与滑动距离的本质
算法·408·王道计算机考研
xiaoye-duck1 小时前
《算法题讲解指南:动态规划算法--路径问题》--9.最小路径和,10.地下城游戏
c++·算法·动态规划
渡过晚枫1 小时前
[第十四届蓝桥杯/java/算法]国赛A——跑步计划
算法
hanlin031 小时前
刷题笔记:力扣第17题-电话号码的字母组合
笔记·算法·leetcode
weixin_537590452 小时前
《C程序设计语言》练习答案(练习1-5)
c语言·开发语言
不是株2 小时前
算 法
数据结构·python·算法
云泽8082 小时前
蓝桥杯算法精讲:从宏观角度重新认识递归
算法·职场和发展·蓝桥杯
阿Y加油吧2 小时前
力扣打卡day09——缺失的第一个正数、矩阵置零
数据结构·算法·leetcode
2301_818419012 小时前
C++中的状态模式实战
开发语言·c++·算法