JS数组sort方法如何使用
这次给大家带来JS数组sort方法如何使用,JS数组sort方法使用的注意事项有哪些,下面就是实战案例,一起来看一下。
算法课上,我们会接触很多种排序算法,什么冒泡排序、选择排序、快速排序、堆排序等等。那么javascript的sort方法采用哪种排序算法呢?要搞清楚这个问题,呃,直接看v8源代码好了。v8中对Array.sort的实现是采用javascript完成的,粗看下来,使用了快速排序算法,但明显比我们熟悉的快速排序要复杂。那么到底复杂在什么地方?为什么要搞这么复杂?这是我们今天要探讨的问题。
快速排序算法
快速排序算法之所以被称为快速排序算法,是因为它能达到最佳和平均时间复杂度均为O(nlogn),是一种应用非常广泛的排序算法。它的原理并不复杂,先找出一个基准元素(pivot,任意元素均可),然后让所有元素跟基准元素比较,比基准元素小的,放到一个集合中,其他的放到另一个集合中;再对这两个集合执行快速排序,最终得到完全排序好的序列。
所以快速排序的核心是不断把原数组做切割,切割成小数组后再对小数组进行相同的处理,这是一种典型的分治的算法设计思路。实现一个简单的快速排序算法并不困难。我们不妨试一下:
function QuickSort(arr, func) { if (!arr || !arr.length) return []; if (arr.length === 1) return arr; var pivot = arr[0]; var smallSet = []; var bigSet = []; for (var i = 1; i < arr.length; i++) { if (func(arr[i], pivot) < 0) { smallSet.push(arr[i]); } else { bigSet.push(arr[i]); } } return QuickSort(smallSet, func).concat([pivot]).concat(QuickSort(bigSet, func)); }
这是一个非常基础的实现,选取数组的第一项作为基准元素。
原地(in-place)排序
我们可以注意到,上面的算法中,我们其实是创建了一个新的数组作为计算结果,从空间使用的角度看是不经济的。javascript的快速排序算法中并没有像上面的代码那样创建一个新的数组,而是在原数组的基础上,通过交换元素位置实现排序。所以,类似于push、pop、splice这几个方法,sort方法也是会修改原数组对象的!
我们前面说过,快速排序的核心在于切割数组。那么如果只是在原数组上交换元素,怎么做到切割数组呢?很简单,我们并不需要真的把数组切割出来,只需要记住每个部分起止的索引号。举个例子,假设有一个数组[12, 4, 9, 2, 18, 25],选取第一项12为基准元素,那么按照原始的快速排序算法,会把这个数组切割成两个小数组:[4, 9, 2], 12, [18, 25]。但是我们同样可以不切割,先通过比较、交换元素,将原数组修改成[4, 9, 2, 12, 18, 25],再根据基准元素12的位置,认为0~2号元素是一组,4~5号元素是一组,为了表述方便,我这里将比基准元素小的元素组成的分区叫小数分区,另一个分区叫大数分区。这很像电脑硬盘的分区,并不是真的把硬盘分成了C盘、D盘,而是记录下一些起止位置,在逻辑上分成了若干个分区。类似的,在快速排序算法中,我们也把这个过程叫做分区(partition)。所以相应的,我也要修改一下之前的说法了,快速排序算法的核心是分区。
说了这么多,还是实现一个带分区的快速排序吧:
function swap(arr, from, to) { if (from == to) return; var temp = arr[from]; arr[from] = arr[to]; arr[to] = temp; } function QuickSortWithPartition(arr, func, from, to) { if (!arr || !arr.length) return []; if (arr.length === 1) return arr; from = from || 0; to = to || arr.length - 1; var pivot = arr[from]; var smallIndex = from; var bigIndex = from + 1; for (; bigIndex <= to; bigIndex++) { if (func(arr[bigIndex], pivot) < 0) { smallIndex++; swap(arr, smallIndex, bigIndex); } } swap(arr, smallIndex, from); QuickSortWithPartition(arr, func, from, smallIndex - 1); QuickSortWithPartition(arr, func, smallIndex + 1, to); return arr; }
看起来代码长了很多,不过并不算复杂。首先由于涉及到数组元素交换,所以先实现一个swap方法来处理元素交换。快速排序算法中,增加了两个参数,from和to,分别表示当前要处理这个数组的哪个部分,from是起始索引,to是终止索引;如果这两个参数缺失,则表示处理整个数组。
同样的,我用最简单的方式选取基准元素,即所要处理分区的第一个元素。然后我定义了smallIndex和bigIndex两个变量,分别表示的是左侧小数分区的终止索引和右侧大数分区的终止索引。什么意思?就是说从第一个元素(基准元素)到第smallIndex个元素间的所有元素都比基准元素小,从第smallIndex + 1到第bigIndex个元素都比基准元素大。一开始没有比较时,很显然这两部分分区都是空的,而比较的过程很简单,直接是bigIndex向右移,一直移到分区尾部。每当bigIndex增加1,我们会进行一次判断,看看这个位置上的元素是不是比基准元素大,如果大的话,不用做处理,它已经处于大数分区了;但如果比基准元素小,就需要进行一次交换。怎么交换呢?首先将smallIndex增加1,意味着小数分区增加了一个元素,但此时smallIndex位置的元素很明显是一个大数(这个说法其实不对,如果之前大数分区里面没有元素,此时smallIndex和bigIndex相等,但对交换没有影响),而在bigIndex位置的元素是一个小数,所以只要把这两个位置的元素交换一下就好了。
最后可别忘了一开始的起始元素,它的位置并不正确,不过只要将它和smallIndex位置的元素交换位置就可以了。同时我们得到了对应的小数分区[from...smallIndex - 1]和大数分区[smallIndex + 1...to]。再对这两个分区递归排序即可。
分区过程的优化
上面的分区过程(仅仅)还是有一定的优化空间的,因为上面的分区过程中,大数分区和小数分区都是从左向右增长,其实我们可以考虑从两侧向中间遍历,这样能有效地减少交换元素的次数。举个例子,例如我们有一个数组[2, 1, 3, 1, 3, 1, 3],采用上面的分区算法,一共碰到三次比基准元素小的情况,所以会发生三次交换;而如果我们换个思路,把从右往左找到小于基准和元素,和从左往右找到大于基准的元素交换,这个数组只需要交换一次就可以了,即把第一个3和最后一个1交换。
我们也来尝试写一下实现:
function QuickSortWithPartitionOp(arr, func, from, to) { if (!arr || !arr.length) return []; from = from || 0; to = to || arr.length - 1; if (from >= to - 1) return arr; var pivot = arr[from]; var smallEnd = from + 1; var bigBegin = to; while (smallEnd < bigBegin) { while (func(arr[bigBegin], pivot) > 0 && smallEnd < bigBegin) { bigBegin--; } while (func(arr[smallEnd], pivot) < 0 && smallEnd < bigBegin) { smallEnd++; } if (smallEnd < bigBegin) { swap(arr, smallEnd, bigBegin); } } swap(arr, smallEnd, from); QuickSortWithPartitionOp(arr, func, from, smallEnd - 1); QuickSortWithPartitionOp(arr, func, smallEnd + 1, to); return arr; }
分区与性能
前面我们说过,快速排序算法平均时间复杂度是O(nlogn),但它的最差情况下时间复杂度会衰弱到O(n2)。而性能好坏的关键就在于分区是否合理。如果每次都能平均分成相等的两个分区,那么只需要logn层迭代;而如果每次分区都不合理,总有一个分区是空的,那么需要n层迭代,这是性能最差的场景。
那么性能最差的场景会出现吗?对于一个内容随机的数组而言,不太可能出现最差情况。但我们平时在编程时,处理的数组往往并不是内容随机的,而是很可能预先有一定顺序。设想一下,如果一个数组已经排好序了,由于之前的算法中,我们都是采用第一个元素作为基准元素,那么必然会出现每次分区都会有一个分区为空。这种情况当然需要避免。
一种很容易的解决方法是不要选取固定位置的元素作为基准元素,而是随机从数组里挑出一个元素作为基准元素。这个方法很有效,极大概率地避免了最差情况。这种处理思想很简单,我就不另外写代码了。
然而极大概率地避免最差情况并不等于避免最差情况,特别是对于数组很大的时候,更要求我们在选取基准元素的时候要更谨慎些。
三数取中(median-of-three)
基准元素应当精心挑选,而挑选基准元素的一种方法为三数取中,即挑选基准元素时,先把第一个元素、最后一个元素和中间一个元素挑出来,这三个元素中大小在中间的那个元素就被认为是基准元素。
简单实现一下获取基准元素的方法:
function getPivot(arr, func, from, to) { var middle = (from + to) >> 1; var i0 = arr[from]; var i1 = arr[to]; var i2 = arr[middle]; var temp; if (func(i0, i1) > 0) { temp = i0; i0 = i1; i1 = temp; } if (func(i0, i2) > 0) { arr[middle] = i0; arr[from] = i2; arr[to] = i1; return i0; } else { arr[from] = i0; if (func(i1, i2) > 0) { arr[middle] = i1; arr[to] = i2; return i1; } else { arr[middle] = i2; arr[to] = i1; return i2; } } }
这个例子里我完全没管基准元素的位置,一是降低复杂度,另一个原因是下面讨论重复元素处理时,基准元素的位置没什么意义。不过我把最小的值赋给了第一个元素,最大的值赋给了第二个元素,后面处理重复元素时会有帮助。
当然,仅仅是三数取中获得的基准元素,也不见得是可靠的。于是有一些其他的取中值的方法出现。有几种比较典型的手段,一种是平均间隔取一个元素,多个元素取中位数(即多取几个,增加可靠性);一种是对三数取中进行递归运算,先把大数组平均分成三块,对每一块进行三数取中,会得到三个中值,再对这三个中值取中位数。
不过查阅v8的源代码,发现v8的基准元素选取更为复杂。如果数组长度不超过1000,则进行基本的三数取中;如果数组长度超过1000,那么v8的处理是除去首尾的元素,对剩下的元素每隔200左右(200~215,并不固定)挑出一个元素。对这些元素排序,找出中间的那个,并用这个元素跟原数组首尾两个元素一起进行三数取中。这段代码我就不写了。
针对重复元素的处理
到目前为止,我们在处理元素比较的时候比较随意,并没有太多地考虑元素相等的问题。但实际上我们做了这么多性能优化,对于重复元素引起的性能问题并没有涉及到。重复元素会带来什么问题呢?设想一下,一个数组里如果所有元素都相等,基准元素不管怎么选都是一样的。那么在分区的时候,必然出现除基准元素外的其他元素都被分到一起去了,进入最差性能的case。
那么对于重复元素应该怎么处理呢?从性能的角度,如果发现一个元素与基准元素相同,那么它应该被记录下来,避免后续再进行不必要的比较。所以还是得改分区的代码。
function QuickSortWithPartitionDump(arr, func, from, to) { if (!arr || !arr.length) return []; from = from || 0; to = to || arr.length - 1; if (from >= to - 1) return arr; var pivot = getPivot(arr, func, from, to); var smallEnd = from; var bigBegin = to; for (var i = smallEnd + 1; i < bigBegin; i++) { var order = func(arr[i], pivot); if (order < 0) { smallEnd++; swap(arr, i, smallEnd); } else if (order > 0) { while (bigBegin > i && order > 0) { bigBegin--; order = func(arr[bigBegin], pivot); } if (bigBegin == i) break; swap(arr, i, bigBegin); if (order < 0) { swap(arr, i, smallEnd); smallEnd++; } } } QuickSortWithPartitionDump(arr, func, from, smallEnd); QuickSortWithPartitionDump(arr, func, bigBegin, to); return arr; }
简单解释一下这段代码,上文已经说过,在getPivot方法中,我将比基准小的元素放到第一位,把比基准大的元素放到最后一位。定义三个变量smallEnd、bigBegin、i,从from到smallEnd之间的元素都比基准元素小,从smallEnd到i之间的元素都和基准元素一样大,从i到bigBegin之间的元素都是还没有比较的,从bigBegin到to之间的元素都比基准元素大。了解这个关系就好理解这段代码了。遍历从smallEnd + 1到bigBegin之间的元素:
* 如果这个元素小于基准,那么smallEnd增加1,这时smallEnd位置的元素是等于基准元素的(或者此时smallEnd与i相等),交换smallEnd与i处的元素就可以了。
* 如果这个元素大于基准,相对比较复杂一点。此时让bigBegin减小1,检查大数分区前面一个元素是不是大于基准,如果大于基准,重复此步骤,不断让bigBegin减小1,直到找到不比基准大的元素(如果这个过程中,发现bigBegin与i相等,则中止遍历,说明分区结束)。找到这个不比基准大小元素时需要区分是不是比基准小。如果比基准小,需要做两步交换,先将i位置的大数和bigBegin位置的小数交换,这时跟第一种case同时,smallEnd增加1,并且将i位置的小数和smallEnd位置的元素交换。如果和基准相等,则只需要将i位置的大数和bigBegin位置的小数交换。
* 如果这个元素与基准相等,什么也不用做。
小数组优化
对于小数组(小于16项或10项。v8认为10项以下的是小数组。),可能使用快速排序的速度还不如平均复杂度更高的选择排序。所以对于小数组,可以使用选择排序法要提高性能,减少递归深度。
function insertionSort(a, func, from, to) { for (var i = from + 1; i < to; i++) { var element = a[i]; for (var j = i - 1; j >= from; j--) { var tmp = a[j]; if (func(tmp, element) > 0) { a[j + 1] = tmp; } else { break; } } a[j + 1] = element; } }
v8引擎没有做的优化
由于快速排序的不稳定性(少数情况下性能差,前文已经详细描述过),David Musser于1997设计了内省排序法(Introsort)。这个算法在快速排序的基础上,监控递归的深度。一旦长度为n的数组经过了logn层递归(快速排序算法最佳情况下的递归层数)还没有结束的话,就认为这次快速排序的效率可能不理想,转而将剩余部分换用其他排序算法,通常使用堆排序算法(Heapsort,最差时间复杂度和最优时间复杂度均为nlogn)。
v8引擎额外做的优化
快速排序递归很深,如果递归太深的话,很可以出现“爆栈”,我们应该尽可能避免这种情况。上面提到的对小数组采用选择排序算法,以及采用内省排序算法都可以减少递归深度。不过v8引擎中,做了一些不太常见的优化,每次我们分区后,v8引擎会选择元素少的分区进行递归,而将元素多的分区直接通过循环处理,无疑这样的处理大大减小了递归深度。我大致把v8这种处理的过程写一下:
function quickSort(arr, from, to){ while(true){ // 排序分区过程省略 // ... if (to - bigBegin < smallEnd - from) { quickSort(a, bigBegin, to); to = smallEnd; } else { quickSort(a, from, smallEnd); from = bigBegin; } } }
不得不说是一个很巧妙的实现。
相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!
推荐阅读:
以上是JS数组sort方法如何使用的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

热门话题

磁力链接是一种用于下载资源的链接方式,相比传统的下载方式更为便捷和高效。使用磁力链接可以通过点对点的方式下载资源,而不需要依赖中介服务器。本文将介绍磁力链接的使用方法及注意事项。一、什么是磁力链接磁力链接是一种基于P2P(Peer-to-Peer)协议的下载方式。通过磁力链接,用户可以直接连接到资源的发布者,从而完成资源的共享和下载。与传统的下载方式相比,磁

mdf文件和mds文件怎么用随着计算机技术的不断进步,我们可以通过多种方式来存储和共享数据。在数字媒体领域,我们经常会遇到一些特殊的文件格式。在这篇文章中,我们将讨论一种常见的文件格式——mdf和mds文件,并介绍它们的使用方法。首先,我们需要了解mdf文件和mds文件的含义。mdf是CD/DVD镜像文件的扩展名,而mds文件则是mdf文件的元数据文件。

CrystalDiskMark是一款适用于硬盘的小型HDD基准测试工具,可以快速测量顺序和随机读/写速度。接下来就让小编为大家介绍一下CrystalDiskMark,以及crystaldiskmark如何使用吧~一、CrystalDiskMark介绍CrystalDiskMark是一款广泛使用的磁盘性能测试工具,用于评估机械硬盘和固态硬盘(SSD)的读写速度和随机I/O性能。它是一款免费的Windows应用程序,并提供用户友好的界面和各种测试模式来评估硬盘驱动器性能的不同方面,并被广泛用于硬件评

foobar2000是一款能随时收听音乐资源的软件,各种音乐无损音质带给你,增强版本的音乐播放器,让你得到更全更舒适的音乐体验,它的设计理念是将电脑端的高级音频播放器移植到手机上,提供更加便捷高效的音乐播放体验,界面设计简洁明了易于使用它采用了极简的设计风格,没有过多的装饰和繁琐的操作能够快速上手,同时还支持多种皮肤和主题,根据自己的喜好进行个性化设置,打造专属的音乐播放器支持多种音频格式的播放,它还支持音频增益功能根据自己的听力情况调整音量大小,避免过大的音量对听力造成损害。接下来就让小编为大

网易邮箱,作为中国网民广泛使用的一种电子邮箱,一直以来以其稳定、高效的服务赢得了用户的信赖。而网易邮箱大师,则是专为手机用户打造的邮箱软件,它极大地简化了邮件的收发流程,让我们的邮件处理变得更加便捷。那么网易邮箱大师该如何使用,具体又有哪些功能呢,下文中本站小编将为大家带来详细的内容介绍,希望能帮助到大家!首先,您可以在手机应用商店搜索并下载网易邮箱大师应用。在应用宝或百度手机助手中搜索“网易邮箱大师”,然后按照提示进行安装即可。下载安装完成后,我们打开网易邮箱账号并进行登录,登录界面如下图所示

在如今云存储已经成为我们日常生活和工作中不可或缺的一部分。百度网盘作为国内领先的云存储服务之一,凭借其强大的存储功能、高效的传输速度以及便捷的操作体验,赢得了广大用户的青睐。而且无论你是想要备份重要文件、分享资料,还是在线观看视频、听取音乐,百度网盘都能满足你的需求。但是很多用户们可能对百度网盘app的具体使用方法还不了解,那么这篇教程就将为大家详细介绍百度网盘app如何使用,还有疑惑的用户们就快来跟着本文详细了解一下吧!百度云网盘怎么用:一、安装首先,下载并安装百度云软件时,请选择自定义安装选

MetaMask(中文也叫小狐狸钱包)是一款免费的、广受好评的加密钱包软件。目前,BTCC已支持绑定MetaMask钱包,绑定后可使用MetaMask钱包进行快速登入,储值、买币等,且首次绑定还可获得20USDT体验金。在BTCCMetaMask钱包教学中,我们将详细介绍如何注册和使用MetaMask,以及如何在BTCC绑定并使用小狐狸钱包。MetaMask钱包是什么?MetaMask小狐狸钱包拥有超过3,000万用户,是当今最受欢迎的加密货币钱包之一。它可免费使用,可作为扩充功能安装在网络

长按音箱的播放键后,在软件中连接wifi即可使用。教程适用型号:小米12系统:EMUI11.0版本:小爱同学2.4.21解析1首先找到音箱的播放键,长按进入配网模式。2在手机上的小爱音箱软件中登录小米账号,点击添加新的小爱音箱。3输入wifi的名称和密码后,即可呼唤小爱同学进行使用了。补充:小爱音箱有什么功能1小爱音箱有系统功能、社交功能、娱乐功能、知识功能、生活功能、智能家庭、训练计划。总结/注意事项手机要提前安装好小爱同学APP,方便连接和使用。
