-
Notifications
You must be signed in to change notification settings - Fork 721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
JavaScript 数据结构与算法之美 - 冒泡排序、插入排序、选择排序 #39
Labels
Data Structure and Algorithms
JavaScript 数据结构与算法之美
Comments
This was referenced Jul 19, 2019
This was referenced Jul 25, 2019
第五点重复了 |
在插半排序中, 少了一个重要的前提条件 `const binaryInsertionSort = (arr: Array): Array => {
} |
折半插入感觉没有必要啊,把移动元素和查找位置分开,虽然提高了查找的效率,反而多了一次循环。 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
1. 前言
想学好前端,先练好内功,只有内功深厚者,前端之路才会走得更远。
笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习。
之所以把
冒泡排序、选择排序、插入排序
放在一起比较,是因为它们的平均时间复杂度都为 O(n2)。请大家带着问题:
为什么插入排序比冒泡排序更受欢迎 ?
来阅读下文。2. 如何分析一个排序算法
复杂度分析是整个算法学习的精髓。
时间和空间复杂度的详解,请看 JavaScript 数据结构与算法之美 - 时间和空间复杂度。
学习排序算法,我们除了学习它的算法原理、代码实现之外,更重要的是要学会如何评价、分析一个排序算法。
分析一个排序算法,要从
执行效率
、内存消耗
、稳定性
三方面入手。2.1 执行效率
1. 最好情况、最坏情况、平均情况时间复杂度
我们在分析排序算法的时间复杂度时,要分别给出最好情况、最坏情况、平均情况下的时间复杂度。
除此之外,你还要说出最好、最坏时间复杂度对应的要排序的原始数据是什么样的。
2. 时间复杂度的系数、常数 、低阶
我们知道,时间复杂度反应的是数据规模 n 很大的时候的一个增长趋势,所以它表示的时候会忽略系数、常数、低阶。
但是实际的软件开发中,我们排序的可能是 10 个、100 个、1000 个这样规模很小的数据,所以,在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。
3. 比较次数和交换(或移动)次数
这一节和下一节讲的都是基于比较的排序算法。基于比较的排序算法的执行过程,会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。
所以,如果我们在分析排序算法的执行效率的时候,应该把比较次数和交换(或移动)次数也考虑进去。
2.2 内存消耗
也就是看空间复杂度。
还需要知道如下术语:
其中,冒泡排序就是原地排序算法。
2.3 稳定性
相等
的元素,经过排序之后,相等元素之间原有的先后顺序不变
。比如: a 原本在 b 前面,而 a = b,排序之后,a 仍然在 b 的前面;
相等
的元素,经过排序之后,相等元素之间原有的先后顺序改变
。比如:a 原本在 b 的前面,而 a = b,排序之后, a 在 b 的后面;
3. 冒泡排序
思想
特点
实现
优化:当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。
测试
分析
冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1),是一个
原地
排序算法。在冒泡排序中,只有交换才可以改变两个元素的前后顺序。
为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序。
所以冒泡排序是
稳定
的排序算法。最佳情况:T(n) = O(n),当数据已经是正序时。
最差情况:T(n) = O(n2),当数据是反序时。
平均情况:T(n) = O(n2)。
动画
4. 插入排序
插入排序又为分为 直接插入排序 和优化后的 拆半插入排序 与 希尔排序,我们通常说的插入排序是指直接插入排序。
一、直接插入
思想
一般人打扑克牌,整理牌的时候,都是按牌的大小(从小到大或者从大到小)整理牌的,那每摸一张新牌,就扫描自己的牌,把新牌插入到相应的位置。
插入排序的工作原理:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
步骤
实现
测试
分析
插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是 O(1),所以,这是一个
原地
排序算法。在插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以插入排序是
稳定
的排序算法。最佳情况:T(n) = O(n),当数据已经是正序时。
最差情况:T(n) = O(n2),当数据是反序时。
平均情况:T(n) = O(n2)。
动画
二、拆半插入
插入排序也有一种优化算法,叫做
拆半插入
。思想
折半插入排序是直接插入排序的升级版,鉴于插入排序第一部分为已排好序的数组, 我们不必按顺序依次寻找插入点, 只需比较它们的中间值与待插入元素的大小即可。
步骤
测试
注意
:和直接插入排序类似,折半插入排序每次交换的是相邻的且值为不同的元素,它并不会改变值相同的元素之间的顺序,因此它是稳定的。三、希尔排序
希尔排序是一个平均时间复杂度为 O(nlogn) 的算法,会在下一个章节和 归并排序、快速排序、堆排序 一起讲,本文就不展开了。
5. 选择排序
思路
选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
步骤
实现
测试
分析
选择排序空间复杂度为 O(1),是一种
原地
排序算法。选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,这样破坏了稳定性。所以,选择排序是一种
不稳定
的排序算法。无论是正序还是逆序,选择排序都会遍历 n2 / 2 次来排序,所以,最佳、最差和平均的复杂度是一样的。
最佳情况:T(n) = O(n2)。
最差情况:T(n) = O(n2)。
平均情况:T(n) = O(n2)。
动画
5. 选择排序
思路
选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
步骤
实现
测试
分析
选择排序空间复杂度为 O(1),是一种
原地
排序算法。选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,这样破坏了稳定性。所以,选择排序是一种
不稳定
的排序算法。无论是正序还是逆序,选择排序都会遍历 n2 / 2 次来排序,所以,最佳、最差和平均的复杂度是一样的。
最佳情况:T(n) = O(n2)。
最差情况:T(n) = O(n2)。
平均情况:T(n) = O(n2)。
动画
6. 解答开篇
为什么插入排序比冒泡排序更受欢迎 ?
冒泡排序和插入排序的时间复杂度都是 O(n2),都是原地排序算法,为什么插入排序要比冒泡排序更受欢迎呢 ?
这里关乎到 逆序度、满有序度、有序度。
有序元素对用数学表达式表示就是这样:
满有序度:把完全有序的数组的有序度叫作 满有序度。
逆序度:正好跟有序度相反(默认从小到大为有序)。
同理,对于一个倒序排列的数组,比如 6,5,4,3,2,1,有序度是 0;
对于一个完全有序的数组,比如 1,2,3,4,5,6,有序度就是 n(n-1)/2* ,也就是满有序度为 15。
原因
7. 排序算法的复杂性对比
复杂性对比
算法可视化工具
这里推荐一个算法可视化工具。
算法可视化工具 algorithm-visualizer 是一个交互式的在线平台,可以从代码中可视化算法,还可以看到代码执行的过程。
效果如下图。
8. 最后
喜欢就点个赞吧。
文中所有的代码及测试事例都已经放到我的 GitHub 上了。
参考文章:
The text was updated successfully, but these errors were encountered: