热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

总结在前端排序中遇到的问题

这篇文章给大家罗列了在前段排序中会遇到的问题并写了解决方案,非常详细,有需要的朋友可以参考。

貌似前端圈一直以来流传着一种误解:前端用不到算法知识。长久以来,大家或许都曾受这种说法的影响。直到前阵子遇到一个产品需求,回过头来看,发现事实并非如此。

前端排序

前端排序的场景

前端将排序条件作为请求参数传递给后端,后端将排序结果作为请求响应返回前端,这是一种常见设计。但是对于有些产品则不是那么适用。

试想一个场景:你在使用美食类APP时,是否会经常切换排序方式,一会儿按照价格排序,一会儿按照评分排序。

实际生产中,受限于服务器成本等因素,当单次数据查询成为整体性能瓶颈时,也会考虑通过将排序在前端完成的方式来优化性能。

排序算法

感觉这个没什么介绍的必要,作为计算机科学的一种基础算法,描述就直接照搬 Wikipedia

这里存在这一段内容纯粹是为了承(cou)上(man)启(zi)下(shu)。

Javascript的排序

既然说到前端排序,自然首先会想到Javascript的原生接口 Array.prototype.sort

这个接口自 ECMAScript 1st Edition 起就存在。让我们看看最新的规范中关于它的描述是什么样的。

Array.prototype.sort规范

Array.prototype.sort(compareFn)

代码如下:

The elements of this array are sorted. The sort is not necessarily stable (that is, elements that compare equal do not necessarily remain in their original order). If comparefn is not undefined, it should be a function that accepts two arguments x and y and returns a negative value if x y.

显然,规范并没有限定 sort 内部实现的排序算法是什么。甚至接口的实现都不需要是 稳定排序 的。这一点很关键,接下来会多次涉及。

在这样的背景下,前端排序这件事其实取决于各家浏览器的具体实现。那么,当今主流的浏览器关于排序是怎么实现的呢?接下来,我们分别简单对比一下 ChromeFirefoxMicrosoft Edge

Chrome中的实现

Chrome 的Javascript引擎是 v8。由于它是开源的,所以可以直接看源代码。

整个 array.js 都是用 Javascript 语言实现的。排序方法部分很明显比曾经看到过的快速排序要复杂得多,但显然核心算法还是 快速排序。算法复杂的原因在于 v8 出于性能考虑进行了很多优化。(接下来会展开说)

Firefox中的实现

暂时无法确定Firefox的Javascript引擎即将使用的数组排序算法会是什么。[3]

按照现有的信息,SpiderMoney 内部实现了 归并排序。

Microsoft Edge中的实现

Microsoft Edge 的Javascript引擎 Chakra 的核心部分代码已经于2016年初在Github开源。

通过看源代码可以发现,Chakra 的数组排序算法实现的也是 快速排序。而且相比较于 v8,它就只是实现了纯粹的快速排序,完全没有 v8 中的那些性能优化的踪影。

Javascript数组排序的问题

众所周知,快速排序 是一种不稳定的排序算法,而 归并排序 是一种稳定的排序算法。由于不同引擎在算法选择上的差异,导致前端依赖 Array.prototype.sort 接口实现的Javascript代码,在浏览器中实际执行的排序效果是不一致的。

排序稳定性的差异需要有特定的场景触发才会存在问题;在很多情况下,不稳定的排序也不会造成影响。

假如实际项目开发中,对于数组的排序没有稳定性的需求,那么其实看到这里为止即可,浏览器之间的实现差异不那么重要。

但是若项目要求排序必须是稳定的,那么这些差异的存在将无法满足需求。我们需要为此进行一些额外的工作。

举个例子:

某市的机动车牌照拍卖系统,最终中标的规则为:

    1. 按价格进行倒排序;

    2. 相同价格则按照竞标顺位(即价格提交时间)进行正排序。

排序若是在前端进行,那么采用快速排序的浏览器中显示的中标者很可能是不符合预期的

探究差异的背后

寻找解决办法之前,我们有必要先探究一下出现问题的原因。

Chrome为什么采用快速排序

其实这个情况从一开始便存在。

Chrome测试版 于2008年9月2日发布,然而发布后不久,就有开发者向 Chromium 开发组提交#90 Bug反馈v8的数组排序实现不是稳定排序的。

