审核大哥,这篇文章是介绍前端实现排序算法,是很常见的IT知识,题目是对前端行业的调侃。善意的,友好的,符合 新时代思想文化建设的。
本篇文章参考借鉴了以下文章和极客时间数据结构与算法一文,若涉及侵权,请联系我删除。
前言
上一篇文章是关于冒泡排序的学习总结,还未学习的同学可以先看一下以下文章
本节我们一期学习一个新的排序算法,插入排序
带着问题学习
首先需要思考,一个有序的数组,在往里面插入一个新的数据之后,如何保持数据有序呢?很简单,我们只需要遍历数据,找到数据应该插入的地方,将其插入即可。
这是一个动态排序的过程,即动态的往有序集合里添加数据,我们可以通过这种方式,保持集合中的数据一直有序。而对于一组静态数据,我们也可以借鉴上面的插入方法,来进行排序,于是就有了插入排序。
插入排序是如何借助动态思想,实现排序的?
受限,我们将数组中的数据分为两个区域,已排序区域和未排序区域。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想就是 取未排序区间的元素,在已排序区间中找到合适的位置将其插入,并保证已排序区间的数据,一直有序 。重复这个过程,直到未排序区间为空,算法结束。
如下图所示,要排序的数据是4、5、6、1、3、2,其中左侧为已排序区间,右侧是未排序区间。
插叙排序也包含两种操作,一种是 元素的比较 ,一种是 元素的移动 。当我们需要讲一个数据 a 插入到已排序区间时,需要拿 a 与已排序区间的元素依次比较大小,找到合适的插入位置。
找到插入点后,我们还需要把插入点之后的元素顺序往后移动一位,这样才能腾出位置给 a 元素插入。
对于不同的查找插入点的方法(从头到尾,从尾到头),元素的比较次数是有区别的,但对于一个给定的初始序列,移动操作次数总是固定的。就等于 逆序度。
为什么说义移动次数就等于逆序度呢?把上面的例子做一个分析就一目了然了
满有序度是 n*(n - 1)/2 = 15,初始序列的有序度是 5 ,所以逆序度是 10 ,插入排序中,数据移动总和等于 10 = 3+3+4。(可以参考上一篇文章 )-- 前端不懂算法(一)–冒泡排序
代码实现
function insertSort(arr) {
let n = arr.length;
if (n<=1) return;
for (let i = 1; i < n; ++i) {
let value = arr[i];
let j = i - 1;
//查找插入位置
for (; j >= 0; --j) {
if (arr[j] > value) {
//数据移动
arr[j + 1] = arr[j]
} else {
break;
}
}
//插入数据
arr[j+1] = value;
}
}
Q&A,排序算法的三个问题
第一,插入排序是原地排序算法吗?
从实现过程可以看出,插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是 O(1),也就是说,这是一个 原地排序算法。
第二,插入排序是稳定的排序算法吗?
在插入排序中,对于值相等的元素,我们可以选择将后面出现的,插入到前面出现元素的后面,这样就可以保证原有的前后顺序不变,所以插入排序是稳定的排序算法。
第三,插入排序的时间复杂度是多少?
如果要排序的数据已经是有序的,我们并不需要搬移任何数据。如果我们从尾到头在有序数据组里面查找插入位置。每次只需要比较一个数据就能确定插入位置。所以这种情况下,最好的时间复杂度为O(n)。
如果数组是倒序的,每次插入都相当于在数组的第一个位置插入数据,需要移动大量的数据,所以最坏情况的时间复杂度为O(n2)。
还记得最开始我们讨论的,在数组中插入一个数据的平均时间复杂度是多少吗?没错就是O(n),所以对于插入排序来说,每次插入都相当于在数组中插入一个数据,循环执行 n 次插入操作,所以平均时间复杂度为O(n2)。
选择排序
选择排序的实现方式有点类似于插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
选择排序的三个问题
照例,思考三个问题,前面的解释已经很详细了,这里就直接公布答案。
选择排序的控件复杂度为O(1),是一种原地排序算法。选择排序的最好情况、最坏情况和平时情况时间复杂度都为O(n2)。有兴趣的同学可以自己分析一下。
选择排序是稳定的排序算法吗?这个问题需要重点讨论。
答案是否定的,选择排序是一种不稳定的排序算法。从前面那张图可以看出,选择排序每次都要找出剩余未排序元素中的最小值,然后和前面的元素交换位置,这样就破坏了稳定性。
比如5、8、5、2、9 这组数据,使用选择排序的话,第一次选择 2 ,与第一个 5 交换位置,那么这两个 5 的前后顺序就变了,所以不稳定。正因如此,相对于冒泡排序和插入排序,选择排序就稍稍逊色一些。
排序系列开篇问题
我在本系列开篇的地方了一个问题,为什么插入排序比冒泡排序更受欢迎?如果不记得的同学可以查看上一篇博文
我们已经学习了三种排序,其中冒泡排序和插入排序的时间复杂度都是 O(n2)。都是原地排序算法,为什么插入排序比冒泡排序更受欢迎?
因为之前我们说过,冒泡排序不管怎么优化,其元素交换的次数是一个固定值,是原始数据的逆序度。插入排序也是一样的,不管怎么优化,元素移动的次数也是逆序度。
但是,从代码实现上来看,冒泡排序的数据交换,要比插入排序的数据移动复杂的多,冒泡需要三个赋值,而插入只需要一个,下面是代码部分:
if (arr[j] > arr[j + 1]) {
let temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
//表示有数据交换
flag = true;
}
if (arr[j] > value) {
//数据移动
arr[j + 1] = arr[j]
} else {
break;
}
我们把执行一个赋值语句的时间记做单位时间 unit
,然后分别用冒泡排序和插入排序对同一个逆序度是 K 的数组进行排序。用冒泡排序,需要 K 次交换,每次需要三个赋值语句,所以交换操作总耗时就是 3*K unit
而插入排序排序中移动操作只需要 K 个单位时间。
这个只是理论的推测,大家可以分别用两个方法对一个数组进行排序,计算一下算法执行时间,我以前做过一个随机生成1000个数组,每个数组包含200个数据的实验,冒泡排序大概需要 700ms , 而插入排序只需要 100ms 就能搞定。
当然插入排序也是可以进一步优化的,具体方法可以学习一下 希尔排序。
内容小结
想要分析,评价一个排序算法,需要从执行效率,内存消耗,稳定性三个方面考虑。因此这一节中,我们分析了三种,时间复杂度为 O(n2) 的排序算法
上面三种算法中,冒泡排序,选择排序,基本都是停留在理论层面,了解一下,拓展一下思路,实际开发中运用的并不多,但是插入排序还是很有用的,后面讲排序优化的时候,会讲到有些编程语言中的排序函数,就是使用的插入排序算法。
本文这几个排序算法,代码简单,比较适合小型排序,用起来也很高效。但是在大规模数据排序的时候,时间复杂度还是有点高,需要倾向于下一节的时间复杂度为 O(nlogn)的排序算法。
文中不足之处,请各位大佬指点。