插入排序⁻⁻⁻⁻直接插入排序&希尔排序

引言

所谓的排序,就是使一串记录按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

常见的排序算法有:

今天我们主要学习插入排序的直接插入排序和希尔排序。

直接插入排序

什么是直接插入排序?

直接插入排序其实是一种很简单的排序算法哦。它就像我们平时整理扑克牌一样,一张一张地来,把每一张牌都插到已经排好序的部分里面,直到全部都排好。

我们再通过一个动图理解一下:

思路

  1. 从第二个元素始,逐一遍历数组。
  2. 当前元素与前序已排序元素逐一比较,找到插入位置。
  3. 插入当前元素至正确位置,前序元素后移。
  4. 重复上述步骤,直至数组全排序。

代码

void insertSort(int* a, int n)
{
	for (int i = 1; i < n; i++) {
		int curIndex = i - 1, tmp = a[i];
		while (curIndex >= 0) {
			if (tmp < a[curIndex]) {
				a[curIndex + 1] = a[curIndex];
				--curIndex;
			}
			else {
				break;
			}
		}
		a[curIndex + 1] = tmp;
	}
}

时间复杂度

从代码我们可以看出,直接插入排序的时间复杂度是 O(n²),在数据比较少或者已经部分有序的情况下,它的性能还是不错的。

希尔排序

什么是希尔排序?

希尔排序,又称为缩小增量排序,是直接插入排序的一种更高效的改进版本。

前面我们知道,直接插入排序比最差的条件下时间复杂度可以达到O(n²),但是在部分有序的情况下,它的性能还是不错的。

希尔排序的基本思想是预排序,即先将数组内的数据变得部分有序,最后再通过直接插入排序将部分有序的数组进组排序。

那它具体是如何实现预排序的呢?接下来我们会进行讲解。

讲解与分析

接下来,我们具体来通过一个例子来理解一下希尔排序:先将整个待排序数组分割成若干个子序列(也称为分组),使得每个子序列中的元素在原数组中的位置间隔gap,然后对每个子序列分别进行直接插入排序,这样可以使原数组变得部分有序。我们一起来看一下:

原数组:int a[]={9,1,2,5,7,4,8,6,3,5};

接下来我们按照间隔(gap)为3时将原数组进行升序排序,什么意思呢?即下标为0,3,6,9的树为一组进行排序,对应上面的数组也就是将9,5,8,5进行排序:

tmp之前的数据已经按升序排序,开始进入下一轮排序:

tmp之前的数据已经按升序排序,开始进行下一轮排序:

如果我们让gap为2,原数组排序下来会是这样的:

我们发现,gap越大,排序就越快,但是越不接近有序;gap越小,排序就越慢,但是越接近有序。

当gap等于1就是直接插入排序,gap大于1就是预排序。

那我们可以先让gap初始化为较大的数,我们可以逐渐缩小gap,再对新的子序列进行直接插入排序,直到最后增量减至1,此时整个数组几乎已经有序,最后再进行一次直接插入排序即可完成排序。

这里补充说明一下gap的初始值,一般初始化为数组长的一半或三分之一。如果是数组长度的二分之一,每次gap就是按照gap/=2去缩小;如果是数组长的三分之一,每次gap就是按照gap=gap/3+1去缩小(为什么不是gap/=3呢?我们要考虑到两个整数相除时,最后结果会向下取整,如果数组长度n=6,第一次排序时gap=n/3=2,第二次排序时gap/=3就是2/3=0,那这样下去永远都无法让gap==1,也就无法进行直接插入排序。所以应该是gap=gap/3+1)。

代码

void shellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1) {
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i += gap) {
			int curIndex = i, tmp = a[i + gap];
			while (curIndex >= 0) {
				if (tmp < a[curIndex]) {
					a[curIndex + gap] = a[curIndex];
					curIndex -= gap;
				}
				else {
					break;
				}
			}
			a[curIndex + gap] = tmp;
		}
	}
	
}