这个Bug ISSUE讨论的时间跨度很大。一直到2015年11月10日,仍然有开发者对v8的数组排序实现问题提出评论。

同时我们还注意到,该ISSUE曾经已被关闭。但是于2013年6月被开发组成员重新打开,作为 ECMAScript Next 规范讨论的参考。

而es-discuss的最后结论是这样的

代码如下:

It does not change. Stable is a subset of unstable. And vice versa, every unstable algorithm returns a stable result for some inputs. Mark's point is that requiring “always unstable” has no meaning, no matter what language you chose.
/Andreas

正如本文前段所引用的已定稿 ECMAScript 2015 规范中的描述。

时代特点

IMHO,Chrome发布之初即被报告出这个问题可能是有其特殊的时代特点。

上文已经说到,Chrome第一版 是 2008年9月 发布的。根据statcounter的统计数据,那个时期市场占有率最高的两款浏览器分别是 IE(那时候只有IE6和IE7) 和 Firefox,市场占有率分别达到了67.16%和25.77%。也就是说,两个浏览器加起来的市场占有率超过了90%。

而根据另一份浏览器排序算法稳定性的统计数据显示,这两款超过了90%市场占有率的浏览器都采用了稳定的数组排序。所以Chrome发布之初被开发者质疑也是合情合理的。

符合规范

从Bug ISSUE讨论的过程中,可以大概理解开发组成员对于引擎实现采用快速排序的一些考量。

其中一点,他们认为引擎必须遵守ECMAScript规范。由于规范不要求稳定排序的描述,故他们认为 v8 的实现是完全符合规范的。

性能考虑

另外,他们认为 v8 设计的一个重要考量在于引擎的性能。

快速排序 相比较于 归并排序,在整体性能上表现更好:

更高的计算效率。快速排序 在实际计算机执行环境中比同等时间复杂度的其他排序算法更快(不命中最差组合的情况下)
更低的空间成本。前者仅有O(㏒n)的空间复杂度,相比较后者O(n)的空间复杂度在运行时的内存消耗更少
v8在数组排序算法中的性能优化

既然说 v8 非常看中引擎的性能,那么在数组排序中它做了哪些事呢?

通过阅读源代码,还是粗浅地学习了一些皮毛。

混合插入排序
快速排序 是分治的思想,将大数组分解,逐层往下递归。但是若递归深度太大,为了维持递归调用栈的内存资源消耗也会很大。优化不好甚至可能造成栈溢出。

目前v8的实现是设定一个阈值,对最下层的10个及以下长度的小数组使用 插入排序。

根据代码注释以及 Wikipedia 中的描述,虽然插入排序的平均时间复杂度为 O(n²) 差于快速排序的 O(n㏒n)。但是在运行环境,小数组使用插入排序的效率反而比快速排序会更高,这里不再展开。

v8代码示例

var QuickSort = function QuickSort(a, from, to) {
  ......
  while (true) {
    // Insertion sort is faster for short arrays.
    if (to - from <= 10) {
      InsertionSort(a, from, to);
      return;
    }
    ......
  }
  ......
};

三数取中

正如已知的,快速排序的阿克琉斯之踵在于,最差数组组合情况下会算法退化。

快速排序的算法核心在于选择一个基准 (pivot),将经过比较交换的数组按基准分解为两个数区进行后续递归。试想如果对一个已经有序的数组,每次选择基准元素时总是选择第一个或者最后一个元素,那么每次都会有一个数区是空的,递归的层数将达到 n,最后导致算法的时间复杂度退化为 O(n²)。因此 pivot 的选择非常重要。

v8采用的是 三数取中(median-of-three) 的优化:除了头尾两个元素再额外选择一个元素参与基准元素的竞争。

第三个元素的选取策略大致为:

当数组长度小于等于1000时,选择折半位置的元素作为目标元素。
当数组长度超过1000时,每隔200-215个(非固定,跟着数组长度而变化)左右选择一个元素来先确定一批候选元素。接着在这批候选元素中进行一次排序,将所得的中位值作为目标元素
最后取三个元素的中位值作为 pivot。

v8代码示例

var GetThirdIndex = function(a, from, to) {
  var t_array = new InternalArray();
  // Use both 'from' and 'to' to determine the pivot candidates.
  var increment = 200 + ((to - from) & 15);
  var j = 0;
  from += 1;
  to -= 1;
  for (var i = from; i > 1][0];
  return third_index;
};

