资讯专栏INFORMATION COLUMN

【30分钟学会】用js玩点算法(1):排序基础

Richard_Gao / 572人阅读

摘要:如果今天这个比例降低了,可能的原因之一是如今的排序算法更加高效,而并非排序的重要性降低了。约定都是从小到大排序,当前项为。冒泡排序比较任何两个相邻的项,如果第一个比第二个大,则交换它们。

前言

前端工程师由于业务特点比较少接触算法的东西,所以本系列也不会讲太过深入的东西,更多的是作为知识扩展和思维逻辑的培养。
排序就是将一组对象按照某种逻辑顺序重新排列的过程,本篇将介绍几种金典的排序算法。

在计算时代早期,大家普遍认为30%的计算周期都用在了排序上。如果今天这个比例降低了,可能的原因之一是如今的排序算法更加高效,而并非排序的重要性降低了。

约定都是从小到大排序,当前项为i。swap是交换数组内位置的函数,实现如下:

function swap(_arr, index1, index2) {
  const arr = _arr;
  arr[index1] += arr[index2];
  arr[index2] = arr[index1] - arr[index2];
  arr[index1] -= arr[index2];
}
冒泡排序

学校里第一个学的排序方式总是冒泡排序,虽然它效率低,但最容易理解。冒泡排序比较任何两个相邻的项,如果第一个比第二个大,则交换它们。元素项向上移动至正确的顺序,就好像气泡升至表面一样,冒泡排序因此得名。

一般方案

基本思路:

前一项(i)与后一项(i+1)项比较,如果前一项比后一项大就交换这两项;

重复这个过程到最后;

一趟完成后再从头开始重复上面的步骤,有多少项就要重复几次。

代码实现:

function bubbleSort(_arr) {
  const arr = [].slice.call(_arr);
  const len = arr.length;
  for (let i = 0; i < len; i += 1) {
    for (let f = 0; f < len - 1; f += 1) {
      if (arr[f] > arr[f + 1]) {
        swap(arr, f, f + 1);
      }
    }
  }
  return arr;
}

示例过程:

// 初始
5 4 9 5 3

// 第一趟
4 5 9 5 3  // 5>4,交换
^ ^
4 5 9 5 3  // 5<9,不变
  ^ ^
4 5 5 9 3  // 9>5,交换
    ^ ^
4 5 5 3 9  // 9>3,交换
      ^ ^

// 第二趟
4 5 5 3 9  // 4<5,不变
^ ^
4 5 5 3 9  // 5=5,不变
  ^ ^
4 5 3 5 9  // 5>3,交换
    ^ ^
4 5 3 5 9  // 5<9,不变
      ^ ^

// 第三趟
4 5 3 5 9  // 4<5,不变
^ ^
4 3 5 5 9  // 5>3,交换
  ^ ^
4 3 5 5 9  // 5=5,不变
    ^ ^
4 3 5 5 9  // 5<9,不变
      ^ ^

// 第四趟
3 4 5 5 9  // 4>3,交换
^ ^
3 4 5 5 9  // 4<5,不变
  ^ ^
3 4 5 5 9  // 5=5,不变
    ^ ^
3 4 5 5 9  // 5<9,不变
      ^ ^

// 第五趟
3 4 5 5 9  // 3<4,不变
^ ^
3 4 5 5 9  // 4<5,不变
  ^ ^
3 4 5 5 9  // 5=5,不变
    ^ ^
3 4 5 5 9  // 5<9,不变
      ^ ^

// 结果
3 4 5 5 9
改进方案

通过上面的排序过程,可以发现其实每一趟就可以确定最后一位的位置了,所以可以不用再比较最后的位置。代码改造也很小,只要在内循环减去已经确定的位置数即可。

function modifiedBubbleSort(_arr) {
  const arr = [].slice.call(_arr);
  const len = arr.length;
  for (let i = 0; i < len; i += 1) {
    for (let f = 0; f < len - i - 1; f += 1) {
      if (arr[f] > arr[f + 1]) {
        swap(arr, f, f + 1);
      }
    }
  }
  return arr;
}

示例过程:

// 初始
5 4 9 5 3

// 第一趟
4 5 9 5 3  // 5>4,交换
^ ^
4 5 9 5 3  // 5<9,不变
  ^ ^
4 5 5 9 3  // 9>5,交换
    ^ ^
4 5 5 3 9  // 9>3,交换
      ^ ^