时间复杂度

分析

  • 最外层循环:控制间隔(gap)的变化,通常从数组长度n开始,每次除以一个常数(如3)并加1,直到gap为1。此循环的时间复杂度为O(logn)。
  • 内层循环(这种分析方式存在一定不严谨性):包括两层循环,一层用于遍历数组,另一层用于在分组内进行插入排序。for循环以gap为步长遍历数组,因此其迭代次数为n / gap次。在每次for循环的迭代中,while循环负责将当前元素(位于i + gap位置)插入到其前面gap间隔的有序子序列中。最坏情况下,这个插入操作需要比较和移动gap个元素(但实际上由于分组的存在,通常不会达到这么多)。然而,对于时间复杂度的分析,我们可以认为每次while循环至多执行O(gap)次操作,但在整个for循环中,由于分组的存在,每个元素至多被比较和移动一次。因此,对于每个固定的gap值,内层循环(包括for和while)的总时间复杂度为O(n)。

注意:严谨的内层循环时间复杂度分析

1.gap较大时:

  • 当gap较大时,数组被分成较少的组,每组包含较多的元素。
  • 由于此时数组尚未排序,每组内的插入排序可能需要进行较多的比较和交换。
  • 但由于组数较少,整体时间复杂度相对较低。

2.gap逐渐减小时:

  • 随着gap的减小,数组被分成更多的组,每组包含的元素减少。
  • 由于前面的排序过程,数组已经逐渐接近有序状态,因此每组内的插入排序所需的比较和交换次数减少。
  • 此时,虽然组数增多,但每组内的排序工作量减少,整体时间复杂度仍然保持较低水平。

3.gap变化过程中的复杂度峰值:

  • 在gap从大到小变化的过程中,存在一个复杂度峰值。这是因为当gap处于某个中间值时,数组既没有被完全分组(如gap很大时),也没有接近有序(如gap很小时)。
  • 在这个峰值点,每组内的插入排序可能需要较多的比较和交换,导致时间复杂度相对较高。

综合时间复杂度

希尔排序的时间复杂度不是简单的O(nlogn),因为间隔的变化会影响排序的效率。

在某些情况下,希尔排序的时间复杂度可以接近O(nlogn),特别是在数组已经部分有序或间隔选择得当的情况下。

然而,在最坏情况下,希尔排序的时间复杂度可能更高,一些研究表明其时间复杂度在O(n^1.25)到O(n^1.6)之间,通常可以简化为O(n^1.3)。

相关推荐
橘子真甜~8 分钟前
Linux操作系统3-文件与IO操作1(从C语言IO操作到系统调用)
linux·运维·服务器·c语言·io·文件操作·文件fd
想成为高手4991 小时前
深入理解AVL树:结构、旋转及C++实现
开发语言·数据结构·c++
£suPerpanda1 小时前
P3916 图的遍历(Tarjan缩点和反向建边)
数据结构·c++·算法·深度优先·图论
努力进修2 小时前
【Java-数据结构篇】Java 中栈和队列:构建程序逻辑的关键数据结构基石
android·java·数据结构
Lbs_gemini06037 小时前
C++研发笔记14——C语言程序设计初阶学习笔记12
c语言·开发语言·c++·笔记·学习
叁散8 小时前
PTA--数据结构预习报告: 考试排名汇总
数据结构
是老余10 小时前
算法基础之链表:移除链表元素leetcode203
数据结构·算法·链表
不修×蝙蝠11 小时前
数据结构--二叉树的创建和遍历
java·数据结构·二叉树·深度遍历·广度遍历·迭代法·递归法
kitesxian11 小时前
Leetcode62. 不同路径(HOT100)
数据结构·算法·leetcode
立志成为master12 小时前
HBU算法设计第五章(回溯)
数据结构·c++·算法·dfs