【数据结构初阶】排序--直接插入排序和希尔排序

⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:数据结构初阶
⭐代码仓库:Data Structure
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!


前言

排序在我们日常生活中十分地常见,我们从小学习的排名,我们中国大学的软科排名等等等等,这些在我们日常生活中十分常见,所以我们常见的有七种排序,分为四大类,分别为:直接插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序和归并排序,我们在之前已经介绍过了堆排序,今天我们介绍一下直接插入排序和希尔排序:
在这里插入图片描述


一、排序的概念及其运用

1、排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的,也就是说多个相同的数据在经过排序后相对位置没有发生很大的变化。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

2、排序的运用

这个是在京东界面上根据红色框框里面的进行排序。
在这里插入图片描述


二、插入排序

插入排序可以分为两种常见的:直接插入排序和希尔排序:

1、直接插入排序

(1)思路及演示

我们小时候玩过扑克牌,当我们手里一副好牌(9 10 J Q A),缺张老K,此时我摸到一张老K,是在是太开心了,我会很开心地将老K插到Q和A中间,这就是插入排序,
在这里插入图片描述
在这里插入图片描述
如上图是一轮的插入数,其思想是将end放在最后一个数的前面一个位置,tmp提前存的是最末尾元素,将end往前走,拿升序举例,碰到tmp比end指向的那个数小的时候,将end后面的数逐个往后移,end再继续往前走,直到end比0小的时候,最后将end+1这个位置用原本tmp提前存的数进行替代,这就是为什么我们用tmp存的是数组的值而不是tmp存的是下标了,如下图演示:

在这里插入图片描述

一趟写好了,那我们就循环n趟不就是完整的直接插入排序了吗?我们的end变量跟着i进行走,end为数组的最后一个元素的前一个元素。

(2)代码

//插入排序
//升序
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		//先写一趟
		int end = i - 1;
		int tmp = a[i];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				//数据往后移动
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}
		}
		a[end + 1] = tmp;
	}
}

(3)特性总结

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1),它是一种稳定的排序算法
  4. 稳定性:稳定

(4)时间复杂度计算

1、当初始数组为正序时,只需要外循环n-1次,每次进行一次比较,无需移动元素。此时比较次数和移动次数达到最小值为:
最小比较次数=n-1;
最小移动次数=0;
此时时间复杂度为O(n).
2、初始数组为逆序时,需要外部循环n-1次,每次排序中待插入的元素都要和[0,i-1]中的i个元素进行比较且要将这i个元素后移i次,此时比较次数和移动次数达到最大值。
最大比较次数=1+2+3+4+……+n-1=(n-1)n/2;
最大移动次数=1+2+3+……+n-1=(n-1)n/2;
此时时间复杂度为O(n^2).
在这里插入图片描述


2、希尔排序

(1)思路及演示

(i)预排序

总体思路是缩小增量法,也就是先来一个预排序,将这个数组接近有序,即选定一个整数x,把待排序文件中所有数记录并分组,所有距离相同的分在同一组内,并对每一组内的记录进行排序。然后,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序(也就是直接插入排序了)。
我们单纯拿一趟的排序来举例,我们假设gap间隔为3,end从下标为0开始,与间隔gap的gap下标进行比较,升序的话将小的值放在前面,我们看前两轮值都是不变的,那我们看一下第三轮的思想,我们发现gap此时在数组的尾,值为0,我们与end比较,发现0小,那就将end的值覆盖掉gap的值,将end继续往前走一个gap,继续往后覆盖,等到end到数组的头再往前一个gap的时候,发现,这end+gap的值是3,找不到原本的gap为0的值了,此时我们想起来我们之前直接插入排序所讲的gap提前存的是原本数组的gap下标的值,而不是下标,那就很完美了,知道值直接带入即可。
在这里插入图片描述

有了这个思想,我们可以写一个红色gap间隔走完的代码如下:
在这里插入图片描述

测试结果:
在这里插入图片描述
那其实我们想要进行红、蓝、绿三条线依次进行插入排序的话其实也很简单,我们只需要用j去控制一下次数即可,这个控制次数其实很简单,我们发现,当我们的gap为某一值的时候,我们是跳过了中间的某些值,那些值都是需要我们继续进行排序的,也就是说,我们拿gap等于三举例,我们一跳跳三个值,而我们的下一步是从数组的头往后一个位置开始继续跳gap排序的,那也就是说只需要3次即可,那也就是说只需要跳过的数多少即可,也就是gap次:
在这里插入图片描述

测试结果:
在这里插入图片描述

(ii)排序

以上就是我们预排序的结果,预排序是将这一整个数组排序进行接近一个升序或降序的过程,然后再进行排序,这里就很有讲究了,已经很接近升序或者降序的数组我们就可以联想到直接插入排序,这样算法的复杂度会大大降低,所以我们将gap逐渐缩短,缩短到1为止,这里就需要缩短gap的值,至于怎么缩减我们后面进行讨论:
在这里插入图片描述

(2)代码

单组排序:

//希尔排序
void ShellSort1(int* a, int n)
{
	//升序
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int j = 0; j < gap; j++)
		{
			for (int i = j; i < n; i += gap)
			{
				int end = i - gap;
				int tmp = a[end + gap];//存的是值

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

多组排序:

//希尔排序
void ShellSort(int* a, int n)
{
	//int gap = 3;
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		//gap /= 2;
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[i + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			a[end + gap] = tmp;
		}
	}
}

以上两种希尔排序的写法效率是完全一样的,我们重点说一下下面这个代码,如下图演示,是多组排序,往后一位就排一次,代码可以少一层循环,看起来更加地清爽。
在这里插入图片描述

(3)gap取值

这里我们讨论gap的取值,我们先单纯讨论一下单趟gap的取值,当我们取gap很大的时候,一次跳的多,改变速度快;当我们取gap很小的时候,一次跳的少,改变速度慢。这里希尔排序中的gap取值有两个用的比较多的是gap/=2和gap=gap/3+1。这两个缩小gap的公式是比较常用的,当然还可以有很多其他的公式,但最终都是gap==1的时候进行直接插入排序。

(4)特性总结

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定。
  4. 稳定性:不稳定。

(5)时间复杂度计算

我们以一个逆序数组来举例,当刚开始gap比较大的时候,基本上end都需要往后覆盖,其时间复杂度会比较准确,而逐渐往后gap变小了以后,看似end覆盖的更多了,实际上,由于前面已经预排序了,数组接近有序了,这样进行覆盖算的时间复杂度不准确,我们看下图:
在这里插入图片描述

那其实我们阅读书籍能够查阅到关于希尔排序的时间复杂度是很难计算的,因为不知道数组内部具体会成什么样子,gap逐渐往后进行插入排序不知道数据的分布情况,是交换、覆盖值多少次,所以我们阅读书籍,发现老师们做了大量的实验发现希尔排序的时间复杂度约等于n^1.3, 比n*logn大,但接近。

《数据结构(C语言版)》— 严蔚敏
在这里插入图片描述
《数据结构-用面相对象方法与C++描述》— 殷人昆
在这里插入图片描述


总结

排序是找工作或者考研最基础也是最重要的知识点,我们需要知道排序那同样也要知道排序的内涵,不单单知道排序是怎么样进行写代码的,同时也需要知道排序的时空复杂度以及其底层的内涵。

家人们不要忘记点赞收藏+关注哦!!!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

2022horse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值