前言
web 前端或 nodejs 开发中遇到大数据循环、大数据去重时必然会遇到性能的问题。这时,搞清楚哪种方式去重效率最高,选择耗时最少的方案,无疑会带来很大的业务价值。
本文会介绍以下 9 种去重方法,并经过测试验证对比它们的测试性能。
-
- 双重 for 循环 + splice / flag: 使用 splice 删除
- 双重 for 循环 + splice / flag: 使用 flag 标记
-
- for 循环加 indexOf / includes
-
- filter 加 indexOf
-
- 使用 sort 排序后去重
-
- sort 加 reduce
-
- 利用对象 key 唯一(hasOwnProperty)
-
- 使用 Map 数据结构
-
- 使用 Set
-
- 使用 Map 的唯一 Key + Map.values()
测试准备
分别创建一个 1 万、10 万、100 万、1000 万的简单数据,这里我们不取随机数,创建固定重复的量的数据(分别重复 20%,50%,80%)。并使重复数据平均分布,测试各种常用去重方法效率。
/**
* 生成指定长度且包含20%、50%、80%重复值的二维数组
* @param {number} dataLen
* @return {array} 长度为3的二维数组
*/
function genDataWithSpecLen(dataLen) {
var data = [];
[2, 5, 8].map((item) => {
data.push(
Array.from(new Array(dataLen), function (item2, i) {
return i % 10 < item || Math.random();
})
);
});
return data;
}
封装去重函数
/**
* 使用console.time统计数据去重的消耗时间
* @param {*} distinctFunc 去重函数
* @param {*} repeatData 包含20%、50%、80%重复率数据的二维数组
* @return {*}
*/
function distinctTest(distinctFunc, repeatData) {
repeatData.forEach((item) => {
console.group();
var length = item.length;
console.log(`数据长度:%c${item.length.toLocaleString()}`, "color: green");
console.time("耗时");
const startTime = Date.now();
var newArr = distinctFunc(item);
const endTime = Date.now();
console.timeEnd("耗时");
console.log("耗时计算:", endTime - startTime);
console.log(`数据重复量:%c${(length - newArr.length).toLocaleString()}`, "color: blue");
console.log("————————————————————————————————————————————");
console.groupEnd();
});
}
测试环境
- 测试电脑
MacBook Air (M1,2020)
- 测试浏览器
Chrome 版本 114.0.5735.90(正式版本) (arm64)
封装不同方式的去重函数
1. 双重 for 循环 + splice / flag
/**
* 1. 双重for循环 + splice / flag: 使用splice删除
* @param {*} arr
* @returns
*/
function distinctBySplice(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;
}
/**
* 1. 双重for循环 + splice / flag: 使用flag标记
* @param {*} arr
* @returns
*/
function distinctByFlag(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;
}
2. for 循环加 indexOf / includes
/**
* 2. for 循环加 indexOf / includes
* @param {*} arr
* @returns
*/
function distinctByIndexOf(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;
}
3. filter 加 indexOf
/**
* 3. filter 加 indexOf
* @param {*} arr
* @returns
*/
function distinctByFilter(arr) {
return arr.filter((item, i) => {
return arr.indexOf(item) === i;
});
}
4. 使用 sort 排序后去重
/**
* 4. 使用sort排序后去重
* @param {*} arr
* @returns
*/
function distinctBySort(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;
}
5. sort 加 reduce
/**
* 5. sort 加 reduce
* @param {*} arr
* @returns
*/
function distinctBySortReduce(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]]
);
}
6. 利用对象 key 唯一(hasOwnProperty)
/**
* 6. 利用对象key唯一(hasOwnProperty)
* @param {*} arr
* @returns
*/
function distinctByHasOwnProperty(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;
}
7. 使用 Map 数据结构
/**
* 7. 使用Map数据结构
* @param {*} arr
* @returns
*/
function distinctByMap(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;
}
8. 使用 Set
/**
* 8. 使用Set (仅限相同类型的简单数据)
* @param {*} arr
* @returns
*/
function distinctBySet(arr) {
return [...new Set(arr)];
}
9. 使用 Map 的唯一 Key + Map.values()
/**
* 9. 使用 Map 的唯一 Key + Map.values()
* @param {*} arr
* @returns
*/
function distinctByMapKey(arr) {
const newMap = new Map();
for (let i = 0, len = arr.length; i < len; i++) {
if (!newMap.has(arr[i])) {
// newMap.has(arr[i])
newMap.set(arr[i], arr[i]);
}
}
return Array.from(newMap.values());
}
测试去重效率
1. 通过 console.time 计算耗时,验证单个测试不同去重函数的效率
/**
* 使用console.time统计数据去重的消耗时间
* @param {*} distinctFunc 去重函数
* @param {*} repeatData 包含20%、50%、80%重复率数据的二维数组
* @return {*}
*/
function distinctTestByConsoleTime(distinctFunc, repeatData) {
repeatData.forEach((item) => {
console.group();
var length = item.length;
console.log(`数据长度:%c${item.length.toLocaleString()}`, "color: green");
console.time("耗时");
const startTime = Date.now();
var newArr = distinctFunc(item);
const endTime = Date.now();
console.timeEnd("耗时");
console.log("耗时计算:", endTime - startTime);
console.log(`数据重复量:%c${(length - newArr.length).toLocaleString()}`, "color: blue");
console.log("————————————————————————————————————————————");
console.groupEnd();
});
}
/**
* 按指定去重方法测试去重并打印测试结果(覆盖20%、50%、80%重复率)
* @param {*} distinctBySpecFunc 去重函数
* @param {*} distinctTypeDesc 去重方法说明
* @param {*} maxDataLen 去重的最大数据量,默认:10w -> 5, 可选: 100w -> 6 | 1000w -> 7
* @return {*}
*/
function distinctBySpec(distinctBySpecFunc, distinctTypeDesc, maxDataLen = 5) {
console.group(distinctTypeDesc);
distinctTestByConsoleTime(distinctBySpecFunc, genDataWithSpecLen(10 ** 4));
distinctTestByConsoleTime(distinctBySpecFunc, genDataWithSpecLen(10 ** 5));
if (maxDataLen >= 6) {
distinctTestByConsoleTime(distinctBySpecFunc, genDataWithSpecLen(10 ** 6));
}
if (maxDataLen >= 7) {
distinctTestByConsoleTime(distinctBySpecFunc, genDataWithSpecLen(10 ** 7));
}
console.groupEnd(distinctTypeDesc);
}
function distinctMain() {
distinctBySpec(distinctBySplice, "1. 双重for循环 + splice / flag: 使用splice删除");
distinctBySpec(distinctByFlag, "1. 双重for循环 + splice / flag: 使用flag标记");
distinctBySpec(distinctByIndexOf, "2. for 循环加 indexOf / includes");
distinctBySpec(distinctByFilter, "3. filter 加 indexOf");
distinctBySpec(distinctBySort, "4. 使用sort排序后去重", 7);
distinctBySpec(distinctBySortReduce, "5. sort 加 reduce", 7);
distinctBySpec(distinctByHasOwnProperty, "6. 利用对象key唯一(hasOwnProperty)", 7);
distinctBySpec(distinctByMap, "7. 使用Map数据结构", 7);
distinctBySpec(distinctBySet, "8. 使用Set (仅限相同类型的简单数据)", 7);
distinctBySpec(distinctByMapKey, "9. 使用Map的Key唯一值", 7);
}
2. 使用 endTime - startTime 计算耗时,使用 console.table 全面对比不同去重函数的效率
/**
* 获取按指定去重方法测试去重后的测试结果(仅包含指定重复率的测试结果)
* @param {*} distinctFunc 去重函数
* @param {*} repeatData 包含重复数据的长度为3的二维数组
* @param {*} timeLen 去重测试的耗时方法说明
* @return {*}
*/
function distinctTestWithFormat(distinctFunc, repeatData, timeLen) {
return repeatData.map((item) => {
const testResult = {};
var length = item.length;
testResult["数据长度"] = item.length.toLocaleString();
const startTime = Date.now();
var newArr = distinctFunc(item);
const endTime = Date.now();
testResult["数据重复量"] = (length - newArr.length).toLocaleString();
// testResult[timeLen ?? '耗时(ms)'] = (endTime - startTime).toLocaleString();
testResult.time = {
label: timeLen ?? "耗时(ms)",
value: (endTime - startTime).toLocaleString(),
};
return testResult;
});
}
/**
* 获取按指定去重方法测试去重后的测试结果(覆盖20%、50%、80%重复率)
* @param {*} distinctBySpecFunc 去重函数
* @param {*} distinctTypeDesc 去重方法说明
* @param {*} maxDataLen 去重的最大数据量,默认:10w -> 5, 可选: 100w -> 6 | 1000w -> 7
* @return {DistinctWithFormatReturn}
*/
function distinctBySpecWithFormatReturn(distinctBySpecFunc, distinctTypeDesc, maxDataLen = 5) {
const resArr = [distinctTestWithFormat(distinctBySpecFunc, genDataWithSpecLen(10 ** 4), distinctTypeDesc), distinctTestWithFormat(distinctBySpecFunc, genDataWithSpecLen(10 ** 5), distinctTypeDesc)];
if (maxDataLen >= 6) {
resArr.push(distinctTestWithFormat(distinctBySpecFunc, genDataWithSpecLen(10 ** 6), distinctTypeDesc));
}
if (maxDataLen >= 7) {
resArr.push(distinctTestWithFormat(distinctBySpecFunc, genDataWithSpecLen(10 ** 7), distinctTypeDesc));
}
const allPerfLabel = [
["1w 20%重复", "1w 50%重复", "1w 80%重复"],
["10w 20%重复", "10w 50%重复", "10w 80%重复"],
["100w 20%重复", "100w 50%重复", "100w 80%重复"],
["1000w 20%重复", "1000w 50%重复", "1000w 80%重复"],
];
return new Array(resArr.length * 3).fill("").map((el, index) => {
const [i, j] = [Math.floor(index / 3), index % 3];
el = { ["指标项"]: allPerfLabel?.[i]?.[j] };
const resItem = resArr[i][j];
if (resItem) {
el["数据重复量"] = resItem["数据重复量"];
el[resItem.time.label] = resItem.time.value;
el.time = {
label: resItem.time.label,
value: resItem.time.value,
};
}
return el;
});
}
function distinctMainWithFormat() {
console.time("测试完成耗时");
const allTestRes = [distinctBySpecWithFormatReturn(distinctBySplice, "1. 双重for循环 + splice / flag: 使用splice删除"), distinctBySpecWithFormatReturn(distinctByFlag, "1. 双重for循环 + splice / flag: 使用flag标记"), distinctBySpecWithFormatReturn(distinctByIndexOf, "2. for 循环加 indexOf / includes"), distinctBySpecWithFormatReturn(distinctByFilter, "3. filter 加 indexOf"), distinctBySpecWithFormatReturn(distinctBySort, "4. 使用sort排序后去重", 7), distinctBySpecWithFormatReturn(distinctBySortReduce, "5. sort 加 reduce", 7), distinctBySpecWithFormatReturn(distinctByHasOwnProperty, "6. 利用对象key唯一(hasOwnProperty)", 7), distinctBySpecWithFormatReturn(distinctByMap, "7. 使用Map数据结构", 7), distinctBySpecWithFormatReturn(distinctBySet, "8. 使用Set", 7), distinctBySpecWithFormatReturn(distinctByMapKey, "9. 使用Map的Key唯一值", 7)];
const allPerfLabel = [
["1w 20%重复", "1w 50%重复", "1w 80%重复"],
["10w 20%重复", "10w 50%重复", "10w 80%重复"],
["100w 20%重复", "100w 50%重复", "100w 80%重复"],
["1000w 20%重复", "1000w 50%重复", "1000w 80%重复"],
];
const allTestReport = new Array(4 * 3).fill("").map((el, index) => {
const [i, j] = [Math.floor(index / 3), index % 3];
el = { ["指标项"]: allPerfLabel?.[i]?.[j] };
allTestRes.forEach((item, k) => {
const resItem = item[index];
try {
if (resItem) {
el["数据重复量"] = resItem["数据重复量"];
el[resItem.time.label] = resItem.time.value;
}
} catch (error) {
console.log("error", error);
debugger;
}
});
console.log("el", el);
return el;
});
console.timeEnd("测试完成耗时");
console.table(allTestReport);
}
循环效率验证
/**
* 计算耗时的公共函数
* @param {*} timingType
* @param {*} cb 计算耗时的实际方法体
* @return {*}
*/
function useTiming(timingType, cb) {
console.time(timingType);
cb();
console.timeEnd(timingType);
console.log("————————————————————————————————————————————");
}
// 循环效率计时
function LoopTestMain() {
var array = Array.from(new Array(10000000)),
len = array.length; // 创建1千万条数据的数组
useTiming("for耗时", () => {
for (let i = 0; i < len; i++) {
// 循环体
}
});
useTiming("while耗时", () => {
let i = 0;
while (i < len) {
i++;
}
});
useTiming("for of耗时", () => {
for (const iterator of array) {
}
});
useTiming("forEach耗时", () => {
array.forEach((element) => {});
});
useTiming("filter耗时", () => {
array.filter((element) => {});
});
useTiming("reduce耗时", () => {
array.reduce((element) => {});
});
useTiming("map耗时", () => {
array.map((element) => {});
});
useTiming("for in耗时", () => {
for (const key in array) {
}
});
}
通过 1000w 条数据测试,实践证明,循环相率:
for
> while
> reduce
> forEach
> filter
> for of
> map
> for in
测试总结
经过实践验证,前三种方法都属于双重 for 循环,测试 100w 数据时直接卡死,不推荐。
除前三种方法之外,其他去重效率综合排名:
8. 使用Set
> 7. 使用 Map 数据结构
> 9. 使用 Map 的唯一 Key + Map.values()
> 4. 使用 sort 排序后去重
> 5. sort 加 reduce
> 6. 利用对象 key 唯一(hasOwnProperty)
如果是简单类型的数据去重,推荐8. 使用Set
;如果是复杂类型数据去重,推荐 7. 使用 Map 数据结构
此次验证的所有代码都放在 GitHub , 有需要的同学可随时下载验证。