JS 大数据去重的性能测试大全 千万级数据去重

本文详细介绍了9种在JavaScript中进行数组去重的方法,包括双重for循环、indexOf、filter、sort等,并通过测试不同数据量(1万到1000万,不同重复率)来比较它们的执行效率。测试结果显示,对于简单类型数据,使用Set的去重效率最高,而对于复杂类型数据,Map结构表现更优。此外,文章提供了所有测试代码以供参考和验证。
摘要由CSDN通过智能技术生成

在这里插入图片描述

前言

web 前端或 nodejs 开发中遇到大数据循环、大数据去重时必然会遇到性能的问题。这时,搞清楚哪种方式去重效率最高,选择耗时最少的方案,无疑会带来很大的业务价值。

本文会介绍以下 9 种去重方法,并经过测试验证对比它们的测试性能。

    1. 双重 for 循环 + splice / flag: 使用 splice 删除
    • 双重 for 循环 + splice / flag: 使用 flag 标记
    1. for 循环加 indexOf / includes
    1. filter 加 indexOf
    1. 使用 sort 排序后去重
    1. sort 加 reduce
    1. 利用对象 key 唯一(hasOwnProperty)
    1. 使用 Map 数据结构
    1. 使用 Set
    1. 使用 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 , 有需要的同学可随时下载验证。

参考资料

JavaScript 数组高性能去重 千万级数据去重效率测试 高效去重详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值