JavaScript排序,不仅是冒泡

作编程,排序是个必然的需求。前端也不例外,虽然很少,可是你确定会遇到。javascript

不过说到排序,最容易想到的就是冒泡排序,选择排序,插入排序了。前端

冒泡排序

依次比较相邻的两个元素,若是后一个小于前一个,则交换,这样从头至尾一次,就将最大的放到了末尾。java

从头至尾再来一次,因为每进行一轮,最后的都已是最大的了,所以后一轮须要比较次数能够比上一次少一个。虽然你仍是可让他从头至尾来比较,可是后面的比较是没有意义的无用功,为了效率,你应该对代码进行优化。node

图片演示以下:git

代码实现:算法

 
function bubbleSort(arr) { var len = arr.length; for (var i = 0; i < len - 1; i++) { for (var j = 0; j < len - 1 - i; j++) { if (arr[j] > arr[j+1]) { // 相邻元素两两对比 var temp = arr[j+1]; // 元素交换 arr[j+1] = arr[j]; arr[j] = temp; } } } return arr; }

选择排序

选择排序我以为是最简单的了,大一学VB的时候,就只记住了这个排序方法,原理很是简单:每次都找一个最大或者最小的排在开始便可。chrome

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置编程

  2. 再从剩余未排序元素中继续寻找最小(大)元素,而后放到已排序序列的末尾。数组

  3. 重复第二步,直到全部元素均排序完毕。数据结构

动图演示:

代码演示:

 
function selectionSort(arr) { var len = arr.length; var minIndex, temp; for (var i = 0; i < len - 1; i++) { minIndex = i; for (var j = i + 1; j < len; j++) { if (arr[j] < arr[minIndex]) { // 寻找最小的数 minIndex = j; // 将最小数的索引保存 } } temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; } return arr; }

插入排序

插入排序也比较简单。就像打扑克同样,依次将拿到的元素插入到正确的位置便可。

  1. 将第一待排序序列第一个元素看作一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

  2. 从头至尾依次扫描未排序序列,将扫描到的每一个元素插入有序序列的适当位置。(若是待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

动图演示:

代码示例:

 
function insertionSort(arr) { var len = arr.length; var preIndex, current; for (var i = 1; i < len; i++) { preIndex = i - 1; current = arr[i]; while(preIndex >= 0 && arr[preIndex] > current) { arr[preIndex+1] = arr[preIndex]; preIndex--; } arr[preIndex+1] = current; } return arr; }

简单的代价是低效

上面三种都是很是简单的排序方法,简单的同时呢,效率也会比较低,仍是拿这本书里的对比图来讲明:

时间复杂度都高达 O(n^2) ,而它们后面的一些排序算法时间复杂度基本都只有 O(n log n) 。

个人强迫症又犯了,我想要高效率一点的排序方法。

归并排序

简单把这本书的内容过了一遍,当时就理解了这个归并排序,所以这里就谈一下这个归并排序吧。

基本原理是分治法,就是分开而且递归来排序。

步骤以下:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  4. 重复步骤 3 直到某一指针达到序列尾;
  5. 将另外一序列剩下的全部元素直接复制到合并序列尾。

动图演示:

代码示例:

 
function mergeSort(arr) { // 采用自上而下的递归方法 var len = arr.length; if(len < 2) { return arr; } var middle = Math.floor(len / 2), left = arr.slice(0, middle), right = arr.slice(middle); return merge(mergeSort(left), mergeSort(right)); } function merge(left, right) { var result = []; while (left.length && right.length) { if (left[0] <= right[0]) { result.push(left.shift()); } else { result.push(right.shift()); } } while (left.length) result.push(left.shift()); while (right.length) result.push(right.shift()); return result; }

既然是个爱折腾的人,折腾了总得看看效果吧。

效率测试

因为我学这个来进行排序不是对简单数组,数组内都是对象,要对对象的某个属性进行排序,还要考虑升降序。

所以个人代码实现以下:

 
/** * [归并排序] * @param {[Array]} arr [要排序的数组] * @param {[String]} prop [排序字段,用于数组成员是对象时,按照其某个属性进行排序,简单数组直接排序忽略此参数] * @param {[String]} order [排序方式 省略或asc为升序 不然降序] * @return {[Array]} [排序后数组,新数组,并不是在原数组上的修改] */ var mergeSort = (function() { // 合并 var _merge = function(left, right, prop) { var result = []; // 对数组内成员的某个属性排序 if (prop) { while (left.length && right.length) { if (left[0][prop] <= right[0][prop]) { result.push(left.shift()); } else { result.push(right.shift()); } } } else { // 数组成员直接排序 while (left.length && right.length) { if (left[0] <= right[0]) { result.push(left.shift()); } else { result.push(right.shift()); } } } while (left.length) result.push(left.shift()); while (right.length) result.push(right.shift()); return result; }; var _mergeSort = function(arr, prop) { // 采用自上而下的递归方法 var len = arr.length; if (len < 2) { return arr; } var middle = Math.floor(len / 2), left = arr.slice(0, middle), right = arr.slice(middle); return _merge(_mergeSort(left, prop), _mergeSort(right, prop), prop); }; return function(arr, prop, order) { var result = _mergeSort(arr, prop); if (!order || order.toLowerCase() === 'asc') { // 升序 return result; } else { // 降序 var _ = []; result.forEach(function(item) { _.unshift(item); }); return _; } }; })();

须要对哪一个属性进行排序是不肯定,能够随意指定,所以写成了参数。有因为不想让这些东西在每次循环都进行判断,所以代码有点冗余。

关于降序的问题,也没有加入参数中,而是简单的升序后再逆序输出。缘由是不想让每次循环递归里都去判断条件,因此简单处理了。

下面就是见证效率的时候了,一段数据模拟:

 
var getData = function() { return Mock.mock({ "list|1000": [{ name: '@cname', age: '@integer(0,500)' }] }).list; };

实际测试来啦:

 
// 效率测试 var arr = getData(); console.time('归并排序'); mergeSort(arr, 'age'); console.timeEnd('归并排序'); console.time('冒泡排序'); for (var i = 0, l = arr.length; i < l - 1; ++i) { var temp; for (var j = 0; j < l - i - 1; ++j) { if (arr[j].age > arr[j + 1].age) { temp = arr[j + 1]; arr[j + 1] = arr[j]; arr[j] = temp; } } } console.timeEnd('冒泡排序');

进行了五次,效果以下:

 
// 归并排序: 6.592ms // 冒泡排序: 25.959ms // 归并排序: 1.334ms // 冒泡排序: 20.078ms // 归并排序: 1.085ms // 冒泡排序: 16.420ms // 归并排序: 1.200ms // 冒泡排序: 16.574ms // 归并排序: 2.593ms // 冒泡排序: 12.653ms

最低4倍,最高近16倍的效率之差仍是比较满意的。

虽然 1000 条数据让前端排序的可能性不大,可是几十上百条的状况仍是有的。另外因为node, JavaScript 也能运行的服务端了,这个效率的提高也仍是有用武之地的。

一点疑问

归并排序里面使用了递归,在《数据结构与算法 JavaScript 描述》中,做者给出了自下而上的迭代方法。可是对于递归法,做者却认为:

However, it is not possible to do so in JavaScript, as the recursion goes too deep for the language to handle.

然而,在 JavaScript 中这种方式不太可行,由于这个算法的递归深度对它来说太深了。

gitbook上这本书的做者对此有疑问,我也有疑问。

归并中虽然用了递归,可是他是放在 return 后的呀。关于在renturn后的递归是有尾递归优化的呀。

关于尾递归优化是指:原本外层函数内部再调用一个函数的话,因为外层函数须要等待内层函数返回后才能返回结果,进入内层函数后,外层函数的信息,内存中是必须记住的,也就是调用堆栈。而内部函数放在 return 关键字后,就表示外层函数到此也就结束了,进入内层函数后,没有必要再记住外层函数内的全部信息。

上面是个人理解的描述,不知道算不算准确。 chrome 下已经能够开启尾递归优化的功能了,我以为这个递归是不应影响他在 JavaScript 下的使用的。

最后

有兴趣的话,推荐读读这本书,进行排序的时候,能够考虑一些更高效的方法。

不过须要注意的是,这些高效率的排序方法,通常都须要相对较多的额外内存空间,须要权衡一下。

另外,很是小规模的数据就没有必要了。一是影响过小,而是咱们人的效率问题,一分钟能从头写个冒泡、选择、插入的排序方法,而换成是归并排序呢?

 

来自:http://blog.cdswyda.com/post/javascript/2017-03-22-js-sort-not-only-bubblesort

相关文章
相关标签/搜索