// 第二趟
4 5 5 3 9  // 4<5,不变
^ ^
4 5 5 3 9  // 5=5,不变
  ^ ^
4 5 3 5 9  // 5>3,交换
    ^ ^

// 第三趟
4 5 3 5 9  // 4<5,不变
^ ^
4 3 5 5 9  // 5>3,交换
  ^ ^

// 第四趟
3 4 5 5 9  // 4>3,交换
^ ^

// 结果
3 4 5 5 9
选择排序

选择排序算法是一种原址比较排序算法。这也是比较简单的过程,只要不断遍历找到最小的数依次放入位置即可。
基本思路:

设定一个指针指向最小的数,从0号位开始;

遍历数据,如果遇到比当前指针指向的数还小的数,就将指针重新指向这个新位置;

遍历完成即得到了最小的数的位置,把0号位与这个位置的数交换;

接下来就是1号位,重复以上步骤直到全部位置都正确。

代码实现:

function selectionSort(_arr) {
  const arr = [].slice.call(_arr);
  const len = arr.length;
  for (let i = 0; i < len - 1; i += 1) {
    let indexMin = i;
    for (let f = i + 1; f < len; f += 1) {
      if (arr[indexMin] > arr[f]) {
        indexMin = f;
      }
    }
    if (indexMin !== i) {
      swap(arr, indexMin, i);
    }
  }
  return arr;
}

示例过程:

// 初始
5 4 9 5 3

// 第一趟,指针指向0号位
5 4 9 5 3  // 4<5,指针指向1号位
  ^
5 4 9 5 3  // 9>4,指针不变
  ^
5 4 9 5 3  // 5>4,指针不变
  ^
5 4 9 5 3  // 3<4,指针指向4号位
        ^
3 4 9 5 5   // 遍历结束,交换0号位和4号位

// 第二趟,指针指向1号位
3 4 9 5 5  // 9>4,指针不变
  ^
3 4 9 5 5  // 5>4,指针不变
  ^
3 4 9 5 5  // 5>4,指针不变
  ^
3 4 9 5 5  // 遍历结束,1号位不变

// 第三趟,指针指向2号位
3 4 9 5 5  // 5<9,指针指向3号位
      ^
3 4 9 5 5  // 5=5,指针不变
      ^
3 4 5 9 5  // 遍历结束,交换2号位和3号位

// 第四趟,指针指向3号位
3 4 5 9 5  // 5<9,指针指向4号位
        ^
3 4 5 5 9  // 遍历结束,交换3号位和4号位

// 结果
3 4 5 5 9
插入排序

插入排序就是要把后面的数往前面插入。假定第一项已经排序了,接着从第二项开始,依次判断当前项应该插入到前面的哪个位置。
基本思路:

从第二项开始(i=1),当前项(i),缓存其值和位置;

向前遍历,指针f初始化为i位置,如果f-1大于当前项的值,则交换f和f-1(即f-1向后移动一位),并f--;

如果遇到f-1小于当前值,或f=0时停止循环,这时候f即是当前项的位置,将之前的缓存值写入该位置。

代码实现:

function insertionSort(_arr) {
  const arr = [].slice.call(_arr);
  const len = arr.length;
  for (let i = 1; i < len; i += 1) {
    let f = i;
    const temp = arr[i];
    while (f > 0 && arr[f - 1] > temp) {
      arr[f] = arr[f - 1];
      f -= 1;
    }
    arr[f] = temp;
  }
  return arr;
}

示例过程:

// 初始
5 4 9 5 3

// 第一趟,当前项是1号位,数字4
_ 5 9 5 3  // 4<5,5向后移动
^ ^
4 5 9 5 3  // 遍历结束,写入4
^

// 第二趟,当前项是2号位,数字9
4 5 9 5 3  // 9>5,不变
  ^
4 5 9 5 3  // 9>4,不变,遍历结束
^

// 第三趟,当前项是3号位,数字5
4 5 _ 9 3  // 5<9,9向后移动
    ^ ^
4 5 _ 9 3  // 5=5,不变
  ^
4 5 _ 9 3  // 5>4,不变
^
4 5 5 9 3  // 遍历结束,写入5
    ^

// 第四趟,当前项是4号位,数字3
4 5 5 _ 9  // 3<9,9向后移动
      ^ ^
4 5 _ 5 9  // 3<5,5向后移动
    ^ ^
