探秘前端JavaScript算法(一)

222 阅读8分钟

大家好,我是拾七,持续给大家补给前端干货🔥

在前端开发的世界里,数据结构和算法是我们的基石。数组、链表、栈、队列、树、图……这些基本的数据结构为我们提供了存储和组织数据的方式,而各种算法则是我们解决各种问题的思路和方法。

想象一下,当你面对一个巨大的数据集时,如何快速找到想要的数据?当你需要对列表或表格数据进行排序时,如何选择最合适的排序算法?当你想要实现流畅自然的动画效果时,如何选择最优的动画算法?前端算法就是帮助你解决这些问题的利器。

如何分析一个排序算法?

复杂度分析是整个算法学习的精髓。时间复杂度,表示一个算法执行所耗费的时间;空间复杂度,表示运行完一个程序所需耗费内存的大小。学习排序算法,除了学习它的算法原理、代码实现之外,更重要的是要学会如何评价、分析一个排序算法,从执行效率、内存消耗、稳定性三方面入手。

执行效率

首先,我们在分析排序算法的时间复杂度时,要分别给出最好情况、最坏情况、平均情况下的时间复杂度。除此之外,还要说出最好、最坏时间复杂度对应的要排序的原始数据是什么样的。

其二,我们知道,时间复杂度反应的是数据规模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-1length-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.43798828125ms​const 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;};​

还有更多算法,点关注收藏,持续更新中