var QuickSort = function QuickSort(a, from, to) {
  ......
  while (true) {
    ......
    if (to - from > 1000) {
      third_index = GetThirdIndex(a, from, to);
    } else {
      third_index = from + ((to - from) >> 1);
    }
  }
  ......
};

原地排序

在温习快速排序算法时,我在网上看到了很多用Javascript实现的例子。

但是发现一大部分的代码实现如下所示

var quickSort = function(arr) {
  if (arr.length <= 1) { return arr; }
  var pivotIndex = Math.floor(arr.length / 2);
  var pivot = arr.splice(pivotIndex, 1)[0];
  var left = [];
  var right = [];
  for (var i = 0; i 

以上代码的主要问题在于:利用 leftright 两个数区存储递归的子数组,因此它需要 O(n) 的额外存储空间。这与理论上的平均空间复杂度 O(㏒n) 相比差距较大。

额外的空间开销,同样会影响实际运行时的整体速度。这也是快速排序在实际运行时的表现可以超过同等时间复杂度级别的其他排序算法的其中一个原因。所以一般来说,性能较好的快速排序会采用原地 (in-place) 排序的方式。

v8 源代码中的实现是对原数组进行元素交换。

Firefox为什么采用归并排序

它的背后也是有故事的。

Firefox其实在一开始发布的时候对于数组排序的实现并不是采用稳定的排序算法,这块有据可考。

Firefox(Firebird)最初版本 实现的数组排序算法是 堆排序,这也是一种不稳定的排序算法。因此,后来有人对此提交了一个Bug。

Mozilla开发组内部针对这个问题进行了一系列讨论。

从讨论的过程我们能够得出几点

1.同时期 Mozilla 的竞争对手是 IE6,从上文的统计数据可知IE6是稳定排序的

2.Javascript之父 Brendan Eich 觉得 Stability is good

3.Firefox在采用 堆排序 之前采用的是 快速排序

基于开发组成员倾向于实现稳定的排序算法为主要前提,Firefox3 将 归并排序 作为了数组排序的新实现。

解决排序稳定性的差异

以上说了这么多,主要是为了讲述各个浏览器对于排序实现的差异,以及解释为什么存在这些差异的一些比较表层的原因。

但是读到这里,读者可能还是会有疑问:如果我的项目就是需要依赖稳定排序,那该怎么办呢?

解决方案

其实解决这个问题的思路比较简单。

浏览器出于不同考虑选择不同排序算法。可能某些偏向于追求极致的性能,某些偏向于提供良好的开发体验,但是有规律可循。

从目前已知的情况来看,所有主流浏览器(包括IE6,7,8)对于数组排序算法的实现基本可以枚举:

1.归并排序 / Timsort

2.快速排序

所以,我们将快速排序经过定制改造,变成稳定排序的是不是就可以了?

一般来说,针对对象数组使用不稳定排序会影响结果。而其他类型数组本身使用稳定排序或不稳定排序的结果是相等的。因此方案大致如下:

将待排序数组进行预处理,为每个待排序的对象增加自然序属性,不与对象的其他属性冲突即可。
自定义排序比较方法compareFn,总是将自然序作为前置判断相等时的第二判断维度。

面对归并排序这类实现时由于算法本身就是稳定的,额外增加的自然序比较并不会改变排序结果,所以方案兼容性比较好。

但是涉及修改待排序数组,而且需要开辟额外空间用于存储自然序属性,可想而知 v8 这类引擎应该不会采用类似手段。不过作为开发者自行定制的排序方案是可行的。

方案代码示例

'use strict';

const INDEX = Symbol('index');

function getComparer(compare) {
  return function (left, right) {
    let result = compare(left, right);

    return result === 0 &#63; left[INDEX] - right[INDEX] : result;
  };
}

function sort(array, compare) {
  array = array.map(
    (item, index) => {
      if (typeof item === 'object') {
        item[INDEX] = index;
      }

      return item;
    }
  );

  return array.sort(getComparer(compare));
}

以上只是一个简单的满足稳定排序的算法改造示例。

之所以说简单,是因为实际生产环境中作为数组输入的数据结构冗杂,需要根据实际情况判断是否需要进行更多样的排序前类型检测。

标注

1.前端现在已经是一个比较宽泛的概念。本文中的前端主要指的是以浏览器作为载体,以 Javascript 作为编程语言的环境

2.本文无意于涉及算法整体,谨以常见的排序算法作为切入点

3.在确认 Firefox 数组排序实现的算法时,搜到了 SpiderMoney 的一篇排序相关的Bug。大致意思是讨论过程中有人建议用极端情况下性能更好的 Timsort 算法替换 归并排序,但是 Mozilla 的工程师表示由于 Timsort 算法存在License授权问题,没办法在 Mozilla 的软件中直接使用算法,等待对方的后续回复

总结

以上就是大家在前端排序中遇到的问题总结和解决方案,这几年越来越多的项目正在往富客户端应用方向转变,前端在项目中的占比变大。随着未来浏览器计算能力的进一步提升,它允许进行一些更复杂的计算。伴随职责的变更,前端的形态也可能会发生一些重大变化。行走江湖,总要有一技傍身。


推荐阅读
  • 本文由编程笔记#小编为大家整理,主要介绍了markdown[软件代理设置]相关的知识,希望对你有一定的参考价值。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 解决Cydia数据库错误:could not open file /var/lib/dpkg/status 的方法
    本文介绍了解决iOS系统中Cydia数据库错误的方法。通过使用苹果电脑上的Impactor工具和NewTerm软件,以及ifunbox工具和终端命令,可以解决该问题。具体步骤包括下载所需工具、连接手机到电脑、安装NewTerm、下载ifunbox并注册Dropbox账号、下载并解压lib.zip文件、将lib文件夹拖入Books文件夹中,并将lib文件夹拷贝到/var/目录下。以上方法适用于已经越狱且出现Cydia数据库错误的iPhone手机。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 本文讨论了编写可保护的代码的重要性,包括提高代码的可读性、可调试性和直观性。同时介绍了优化代码的方法,如代码格式化、解释函数和提炼函数等。还提到了一些常见的坏代码味道,如不规范的命名、重复代码、过长的函数和参数列表等。最后,介绍了如何处理数据泥团和进行函数重构,以提高代码质量和可维护性。 ... [详细]
  • macOS Big Sur全新设计大版本更新,10+个值得关注的新功能
    本文介绍了Apple发布的新一代操作系统macOS Big Sur,该系统采用全新的界面设计,包括图标、应用界面、程序坞和菜单栏等方面的变化。新系统还增加了通知中心、桌面小组件、强化的Safari浏览器以及隐私保护等多项功能。文章指出,macOS Big Sur的设计与iPadOS越来越接近,结合了去年iPadOS对鼠标的完善等功能。 ... [详细]
  • 2016 linux发行版排行_灵越7590 安装 linux (manjarognome)
    RT之前做了一次灵越7590黑苹果炒作业的文章,希望能够分享给更多不想折腾的人。kawauso:教你如何给灵越7590黑苹果抄作业​zhuanlan.z ... [详细]
  • JavaScript简介及语言特点
    本文介绍了JavaScript的起源和发展历程,以及其在前端验证和服务器端开发中的应用。同时,还介绍了ECMAScript标准、DOM对象和BOM对象的作用及特点。最后,对JavaScript作为解释型语言和编译型语言的区别进行了说明。 ... [详细]
  • 本文整理了常用的CSS属性及用法,包括背景属性、边框属性、尺寸属性、可伸缩框属性、字体属性和文本属性等,方便开发者查阅和使用。 ... [详细]
  • 我只是互联网中的菜鸟一个,由于心血来潮也整了一个个人站,但在网络中游荡了大半个世纪,才发现给网站定位是多么的重要,只有好的运营模式及盈利模式,网站才能发展的更好,否则累死也赚不服务 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • this prototype 闭包 总结
    this对象整理下思路:一般用到this中的情景:1.构造方法中functionA(){this.nameyinshen;}varanewA() ... [详细]
  • (PC+WAP)织梦模板户外设备类网站
    模板介绍:织梦内核开发的模板,该模板属于户外设备、宣传栏类企业都可使用,这款模板使用范围极广,不仅仅局限于一类型的企业&#x ... [详细]
  • pyecharts 介绍
    一、pyecharts介绍ECharts,一个使用JavaScript实现的开源可视化库,可以流畅的运行在PC和移动设备上,兼容当前绝大部 ... [详细]
author-avatar
知无不言之歌
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有