4 _ 5 5 9  // 3<5,5向后移动
  ^ ^
_ 4 5 5 9  // 3<4,4向后移动
^ ^
3 4 5 5 9  // 遍历结束,写入3
^

// 结果
3 4 5 5 9
归并排序

归并排序是一种分治算法。其思想是将原始数组切分成较小的数组,直到每个小数组只有一个位置,接着将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。
基本思路:

将数组从中间切成两个数组;

如果切出来的数组长度不为1,则重复上一步,直到所有切分出来的数组的长度都为1;

以从小到大的顺序合并小数组,先是两个长度为1的数组合并成长度为2的数组;

再是两个长度为2的数组合并为长度为4的数组,以此类推。

代码实现:

function mergeSort(_arr) {
  const arr = [].slice.call(_arr);
  function merge(left, right) {
    const result = [];
    let iL = 0;
    let iR = 0;
    const lenL = left.length;
    const lenR = right.length;
    while (iL < lenL && iR < lenR) {
      if (left[iL] < right[iR]) {
        result.push(left[iL]);
        iL += 1;
      } else {
        result.push(right[iR]);
        iR += 1;
      }
    }
    while (iL < lenL) {
      result.push(left[iL]);
      iL += 1;
    }
    while (iR < lenR) {
      result.push(right[iR]);
      iR += 1;
    }
    return result;
  }
  return (function cut(_array) {
    const len = _array.length;
    if (len === 1) {
      return _array;
    }
    const mid = Math.floor(len / 2);
    const left = _array.slice(0, mid);
    const right = _array.slice(mid, len);
    return merge(cut(left), cut(right));
  }(arr));
}

示例过程:

// 初始
5 4 9 5 3

// 切分
[5 4] [9 5 3]  // 中间数是9
     ^
([5] [4]) [9 5 3]  // 进入左侧数组,中间数是4
    ^
([5] [4]) ([9] [5 3])  // 左侧切分完,进入右侧数组,中间数是5
              ^
([5] [4]) ([9] ([5] [3]))  // 左侧切分完,进入右侧数组,中间数是3
                   ^

// 合并[5]和[3]
([5] [4]) ([9] [3 $])  // 3<5,入3
                ^
([5] [4]) ([9] [3 5])  // 入5,完毕
                  ^

// 合并[9]和[3 5]
([5] [4]) [3 $ $]  // 3<9,入3
           ^
([5] [4]) [3 5 $]  // 5<9,入5
             ^
([5] [4]) [3 5 9]  // 入9,完毕
               ^

// 合并[5]和[4]
[4 $] [3 5 9]  // 4<5,入4
 ^
[4 5] [3 5 9]  // 入5,完毕
   ^

// 合并[4 5]和[3 5 9]
[3 $ $ $ $]  // 4>3,入3
 ^
[3 4 $ $ $]  // 4<5,入4
   ^
[3 4 5 $ $]  // 5=5,入5
     ^
[3 4 5 5 $]  // 入5
       ^
[3 4 5 5 9]  // 入9,完毕
         ^

// 结果
3 4 5 5 9
快速排序

快速排序的思想跟归并很像,都是分治方法,但它没有像归并排序那样将它们分割开,而是使用指针游标来标记,每次会确定一个主元的位置。稍微会比前面的复杂一些。
基本思路:

取数组的第0项作为主元,缓存0号位的数。

设定一个从0号位开始的low指针,一个从末尾开始的high指针;

先从high指针开始移动,指针指向的数与主元做比较,如果大于或等于主元则继续向前移动,如果小于主元则停下并把high指针指向的数替换到当前low指针指向的位置;

再从low指针开始移动,指针指向的数与主元做比较,如果小于或等于主元则继续向后移动,如果大于主元则停下并把low指针指向的数替换到当前high指针指向的位置;

如此循环交替移动两个指针,直到low指针的指向位高于或等于high的指向位;

至此low指向位即是主元的位置pivotloc,将主元写入low指向的位置;

以此位置pivotloc为分割,在左右两边重复上述的步骤,直到排序完成。

代码实现:

