vlambda博客
学习文章列表

数据结构->八大排序之插入排序

本章将介绍常见八大排序中的 插入排序;


可能你会好奇,为什么只介绍插入排序呢?


因为我学完了所有排序以后,也看了市面上很多写排序的文章,都是一篇文章全部把所有排序都写完了的!


我觉得一篇文章把所有排序全部写完的话,会存在2个问题:


1、一篇文章写完所有排序,会导致某个知识点不全面,换个话来说,就是越往后写,容易产生疲劳;

 

2、读者的直观感受可能不是那么好,毕竟更多的是要让大家能看懂!其次自己也能看懂!


1. 排序是什么

🍑 排序的概念

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

稳定性: 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

 

内部排序: 数据元素全部放在内存中的排序。

 

外部排序: 数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。


🍑 排序的运用

这里给大家举几个生活中,常见排序的栗子👇:

购物平台里面按某个商品的维度排序

数据结构->八大排序之插入排序

全国高校排名

数据结构->八大排序之插入排序

其实在我们平时日常生活中还有很多都会用到排序的地方;

由此可见,排序与我们的生活息息相关😄

2. 插入排序分类

插入排序可以分为:直接插入排序 和 希尔排序

数据结构->八大排序之插入排序

3. 直接插入排序

🍑 基本思想

🎃直接插入排序是一种简单的插入排序法;

插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。

 

实际中我们玩扑克牌时,就用了插入排序的思想,不信你看👇

数据结构->八大排序之插入排序

❗❓那么怎么去理解它呢?很简单

直接插入排序是指:在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的

按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序 。

 

但我们并不能确定待排元素中究竟哪一部分是有序的,所以我们一开始只能认为第一个元素是有序的依次将其后面的元素插入到这个有序序列中来直到整个序列有序为止

数据结构->八大排序之插入排序


🍑 动图演示

🍅我们先来看个动态图演示:


1、从图中观察的现象是如果后一个数不比前一个数小,那就不需要插入,不插入的动作就是break出循环

 

2、如果前面的数都比pos(拿出的数)值大,那么就将前n个数都往后挪动,直到比pos值小或者相等就停止,可以用循环控制,这里防止越界需要再加判断

📃 代码实现

void InsertSort(int* a, int n)

{

int i = 0;


//注意控制好终止条件,这里的end的位置是在倒数第二个位置,所以要-1

for (i = 0; i < n - 1; i++)

{

int end = i;//记录有序序列的最后一个元素的下标


int tmp = a[end + 1];//待插入的元素

while (end >= 0)

{

if (tmp < a[end])//还需继续比较

{

a[end + 1] = a[end];

end--;

}

else//找到应插入的位置

{

break;

}

}

a[end + 1] = tmp;

//代码执行到此位置有两种情况:

//1.待插入元素找到应插入位置(break跳出循环到此)。

//2.待插入元素比当前有序序列中的所有元素都小(while循环结束后到此)。

}

}

🍑 概括总结

复杂度分析:

元素集合越接近有序,直接插入排序算法的时间效率越高,反之越低

时间复杂度:O ( n 2 ) O(n^2)O(n2 )


空间复杂度:O ( 1 ) O(1)O(1)


稳定性的分析:直接插入排序在遇到相同的数时,可以就放在这个数的后面,就可以保持稳定性了,所以说这个排序是稳定的。


特性总结:


插入排序是一种最简单直观的排序算法;


 


它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。


 


也就是说:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。


4. 希尔排序

🍑 基本思想

希尔排序,也称缩小增量法,是插入排序的一种更高效的改进版本;


 


但希尔排序是非稳定排序算法。


❗❓那么它的思路是什么呢?


每次从一个有序序列开始,将待排元素与有序序列中的元素从后往前逐个比较,


 


若有序序列中的元素大于待排元素,则将较大的元素往后覆盖;


 


否则,将待排元素插入其前面,并结束此轮比较。


🍑 动图演示

🤔实现原理


1、先选定一个小于N的整数gap作为第一增量,然后将所有距离为gap的元素分在同一组,并对每一组的元素进行直接插入排序。然后再取一个比第一增量小的整数作为第二增量,重复上述操作…


 


2、当增量的大小减到1时,就相当于整个序列被分到一组,进行一次直接插入排序,排序完成。


 


为什么要让gap由大到小呢?


 


答:gap越大,数据挪动得越快;gap越小,数据挪动得越慢。前期让gap较大,可以让数据更快得移动到自己对应的位置附近,减少挪动次数。


 


注:一般情况下,取序列的一半作为增量,然后依次减半,直到增量为1(也可自己设置)。


🍑 举例说明

现在我们用希尔排序对该序列进行排序。




我们用序列长度的一半作为第一次排序时gap的值,此时相隔距离为5的元素被分为一组(共分了5组,每组有2个元素),然后分别对每一组进行直接插入排序。

数据结构->八大排序之插入排序

gap的值折半,此时相隔距离为2的元素被分为一组(共分了2组,每组有5个元素),然后再分别对每一组进行直接插入排序。

数据结构->八大排序之插入排序

gap的值再次减半,此时gap减为1,即整个序列被分为一组,进行一次直接插入排序。

数据结构->八大排序之插入排序

上述示例中,前两趟就是希尔排序的预排序,最后一趟就是希尔排序的直接插入排序。

📃 代码实现

//希尔排序

void ShellSort(int* a, int n)

{

int gap = n;

while (gap > 1) //别加等号,不然就是死循环

{

        //控制gap值的变化,让数组接近有序,gap == 1就可以直接插入排序

gap = gap / 2;//gap折半

int i = 0;

//进行一趟排序

for (i = 0; i < n - gap; i++)

{

int end = i;

int tmp = a[end + gap];

while (end >= 0)

{

                //当前的end的值比tmp大就要往end+gap位置挪

//所以要提前保存end+gap的值

if (tmp < a[end])

{

a[end + gap] = a[end];

end -= gap;

}

else

{

break;

}

}

a[end + gap] = tmp;

}

}

}

🍑 概括总结

复杂度分析:

时间复杂度:O ( N l o g N ) O(NlogN)O(NlogN) 

空间复杂度:O ( 1 ) O(1)O(1) 

平均时间复杂度:O ( N 1.3 ) O(N^{1.3})O(N1.3 )

 

稳定性的分析:可以这样想,相同的数被分到了不同的组,就不能保证原有的顺序了,所以说这个排序是不稳定的。


特性总结:

希尔排序是对直接插入排序的优化。

当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就 会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。


希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的 希尔排序的时间复杂度都不固定:


在 《数据结构(C语言版)》 中,是这样说的:



在 《数据结构-用面相对象方法与C++描述》 中,是这样说的:


因为我们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:O ( n 1.25 ) O(n^{1.25})O(n1.25) 到 O ( 1.6 ∗ n 1.25 ) O(1.6*n^{1.25})O(1.6∗n 1.25 ) 来计算

稳定性:不稳定


5. 总结

以上就是关于 插入排序 的全部内容