JS中sort函数的用法以及底层原理

1,386 阅读7分钟

JS中sort函数的语法

Array.sort(compareFunctuion)

Array.sort(compareFunctuion)中的参数

Array.sort(compareFunctuion)中的参数是一个匿名函数,该参数可传可不传。

一 . JS中sort函数的基本用法

注意:sort函数不会复刻出一个数组,而是在原来的数组上进行排序。

1.sort()不传参

无参数时,调用Array.sort()底层的compareFunctuion的默认值,默认为升序排序,排序方式是根据字符串Unicode码点。

当给字符串进行排序时,sort函数可以运行出正确的结果,但当进行数值排序时,会出现问题。代码如下:

let arr = [455,56,9,90,1,4];
console.log(arr.sort());
// 运行结果为[1, 4, 455, 56, 9, 90]

之所以出现这样的情况是因为,如果不传参数,sort函数底层会运行默认的compareFunctuion函数,其会将数组中的元素全转化为字符串形式,以字符串中每个字符的Unicode编码逐一进行比较,若相等则比较下一个字符,直到出现不相等或没有字符可比较时,才结束。然后按照从小到大的顺序排放。

2.sort()传参

传入的匿名函数可接收两个参数x,y。x,y代表了数组中要比较的元素的值,该匿名函数的返回值是整数,即,根据返回值不同,来决定排序的方式。有以下三种返回值:

注意:只有当传入函数的返回值大于0时,才交换x,y的位置 ,函数的返回值小于等于0不做任何操作

1.当x-y>0,返回一个正整数时,交换 x 与 y 的位置,按升序方式,从小到大排列

let arr = [455,56,9,90,1,4];
console.log(
  arr.sort(function(x,y){
    return x-y
})
);
// 结果[1,4,9,56,90,455]

2.当x-y=0时,x 和 y 的相对位置不变。

3.当x-y<0 ,看成是降序排列,从大到小排序 ,由上面的注意可知,此时return后面要写成y - x。

let arr = [455,56,9,90,1,4];
console.log(
  arr.sort(function(x,y){
    return y-x
})
);
// 结果[455,90,56,9,4,1]

3.数组对象排序

最重要的还是这个对象属性排序,当后台给我们前端很多数据并且没有排序时,我们一般都是要重新进行排序,而后台给的数据往往是好几层,不会像前面那种简单的就一个数组,这个时候就要用sort中对象属性排序了

  // 3.对象属性排序
    var obj = [
        {name:"lucy", num:400},
        {name:"nancy", num:110},
        {name:"maria", num:200}
    ];
   obj.sort(compare("num"));
   console.log(obj);

   //数组对象属性值排序
   function compare(property){
       return function(a,b){
           //value1 - value2升序
           //value2 - value1降序
           var value1 = a[property];
           var value2 = b[property];
           return value1 - value2;//升序
       }
   }

运行结果:
[
{name:"nancy", num:110},
{name:"maria", num:200},
{name:"lucy", num:400}
]
注意: compare()中参数必须是这个对象的属性名称,而你要比较的这些对象里面,一定要有这个属性名称,否则会出错

二 . 底层原理

在V8 引擎 sort 函数使用了两种排序算法,分别是,插入排序InsertionSort 和 快速排序QuickSort。

  • 当 n<=10 时,采用插入排序
  • 当 n>10 时,采用三路快速排序
  • 10<n <=1000,采用中位数作为哨兵元素;
  • n>1000,每隔 200~215 个元素挑出一个元素,放到一个新数组中,然后对它排序,找到中间位置的数,以此作为中位数。
  • 根据数组中元素的个数采用不同的算法,这与两种算法的时间复杂度 O(n)有关。想知道这方面知识,可自行查找
为何要选择哨兵元素

因为快速排序的性能瓶颈在于递归的深度,最坏的情况是每次的哨兵都是最小元素或者最大元素,那么进行 partition(一边是小于哨兵的元素,另一边是大于哨兵的元素)时,就会有一边是空的。如果这么排下去,递归的层数就达到了n, 而每一层的复杂度是 O(n),因此快排这时候会退化O(n^2)级别。

这种情况是要尽力避免的,那么如何来避免?就是让哨兵元素尽可能地处于数组的中间位置,让最大或者最小的情况尽可能少

1.插入排序算法:

将 n 个待排序的元素看成为一个有序表和一个无序表。开始时有序表中只包含一个元素, 无序表中包含有 n-1 个元素, 排序过程中每次从无序表中取出第一个元素, 把它的值依次与有序表元素的值进行比较, 将它插入到有序表中的适当位置, 使之成为新的有序表。

插入排序图解

注意:橙色是已排列好的元素,蓝色是未排列的元素,红色的是正在进行排列的元素

插入排序.gif

2.快速排序算法:

(1).首先在数组中选择一个元素作为一个分界值。即,选定一个元素作为Pivot中心轴。
(2).将大于Pivot的数字放在Pivot的右边
(3).将小于Pivot的数字放在Pivot的左边
(4).重复上述三步操作,直至Pivot中心轴左右的元素个数各为一,即快速排序进行完成。
可以看出,其本质是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

B站上一个挺好的的快速排序算法的讲解视频www.bilibili.com/video/BV1at…

快速排序图解

快速排序.gif

>>(右移运算符)

其实来说,右移运算符就是/2的n次方,然后四舍五入。
如,以下换算 m >> n 等价于 m/2^n,例如,500>>1 = 500 / 2^1 = 250

JS中sort函数底层的实现代码