function quickSort(_arr) {
  const arr = [].slice.call(_arr);
  function partition(low, high) {
    const pivotkey = arr[low];
    let i = low;
    let j = high;
    while (i < j) {
      while (i < j && arr[j] >= pivotkey) {
        j -= 1;
      }
      arr[i] = arr[j];
      while (i < j && arr[i] <= pivotkey) {
        i += 1;
      }
      arr[j] = arr[i];
    }
    arr[i] = pivotkey;
    return i;
  }
  (function QSort(low, high) {
    if (low < high) {
      const pivotloc = partition(low, high);
      QSort(low, pivotloc - 1);
      QSort(pivotloc + 1, high);
    }
  }(0, arr.length - 1));
  return arr;
}

示例过程:

// 初始
5 4 9 5 3

// 第一趟,主元为5
5 4 9 5 3  // high开始移动,3<5,high停止
^L      ^H
3 4 9 5 3  // 将high指向数3写入到low位置
^L      ^H
3 4 9 5 3  // low开始移动,3<5,继续前进
^L      ^H
3 4 9 5 3  // 4<5,继续前进
  ^L    ^H
3 4 9 5 3  // 9>5,low停止
    ^L  ^H
3 4 9 5 9  // 将low指向数9写入到high位置
    ^L  ^H
3 4 9 5 9  // high开始移动,9>5,继续后退
    ^L  ^H
3 4 9 5 9  // high开始移动,5=5,继续后退
    ^L^H
3 4 5 5 9  // 两指针重合,结束,确定主元5的位置,写入
    *

// 第二趟,主元为3
3 4 5 5 9  // high开始移动,4>3,继续后退
^L^H*
3 4 5 5 9  // 两指针重合,结束,确定主元3的位置,写入
*   *

// 第三趟,主元为4
3 4 5 5 9  // 两指针重合,结束,确定主元4的位置,写入
* * *

// 第四趟,主元为5
3 4 5 5 9  // high开始移动,9>5,继续后退
* * * ^L^H
3 4 5 5 9  // 两指针重合,结束,确定主元5的位置,写入
* * * *

// 第五趟,主元为9
3 4 5 5 9  // 两指针重合,结束,确定主元9的位置,写入
* * * * *

// 结果
3 4 5 5 9
简易性能测试

上述的这么多种排序算法哪个比较快?这是我们比较好奇的问题,我们随机生成10000个数据来测试一下吧。
两个辅助函数:getRandomArray用来生成随机数的数组,costClock用来统计耗时。

function getRandomArray(len = 10000, min = 0, max = 100) {
  const array = [];
  const w = max - min;
  for (let i = 0; i < len; i += 1) {
    array.push(parseInt((Math.random() * w) + min, 10));
  }
  return array;
}

function costClock(fn) {
  const now = new Date().getTime();
  const data = fn();
  const pass = new Date().getTime() - now;
  return {
    data,
    cost: pass,
  };
}

测试用例如下:

const array = getRandomArray(10000);
const result1 = costClock(() => bubbleSort(array));
const result2 = costClock(() => modifiedBubbleSort(array));
const result3 = costClock(() => selectionSort(array));
const result4 = costClock(() => insertionSort(array));
const result5 = costClock(() => mergeSort(array));
const result6 = costClock(() => quickSort(array));
console.log(result1);
console.log(result2);
console.log(result3);
console.log(result4);
console.log(result5);
console.log(result6);

结果如下图,可见快速排序不愧是快速排序,不需要交互数据以及分治方法是其高效的主要原因。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94405.html

相关文章

  • 数据库

    摘要:编辑大咖说阅读字数用时分钟内容摘要对于真正企业级应用,需要分布式数据库具备什么样的能力相比等分布式数据库,他们条最佳性能优化性能优化索引与优化关于索引与优化的基础知识汇总。 mysql 数据库开发常见问题及优化 这篇文章从库表设计,慢 SQL 问题和误操作、程序 bug 时怎么办这三个问题展开。 一个小时学会 MySQL 数据库 看到了一篇适合新手的 MySQL 入门教程,希望对想学 ...

    mengbo 评论0 收藏0
  • 数据库

    摘要:编辑大咖说阅读字数用时分钟内容摘要对于真正企业级应用,需要分布式数据库具备什么样的能力相比等分布式数据库,他们条最佳性能优化性能优化索引与优化关于索引与优化的基础知识汇总。 mysql 数据库开发常见问题及优化 这篇文章从库表设计,慢 SQL 问题和误操作、程序 bug 时怎么办这三个问题展开。 一个小时学会 MySQL 数据库 看到了一篇适合新手的 MySQL 入门教程,希望对想学 ...

    shuibo 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<