### JavaScript 数组高性能去重 千万级数据去重效率测试 高效去重详解 + [引言](#引言) + [测试前提](#测试前提) + [测试准备](#测试准备) + [测试效率](#测试效率) + + [1.双重for循环 + splice / flag](##1.双重for循环 + splice / flag) + [2.for 循环加 indexOf / includes](##2.for 循环加 indexOf / includes) + [3.filter 加 indexOf](##3.filter 加 indexOf) + [4.使用sort排序后去重](##4.使用sort排序后去重) + [5.sort 加 reduce](##5.sort 加 reduce) + [6.利用对象key唯一(hasOwnProperty)](##6.利用对象key唯一(hasOwnProperty)) + [7.使用Map数据结构](##7.使用Map数据结构) + [8.使用Set](##8.使用Set) + [总结推荐](#总结推荐) + [循环效率排行](#循环效率排行) + [结语](#结语) # 引言 网上能搜出一堆关于数据去重的例子,以及测试效率耗时都不尽相同。然而却没给出必要的解释,为什么不同人测试出来的效率不一样呢?本文就详细通过大量测试探索一下,去重效率损耗主要在哪儿?不一样的数据为什么用不同去重方案测试效率耗时不一样?给千万级的数据去重该选用哪种去重方法? # 测试前提 1.测试工具 `chrome浏览器` 2.测试电脑配置 `小米笔记本` `i7-8500U` `16GB` 3.系统 `windows10` `64位` # 测试准备 分别创建一个1万、10万、100万、1000万的简单数据,这里我们不取随机数,创建固定重复的量的数据(分别重复20%,50%,80%)。并使重复数据平均分布,测试各种常用去重方法效率。 ```javascript // distinctTest.js var myriad = []; // 代码就写一个1万的例子,避免篇幅过长。 [2, 5, 8].map(item => { myriad.push(Array.from(new Array(10000), function (item2, i) { return i % 10 < item || Math.random(); })) // 可在此处添加十万、百万.. 数据 }) function distinct(arr) { // 此处为去重代码 return arr; } myriad.forEach(item => { var length = item.length; console.log(`数据长度:${item.length}`) console.time("耗时") var newArr = distinct(item) console.timeEnd("耗时") console.log(`数据重复量:${length - newArr.length}`) console.log('————————————————————————————————————————————') }) 123456789101112131415161718192021222324 ``` # 测试效率 ## 1.双重for循环 + splice / flag **方法解释:** 第一种在元素重复时删除重复的元素,剩余的元素就是去重后的数据。`splice`会改变原数组,如果不想更改,可以用`concat`复制一份,如:`var newArr = arr.concat()`不传入数据相当于复刻一份。 第二种就是用flag标记,新数组中存在相同数据就不往新数组插入当前数据。 **去重代码:** ```javascript // 使用splice删除 function distinct(arr) { for (var i = 0, len = arr.length; i < len; i++) { for (var i2 = i + 1; i2 < len; i2++) { if (arr[i] === arr[i2]) { arr.splice(i2, 1); // 删除重复的数据 i2--; // 删除数据后index需要前移一位 len = arr.length; // 删除数据后重新获取数组长度 } } } return arr; } // 使用flag标记 function distinct(arr) { var newArr = [] for (var i = 0, len = arr.length; i < len; i++) { var flag = true for (var i2 = 0; i2 < newArr.length; i2++) { if (arr[i] === newArr[i2]) { flag = false break; } } flag && newArr.push(arr[i]) } return newArr; } 123456789101112131415161718192021222324252627282930 ``` **测试结果:** `splice`  `flag` ```bash `数据长度:10000` | `数据长度:100000` | `数据长度:1000000` 数据重复量:1999 | 数据重复量:19999 | 页面崩溃 耗时: 98.783935546875ms | 耗时: 9475.10009765625ms | —————————————————————————— | —————————————————————————— | 数据重复量:4999 | 数据重复量:49999 | 耗时: 46.31298828125ms | 耗时: 3659.33984375ms | —————————————————————————— | ——————————————-——————————— | 数据重复量:7999 | 数据重复量:79999 | 耗时: 7.68310546875ms | 耗时: 587.00927734375ms | 123456789 ``` **结果描述:** 双重循环去重,使用`splice`,会更改原来数组数据,在数据不到1万时,处理效率还可接受。达到10万时需要10多秒,达到100万页面直接崩溃。使用`flag`判断稍快于上方,而且重复数据越多,耗时差异越大。 **方法效率分析:** **1**.`splice`删除数据元素,100万条数据都删除,耗时在8秒(8000ms)左右,波动幅度较大,根据重复量多少,效率损耗有明显差异。 **2**. 1万条平均分布的数据,重复20%,需要循环**3千多万次**,重复50%,需要循环**1千多万次**,重复80%,需要循环**2百多万次**。而十万条数据在此之上**翻了100倍**,从以上1万数据与10万数据耗时也能看出,时长比超过**100倍**。因循环次数的急剧增多,耗时也100倍增长。 ## 2.for 循环加 indexOf / includes **方法解释:** 此方法通过判断新数组中是否存在**当前循环到的元素**,如果没有就把**当前循环到的元素**插入新数组。 **去重代码:** ```javascript function distinct(arr) { var newArray = []; for (let i = 0, len = arr.length; i < len; i++) { newArray.indexOf(arr[i]) === -1 && newArray.push(arr[i]) // !newArray.includes(arr[i]) && newArray.push(arr[i]) } return newArray; } 12345678 ``` **测试结果:**  **结果描述:** `for` 也可以替换为 `for of`,虽然`for of`循环比`for`慢,但是`for of`直接返回元素本身,而不需要通过下标取数组数据,`for in`就不需要考虑了,同样返回下标,却比`for`慢了将近1万倍。不过`for in`可以遍历对象,这是其他循环比不了的。 处理1万以内数据效率很高,10万条时,就需要几秒了。百万页面虽未直接崩溃,等待几分钟还未出结果,不过比上面双重循环效率高。`indexOf`与`includes`效率相同,只是返回值不一样。考虑到`indexOf`兼容性强于`includes`,推荐使用`indexOf`,当然写法上`includes`更加简洁。**其实这种方法和上方的双重循环是一样的**,因为`indexOf`和`includes`都是内部循环从数据下标0处开始循环查询,**数据越靠数组后方,查询耗时越长**,因为循环次数越多。 **方法效率分析:** **1**.经测试,`indexOf`和`includes`效率比的`for`循环判断值快了一倍左右,所以虽然实际上都是双重循环,但是整体效率高于双重`for`循环。 **2**.即使在千万级的数据下循环加`push`耗时也不多,**2千万次**循环加`push`耗时不超过200毫秒。 **3**.主要耗时还是在`indexOf`与`includes`判断数据是否重复这一点上,千万级的数据,如果要判断数据是否重复,不重复的数据要从头循环到尾,耗时非常明显。重复数据越少,耗时越多。 ## 3.filter 加 indexOf **方法解释:** 通过`indexOf`判断此元素第一次出现的位置与当前的元素的`index`是否相同来去重。 **去重代码:** ```javascript function distinct(arr) { return arr.filter((item, i) => { return arr.indexOf(item) === i }); } 12345 ``` **测试结果:**  **结果描述:** 在效率稍上低于`for`加`indexOf`的方法,数据量重复较少的情况与`for` 加`indexOf`处理效率上差不多,但数据量多的时候效率就有了明显的差别,同样在千万级的数据下无法处理。 **方法效率分析:** **1**.此方法本质上与上述两种一样,都是双重循环,同样使用了`indexOf`,主要的效率损耗就在个上面。 **2**.经测试`filter`都返回`false`比都返回`true`要快,也就是**重复越多越快**,这就引起我们猜想,返回`true`类似于`push`了新数据到新数组。 **3**.重复量越大,与`for`加`indexOf`去重差距就越明显,数组`filter`方法效率明显要低于`for`循环(最后有纯粹的循环效率耗时对比)。 ## 4.使用sort排序后去重 **方法解释:** 利用`sort`排序,使相同元素相邻排列,通过判断**当前**下标与**前一个**下标元素是否相同从而插入新数据。我见过有人用**当前**下标与**下一个**下标元素判断,但是那种方法最后一个元素是`undefined`时,会造成`undefined`无法被判断为不重复的元素,要解决这个问题就会造成多的判断,以至于效率下降。`sort`排序会更改原数组,如果不想更改可以用`concat()`拷贝一份,如:`var newArr = arr.concat()`原为数组拼接,不传入数据相当于复刻一份。 **去重代码:** ```javascript function distinct(arr) { arr.sort((a, b) => a - b) var arrry = [arr[0]]; for (var i = 1, len = arr.length; i < len; i++) { if (arr[i] !== arr[i - 1]) { arrry.push(arr[i]); } } return arrry; } 12345678910 ``` **测试结果:**  **结果描述:** 使用`sort`排序后去重效率有了显著的提升,数据1百万也能在1秒内处理完。即使1千万也能勉强处理。不过当前配置电脑处理超过2千万的数据,会进入断点提示**Paused before potential out-of-memory crash(在潜在的内存不足崩溃之前暂停)**。在去重**数值**类型数据上`sort`排序去重效率有很大提升。 **方法效率分析:** **1**.利用了`sort`排序,主要计算时间就在排序上。如果是顺序的数值类型数组,在`sort`排序上所需时间就极少,经测试,这种数据即使1千万条、重复20%,也能在1秒内处理完。 **2**. 这种方法也能去重其他类型数据,只需要把`sort`排序传入的**函数**去掉就行。不过在数据处理的效率上就比较差了,10万数据时就已经接近1秒钟了,100万也只能勉强处理,千万就需要用**分钟**计算了。 ## 5.sort 加 reduce **方法解释:** 使用`sort`排序后,利用`reduce`特性,对比新数组**最后一个元素**与循环到的**当前元素**是否相等,不相等就插入数据。 **去重代码:** ```javascript function distinct(arr) { arr.sort((a, b) => a - b) return arr.reduce((newArr, current) => { if (newArr[newArr.length - 1] !== current) { newArr.push(current); } return newArr; }, [arr[0]]); } 123456789 ``` **测试结果:**  **结果描述:** 此方法与上面一种方法效率相似,数据较小时(10万以内),差异并不大,数据较大时能看出差异。同样当前配置电脑处理超过2千万的数据,会进入断点提示**Paused before potential out-of-memory crash(在潜在的内存不足崩溃之前暂停)**。 **方法效率分析:** **1**.与上一个方法一样利用了`sort`排序,主要计算时间就在排序上。 **2**.此方法与上面相比差异不大,虽然`reduce`循环比`for`循环慢(后面有纯粹循环耗时排行),但在取值对比上面稍快。 ## 6.利用对象key唯一(hasOwnProperty) **方法解释:** 对象key具有唯一性,通过给对象key赋值、取值判断元素是否重复,不重复就`push`数据到新数组,重复就跳过。 **去重代码:** ```javascript function distinct(arr) { var newArrry = []; var obj = {}; for (var i = 0, len = arr.length; i < len; i++) { if (obj[arr[i]] !== 1) { // obj.hasOwnProperty(arr[i]) 也可以 newArrry.push(arr[i]) obj[arr[i]] = 1 } } return newArrry; } 1234567891011 ``` **测试结果:**  **结果描述:** 此方法计算速度还不错,与`sort`排序后去重效率相当,同样是超过2千万数据后会进入断点(同上)。不过对象属性不重复有比较大的问题就是 1.`true` 和`"true"`在设为对象key时,值是一样的。(解决方法 使用 `typeof`+`key`) 2.无法设置复杂类型数据为key,结果key只能为`[object Object]`(解决方法 使用 `typeof`+`toString`) 处理的方法过多会造成效率的下降,因此不再此方法上多下功夫,如果数据类型明确,也可以使用这种方法。 **方法效率分析:** **1**.对象属性的存取需要消耗比较多的时间(利用`hasOwnProperty`判断属性是否存在效率也是一样)。 **2**.剩余少部分时间消耗在循环和`push`数据上。 **3**.数据是短字符串的情况下,此方法效率还能提升**5倍**左右。 ## 7.使用Map数据结构 **方法解释:** 与对象`key`唯一去重方法一致,也是通过`get`、`set`数据判断元素是否重复,也可以使用`has`判断。 **去重代码:** ```javascript function distinct(arr) { const newArray = []; const newMap = new Map(); for (let i = 0, len = arr.length; i < len; i++) { if (!newMap.get(arr[i])) { // newMap.has(arr[i]) newMap.set(arr[i], 1); newArray.push(arr[i]); } } return newArray; } 1234567891011 ``` **测试结果:**  **结果描述:** 利用Map数据结构不重复,处理效率还不错,目前效率最高的一种去重方案,不会打乱数据,不会因数据类型问题不好处理。目前只测试到1千万数据,2千万开始本机配置极易进入断点(同上) **方法效率分析:** **1**.此方法时间也主要损耗在`Map`数据的存与取上面,不过效率比对象存取要高。 ## 8.使用Set **方法解释:** 最简单的方法,通过`new` 一个 `Set`对象(只能存不同数据),放入重复数据的数组,再解构出来。 **去重代码:** ```javascript function distinct(arr) { return [...new Set(arr)]; } 123 ``` **测试结果:**  **结果描述:** `es6` 提供的`Set`使用方法最简单,达到的效果也很喜人,目前效率与利用`Map`去重效率相当,不过考虑到`Set`兼容性,只能兼容到`IE11`,并且`IE11`不支持`Array.from`,以及解构。在不考虑IE兼容的情况下推荐使用`Set`。 **方法效率分析:** `Set`去重方法,全部由`Set`数据结构不重复特性处理,没有具体耗时地方,整个方法就是耗时处。 # 总结推荐 **1.首先在不考虑IE兼容的情况下推荐使用`Set`去重**,可以看到每个去重方法具体效率都跟数据量、重复量相关。当然有的也跟数据类型相关,比如使用`sort`排序去重,**2.如果是有序的数值,`sort`排序去重无疑是最快的**,兼容性也很好。如果能确定需要**3.去重的数据类型是简单字符串,推荐使用对象key唯一去重**,经测试,在数据是短字符串的情况下,对象key唯一的去重方式在效率上还能提升**5倍**以上。如果数据类型比较复杂,而又不需要兼容IE10以下,**4.推荐使用`Map`数据唯一去重**。以上所有测试结果都能看到,重复量越大,处理速度越快,反而重复得越少,需要处理的时间就越长。选择去重方案也需要考虑当前项目中是否有使用`babel`工具,即使用了兼容性不好的`Set`、`Map`方法,`babel`也会打包为`ES5`语法。然而并不知道具体会打包成怎样去重,所以上诉方法都请视项目情况使用。 # 循环效率排行 只论循环效率,不考虑内容,看看循环具体排行如何。 ```bash var arr = Array.from(new Array(10000000)); // 创建1千万条数据的数组 console.time('耗时') // 循环体 console.timeEnd('耗时') for // 耗时: 6.65283203125ms - 7.085205078125ms while // 耗时:19.105712890625ms - 19.453857421875ms do while // 耗时:18.89697265625ms - 19.087890625ms for of // 耗时:102.4921875ms - 103.44921875ms forEach() // 耗时:103.834228515625ms - 104.614990234375ms filter() // 耗时:109.2978515625ms - 114.50390625ms reduce() // 耗时:113.99609375ms - 116.65625ms map() // 耗时:140.9560546875ms - 143.347900390625ms for in // 耗时:2094.56201171875ms - 2811.3017578125ms 12345678910111213141516 ``` 数组的方法还有很多就不一一列出来了 可以去此处参考数组对象方法 [Array 对象](https://www.runoob.com/jsref/jsref-obj-array.html) # 结语 说实话,网上虽说`js`去重方法一大堆,但是不管怎么拼凑其实就那几种类型。也见过用`递归`方式去重的方法,不过效率实在太差,数据量稍微大一点就报错。以上所有数据与测试皆是本人实测,在不同电脑、系统、浏览器上会有明显差异。 看在花了大量时间测试的情况下,可以点个赞哦,如果错误欢迎指出。 Last modification:June 8, 2023 © From the Internet Support Appreciate the author AliPayWeChat Like 0 If you think my article is useful to you, please feel free to appreciate
2 comments
文章的叙述风格独特,用词精准,让人回味无穷。
作者的布局谋篇匠心独运,让读者在阅读中享受到了思维的乐趣。