大家好,我是拾七,持续给大家补给前端干货🔥
在前端开发的世界里,数据结构和算法是我们的基石。数组、链表、栈、队列、树、图……这些基本的数据结构为我们提供了存储和组织数据的方式,而各种算法则是我们解决各种问题的思路和方法。
想象一下,当你面对一个巨大的数据集时,如何快速找到想要的数据?当你需要对列表或表格数据进行排序时,如何选择最合适的排序算法?当你想要实现流畅自然的动画效果时,如何选择最优的动画算法?前端算法就是帮助你解决这些问题的利器。
如何分析一个排序算法?
复杂度分析是整个算法学习的精髓。时间复杂度,表示一个算法执行所耗费的时间;空间复杂度,表示运行完一个程序所需耗费内存的大小。学习排序算法,除了学习它的算法原理、代码实现之外,更重要的是要学会如何评价、分析一个排序算法,从执行效率、内存消耗、稳定性三方面入手。
执行效率
首先,我们在分析排序算法的时间复杂度时,要分别给出最好情况、最坏情况、平均情况下的时间复杂度。除此之外,还要说出最好、最坏时间复杂度对应的要排序的原始数据是什么样的。
其二,我们知道,时间复杂度反应的是数据规模n很大的时候的一个增长趋势,所以它表示的时候会忽略系数、常数、低阶。但实际的软件开发中,排序的可能是10个、100个、1000个这样规模很小的数据,所以,在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶考虑进来。
其三,基于比较的排序算法的执行过程,会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。所以,如果我们在分析算法的执行效率的时候,应该把比较次数和交换次数也考虑进去。
内存消耗
这里看的是空间复杂度,需要了解内排序、外排序、原地排序。
内排序:所有排序操作都在内存完成。
外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行。
原地排序:原地排序算法,就是特指空间复杂度是O(1)的排序算法。
稳定性
稳定:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变,比如:a原本在b前面,而a=b,排序之后,a仍然在b的前面。
不稳定:如果待排序的序列中存在值相等的元素,经过排序后,相等元素之间原有的先后顺序改变。比如:a原本在b前面,而a=b,排序之后,a仍然在b的后面。
排序算法 - 冒泡排序( Bubble Sort )
1、冒泡排序只会操作相邻的两个数据
2、每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。
3、一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作。
// 冒泡排序const bubbleSort = (arr) => { console.time('改进前冒泡排序耗时'); const length = arr.length; if (length <= 1) return; for (let i = 0; i < length - 1; i++) { for (let j = 0; j < length - i - 1; j++) { if (arr[j] > arr[j + 1]) { const temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } console.info('改进前', arr); console.timeEnd('改进前冒泡排序耗时');};
问题:当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。
// 冒泡排序(优化后)const bubbleSort2 = (arr) => { console.time('改进后冒泡排序时'); const length = arr.length; if (length <= 1) return; // i < length - 1是因为外层只需要length - 1次就排好了,第length次比较是多余的。 for (let i = 0; i < length - 1; i++) { let hasChange = false; // 提前退出冒泡循环的标志位 // j<length -i -1是因为内层的length - i-1到length-1的位置已经排好了,不需要再比较一次 for (let j = 0; j < length - i - 1; j++) { if (arr[j] > arr[j + 1]) { const temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; hasChange = true; // 表示有数据交换 } } if (!hasChange) break; } console.info('改进后', arr); console.timeEnd('改进后冒泡排序耗时');};
// 测试const arr = [7, 8, 4, 5, 6, 3, 2, 1];bubbleSort(arr);// 改进前 arr : [1, 2, 3, 4, 5, 6, 7, 8]// 改进前冒泡排序耗时: 0.43798828125msconst arr2 = [7, 8, 4, 5, 6, 3, 2, 1];bubbleSort2(arr2);// 改进后 arr : [1, 2, 3, 4, 5, 6, 7, 8]// 改进后冒泡排序耗时: 0.318115234375ms
分析
1、冒泡排序是原地排序算法吗?
冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为O(1),是一个原地排序的算法。
2、冒泡排序是稳定的排序算法吗?
在冒泡排序中,只有交换才可以改变两个元素的前后顺序。
为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序。所以冒泡排序是稳定的排序算法。
3、冒泡排序的时间复杂度是多少?
最佳情况:T(n) = O(n), 数据已经是正序时
最差情况:T(n) = O(n^2),数据时反序时
平均情况:T(n) = O(n^2)
排序算法 - 插入排序( Insertion Sort )
插入排序又分为直接插入排序、优化后的折半插入排序、希尔排序,我们通常说的插入排序是指直接插入排序。
一般人打扑克牌,整理牌的时候,都是按牌的大小(从小到大或者从大到小)整理牌的,那每摸一张新牌,就扫描自己的牌,把新牌插入到相应的位置。插入排序的工作原理:通过构建有序序列,对于未排序数据,在已排序列中从后向前扫描,找到相应位置并插入。
步骤
1、从第一个元素开始,该元素可以认为已经被排序;
2、取出下一个元素,在已经排序的元素序列中从后向前扫描;
3、如果该元素(已排序)大于新元素,将该元素移到下一位置;
4、重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
5、将新元素插入到该位置后;
6、重复步骤2~5
const insertSort = (arr) => { const len = arr.length; if (len <= 1) return; let preIndex, current; for (let i = 1; i < len; i++) { preIndex = i - 1; // 待比较元素的下标 current = arr[i]; // 当前元素 while (preIndex >= 0 && arr[preIndex] > current) { // 前置条件之一:待比较元素比当前元素大 arr[preIndex + 1] = arr[preIndex]; preIndex--; // 游标前移一位 } if (preIndex + 1 != i) { // 避免同一个元素赋值给自身 arr[preIndex + 1] = current; // 将当前元素插入预留空位 console.log('arr', arr); } } return arr;};
分析
1、插入排序是原地排序算法吗 插入排序算法的运行不需要额外的存储空间,所以空间复杂度是O(1),所以,这是一个原地排序算法。
2、插入排序是稳定的排序算法吗?
在插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法。
3、插入排序的时间复杂度是多少?
最佳情况:T(n) = O(n),数据已经是正序
最差情况:T(n) = O(n^2),数据反序
平均情况:T(n) = O(n^2)
折半插入排序是直接插入排序的升级版
鉴于插入排序第一部分为已排好序的数组,我们不必按顺序依次寻找插入点,只需比较它们的中间值与待插入元素的大小即可。
-
取 0 ~ i-1 的中间点 ( m = (i-1) >> 1 ),array[i] 与 array[m] 进行比较,若 array[i] < array[m],则说明待插入的元素 array[i] 应该处于数组的 0 ~ m 索引之间;反之,则说明它应该处于数组的 m ~ i-1 索引之间。
-
重复步骤 1,每次缩小一半的查找范围,直至找到插入的位置。
-
将数组中插入位置后的元素全部后移一位
-
在指定位置插入第i个元素。
PS:x>>1是位运算中的右移运算,表示右移一位,等同于x除以2再取整,即x>>1 == Math.floor(x/2)
// 折半插入const binaryInsertionSort = (array) => { const len = array.length; if (len <= 1) return; let current, i, j, low, high, m; for (i = 1; i < len; i++) { low = 0; high = i - 1; current = array[i]; while (low <= high) { // 步骤 1 & 2 : 折半查找 x>>1 是位运算中的右移运算, 表示右移一位, 等同于 x 除以 2 再取整, 即 x>>1 == Math.floor(x/2) . // 高位区:m ~ i-1 低位区:0 - m m = (low + high) >> 1; if (array[i] >= array[m]) { low = m + 1; } else { high = m - 1; } } for (j = i; j > low; j--) { array[j] = array[j - 1]; } array[low] = current; } return array;};
还有更多算法,点关注收藏,持续更新中