数组去重:从暴力到优雅——六种方法彻底拿下面试官

**大厂面试系列 · JavaScript **
· 时间复杂度全覆盖 · 面试高频


目录

  1. 题目与背景
  2. 六种解法逐一拆解
  3. 复杂度对比总表
  4. 面试答题策略
  5. 代码健壮性要点

1. 题目与背景

数组去重是大厂手写题的经典考点,表面上考的是 API 熟悉度,实质上考察的是算法思维和复杂度意识。面试官真正想听到的不只是"用 Set",而是你能否按复杂度梯度逐步优化,并解释每步优化背后的原理。

题目: 实现一个 unique(arr) 函数,对数组 [1, 2, 3, 2, 5] 去重,返回 [1, 2, 3, 5]。请尽可能多地给出不同实现方式。

相关标签: 数组 API 时间复杂度 空间复杂度 ES6 健壮性校验


2. 六种解法逐一拆解

方法一:双重循环(暴力法)

最直观的思路:遍历原数组,每个元素与已收集的结果数组比较,没出现过就加进去。

function unique(arr) {
  if (!Array.isArray(arr)) return [];
  let res = [arr[0]];
  for (let i = 1; i < arr.length; i++) {
    let flag = true;   // 默认未重复
    for (let j = 0; j < res.length; j++) {
      if (arr[i] === res[j]) { // === 严格相等,类型+值都要一致
        flag = false;
        break;
      }
    }
    if (flag) res.push(arr[i]);
  }
  return res;
}
维度 说明
时间复杂度 O(n²) 两重循环嵌套
空间复杂度 O(n) 结果数组
适用场景 面试讲原理 展示基础思维

注意点:使用 ===(严格相等) 而非 ==,避免 1 == "1" 这类弱类型陷阱,是写出健壮代码的基本意识。


方法二:indexOf 遍历

利用数组的 indexOf 方法替代内层 for 循环,代码更简洁,逻辑相同,复杂度不变。

function unique(arr) {
  if (!Array.isArray(arr)) return [];
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    // indexOf 返回 -1 说明 res 中还没有该元素
    if (res.indexOf(arr[i]) === -1) {
      res.push(arr[i]);
    }
  }
  return res;
}

本质: indexOf 内部仍然是线性扫描,所以外层 O(n) × 内层 O(n) = O(n²),与方法一相同,只是写法更优雅。


方法三:filter + indexOf(函数式写法)

利用 filter 的高阶函数风格,一行表达式完成去重。核心判断:如果某元素第一次出现的下标等于当前下标,说明它还没被记录过。

function unique(arr) {
  if (!Array.isArray(arr)) return [];
  return arr.filter(function(item, index) {
    // 首次出现位置 === 当前位置,则保留
    return arr.indexOf(item) === index;
  });
}

filter 回调返回 true 则保留元素,返回 false 则丢弃。该写法简洁,但 indexOf 依然是 O(n) 内部扫描,整体时间复杂度仍为 O(n²)


方法四:先排序再比较相邻

先调用 sort() 将数组排好序,重复元素必然相邻,只需一次线性遍历比较相邻项即可。

function unique(arr) {
  if (!Array.isArray(arr)) return [];
  arr = arr.sort();   // O(nlogn)
  let res = [arr[0]];
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] !== arr[i - 1]) {   // 相邻不同则保留
      res.push(arr[i]);
    }
  }
  return res;
}
维度 说明
时间复杂度 O(n log n) 排序主导
缺点 改变顺序 原数组顺序丢失

优化明显:从 O(n²) 降到 O(n log n)。代价是排序会改变元素原始顺序,面试时要主动说出这个 trade-off。


方法五:对象 HashMap(空间换时间)

用 JS 对象(Object 字面量)模拟 HashMap,以元素值作为 key,利用对象属性读写的 O(1) 平均时间,将整体复杂度降到线性。

// 空间换时间:额外 O(n) 空间,换取 O(n) 时间
function unique(arr) {
  if (!Array.isArray(arr)) return [];
  let res = [],
      obj = {};   // 对象字面量充当 HashMap
  for (let i = 0; i < arr.length; i++) {
    if (!obj[arr[i]]) {     // key 不存在 → 新元素
      res.push(arr[i]);
      obj[arr[i]] = 1;
    } else {
      obj[arr[i]]++;         // 统计出现次数(可选)
    }
  }
  return res;
}

注意坑点: 对象的 key 会被自动转为字符串,obj[1]obj["1"] 指向同一个 key,导致数字 1 和字符串 "1" 会被误判为重复。面试中需主动指出。


方法六:ES6 Set(最优解)

Set 是 ES6 引入的内置数据结构,天然不存储重复值,底层基于 HashMap,增删查均为 O(1),配合扩展运算符一行搞定。

function unique(arr) {
  if (!Array.isArray(arr)) return [];
  return [...new Set(arr)];
  // 等价写法:Array.from(new Set(arr))
}
维度 说明
时间复杂度 O(n) HashMap O(1) 查找
代码量 1 行 极致简洁
类型区分 ✓ 正确 1 和 “1” 不重复

Set 正确区分 1"1",解决了方法五的类型陷阱,是生产代码的首选。


3. 复杂度对比总表

方法 时间复杂度 空间复杂度 保留顺序 类型安全 推荐度
双重循环 O(n²) O(n) ★☆☆☆☆
indexOf 遍历 O(n²) O(n) ★★☆☆☆
filter + indexOf O(n²) O(1) ★★☆☆☆
排序 + 相邻比较 O(n log n) O(n) ★★★☆☆
对象 HashMap O(n) O(n) ★★★☆☆
ES6 Set O(n) O(n) ★★★★★

4. 面试答题策略

大厂面试考这道题,不是让你背答案,而是看你能不能主动引出优化对话。建议按以下节奏作答:

第一步: 先给出暴力双重循环,说明思路直观、复杂度 O(n²)。

第二步: 主动说"能优化吗?",引出 sort + 相邻比较,降到 O(n log n),但指出顺序丢失的问题。

第三步: 提出 HashMap 思路,说明"空间换时间",降到 O(n),但指出 key 类型问题。

第四步: 最后给出 ES6 Set 作为最优解,总结它完美解决了类型安全和时间复杂度问题。

这个递进式的回答方式会让面试官看到你的算法演进思维,而不只是背题。


5. 代码健壮性要点

每道大厂手写题都暗藏对代码健壮性的考察,以下是数组去重必须注意的细节:

// 1. 入参类型校验
if (!Array.isArray(arr)) {
  console.log('type error');
  return [];
}

// 2. 严格相等替代宽松相等
arr[i] === res[j]  // ✓ 类型+值都相等
arr[i] ==  res[j]  // ✗ 1 == "1" 会是 true

// 3. 使用 obj[variable] 而非 obj.variable
// .name 是访问常量键,obj[arr[i]] 才是动态键访问
obj[arr[i]] = 1;   // ✓
obj.arr[i]  = 1;   // ✗

注释也是代码质量的体现。工程师写的代码不只给机器运行,也给团队其他人阅读。一个函数一个功能,加上清晰的参数说明,体现出良好的工程素养。


整理自学习笔记 · 2026-05-27

JavaScript 算法 大厂面试 ES6

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