function InnerArraySort(array, length, comparefn) {
// 判断函数是否作为参数传入
if (!IS_CALLABLE(comparefn)) {
      comparefn = function (x, y) { //sort函数调用默认的ompareFunctuion
        if (x === y) return 0;
        // %表示的是当前调用的方法不是JS里面的方法,而是V8引擎中C++定义的方法。
        if (%_IsSmi(x) && %_IsSmi(y)) {
          //词典比较
          return %SmiLexicographicCompare(x, y);
        }
        x = TO_STRING(x);
        y = TO_STRING(y);
        if (x == y) return 0;
        else return x < y ? -1 : 1;
   };
}
function InsertionSort(a, from, to) {
  // 插入排序 如,
      for (var i = from + 1; i < to; i++) {   //对[7,8,3,4,6,1]排序
        var element = a[i];                   //当i=2时,将数组下表为2的元素取出放入element中
        for (var j = i - 1; j >= from; j--) { //element从数组为下标为1的元素依次向前比较
          var tmp = a[j];                     //内层循环整个数组的变化
          var order = comparefn(tmp, element);//[7,8,3,4,6,1]->
          if (order > 0) {                    //[7,8,8,4,6,1]->
            a[j + 1] = tmp;                   //[7,7,8,4,6,1]
          } else {                            
            break;
          }
        }                                   //内层循环结束后,此时j=-1
        a[j + 1] = element;                 //[7,7,8,4,6,1]->[3,7,8,4,6,1]
                                            //然后按以上步骤将数组的下标2以后的元素依次与前面排列好的元素进行比较
   }
}
function GetThirdIndex(a, from, to) {   // 元素个数大于1000时,寻找基准点
  // new Array()出来的数组向用户公开,而 new InternalArray()出来的数组仅供内部使用。
  var t_array = new InternalArray();
  var increment = 200 + ((to - from) & 15);
  var j = 0;
  from += 1;
  to -= 1;
  // n>1000,每隔 200~215 个元素挑出一个元素,放到一个新数组中,然后对它排序,找到中间位置的数,以此作为中位数。  
  for (var i = from; i < to; i += increment) {
     t_array[j] = [i, a[i]];     // 记录数组元素的位置和对应的值
     j++;
  }
  t_array.sort(function(a, b) {
     return comparefn(a[1], b[1]);
  });
  var third_index = t_array[t_array.length >> 1][0];  // 记录中位数对应的元素下标
  return third_index;
}
function QuickSort(a, from, to) {  // 快速排序实现
      //哨兵位置
      var third_index = 0;
      while (true) {
        if (to - from <= 10) {
          InsertionSort(a, from, to); // 数据量小,使用插入排序,速度较快
          return;
        }
        if (to - from > 1000) {
          third_index = GetThirdIndex(a, from, to);  // 设置哨兵的位置
        } else {
          // (to - from) >> 1所表示的意思可看上面的向右运算符语法,即,小于1000 直接取中位数对应的元素
          third_index = from + ((to - from) >> 1);
        }
        // 下面开始快排
        var v0 = a[from];
        var v1 = a[to - 1];
        var v2 = a[third_index];
        var c01 = comparefn(v0, v1);
        if (c01 > 0) {
          var tmp = v0;
          v0 = v1;
          v1 = tmp;
        }
        var c02 = comparefn(v0, v2);
        if (c02 >= 0) {
          var tmp = v0;
          v0 = v2;
          v2 = v1;
          v1 = tmp;
        } else {
          var c12 = comparefn(v1, v2);
          if (c12 > 0) {
            var tmp = v1;
            v1 = v2;
            v2 = tmp;
          }
        }
        a[from] = v0;
        a[to - 1] = v2;
        var pivot = v1;
        var low_end = from + 1; 
        var high_start = to - 1;
        a[third_index] = a[low_end];
        a[low_end] = pivot;
        // 接下来进行分隔,将小于pivot的数放在其左边,大于pivot的数放在其右边
        // 左右分别开弓
        partition: for (var i = low_end + 1; i < high_start; i++) {
          var element = a[i];
          var order = comparefn(element, pivot);
          当pivot右边的数小于pivot时,交换两个值的位置,然后继续向数组后移动一位在进行比较,直至有数比pivot大时,左边的不动,右边的开弓
          if (order < 0) {
            a[i] = a[low_end];
            a[low_end] = element;
            low_end++;
          } else if (order > 0) {
            // 右边开弓,pivot右边的数比pivot大时,do-while继续循环,下标减一,继续比较,直至有比pivot小的值出现,退出循环。
            do {
              high_start--;
              if (high_start == i) break partition;
              var top_elem = a[high_start];
              order = comparefn(top_elem, pivot);
            } while (order > 0);
            // 将比pivot小的数放在pivot的左边,然后继续左开弓,重复以上步骤。循环全部结束时,pivot左边的数都是比它小的,右边的数都是比他大的。
            a[i] = a[high_start];
            a[high_start] = element;
            if (order < 0) {
              element = a[i];
              a[i] = a[low_end];
              a[low_end] = element;
              low_end++;
            }
          }
        }
        // 快排的核心思路,递归调用快速排序方法
        if (to - high_start < low_end - from) {
          QuickSort(a, high_start, to);
          to = low_end;
        } else {
          QuickSort(a, from, low_end);
          from = high_start;
        }
    }
 }

参考文章作者:

Z_Maple
链接:juejin.cn/post/697798… 来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

作者:鼓玄
链接:juejin.cn/post/684490… 来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

js位移运算符(<<、>>) 请参考下列文章

本文链接:blog.csdn.net/C_fashionCa…

js中sort总结 - 10年码农 - 博客园 www.cnblogs.com/10yearsmano…

JS - sort方法的使用场景

本文链接:blog.csdn.net/qq_43886365…