JavaScript 数组去重:从 O(n²) 到一行代码的六种写法

8 阅读5分钟

这是一篇总结文章,涵盖你给出的六种数组去重方法,从原理、复杂度到适用场景逐一分析。


JavaScript 数组去重:从 O(n²) 到一行代码的六种写法

数组去重是前端面试和日常业务中极为高频的需求。看似简单的问题,却能考察对数据结构、算法复杂度和语言特性的理解深度。本文梳理六种去重方法,按时间复杂度的演进顺序展开。


1. 双重循环(暴力法)

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error');
        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;
}

原理: 外层遍历原数组,内层遍历已收集的结果数组 res,逐一比对当前元素是否已存在。

复杂度: 时间复杂度 O(n²),空间复杂度 O(n)。

最直观的「人肉」思路,不需要任何 API 知识,但双重循环在数据量大时性能堪忧。优点是兼容性最好,远古浏览器也能跑。


2. indexOf 法

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.error('输入必须是数组');
        return [];
    }
    let res = [];
    for (let i = 0; i < arr.length; i++) {
        if (res.indexOf(arr[i]) === -1) {
            res.push(arr[i]);
        }
    }
    return res;
}

原理:indexOf 替代内层循环,检查 res 中是否已包含当前元素。

复杂度: 时间复杂度仍是 O(n²)——indexOf 内部本质上是遍历查找;空间复杂度 O(n)。

代码比双重循环简洁不少,但性能上没有本质提升。indexOf 是 ES5 方法,IE9+ 支持。


3. filter + indexOf 法

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error');
        return [];
    }
    return arr.filter(function(item, index) {
        return arr.indexOf(item) === index;
    });
}

原理: filter 遍历数组,对每个元素取其第一次出现的位置 arr.indexOf(item),只有该位置与当前索引相等时才保留。巧妙之处:重复元素第一次出现时被保留,后续出现时 indexOf(item) !== index 被过滤掉。

复杂度: 时间复杂度 O(n²)(indexOf 在每次回调中都要遍历),空间复杂度 O(n)。

一行链式调用,最「函数式」的写法,代码极其简洁。但性能并不比前两种好,而且不适合对超大数组使用。


4. 排序 + 相邻比较法

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error');
        return [];
    }
    arr = arr.sort();
    let res = [arr[0]];
    for (let i = 1; i < arr.length; i++) {
        if (arr[i] !== arr[i - 1]) {
            res.push(arr[i]);
        }
    }
    return res;
}

注意: 原代码中 if(arr[i] !== [i-1]) 有笔误,[i-1] 是一个数组字面量而非 arr[i-1],上面已修正。

原理: 先排序让相同元素相邻,再一次遍历,相邻元素不相等时才保留。

复杂度: 时间复杂度 O(n log n)(排序的代价),空间复杂度 O(n)。

在去重问题中,O(n log n) 是一个重要的分水岭——比 O(n²) 快了一个数量级。代价是 sort() 改变了原数组,且默认排序按字符串字典序,对数字数组可能出现 [1, 10, 2, 20] 的意外结果。如需数值排序应传入比较函数 arr.sort((a, b) => a - b)


5. 对象字面量 / HashMap 法(空间换时间)

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error');
        return [];
    }
    let res = [],
        obj = {};
    for (let i = 0; i < arr.length; i++) {
        if (!obj[arr[i]]) {
            res.push(arr[i]);
            obj[arr[i]] = 1;
        }
    }
    return res;
}

原理: 用对象的 key 做哈希映射。遍历数组时,以当前元素为 key 去对象中查找——对象 key 查找是 O(1),总复杂度降到 O(n)。

复杂度: 时间复杂度 O(n),空间复杂度 O(n + m)(m 为不重复元素数)。

用额外的内存换取了线性的时间。有两个注意事项:

  • 对象 key 只能是字符串或 Symbol,obj[123] 和 obj['123'] 会被视为同一个 key,无法区分数字 123 和字符串 '123'
  • 同样,obj[{a:1}] 会变成 obj['[object Object]'],无法处理对象元素。

改进方案:用 ES6 的 Map 代替普通对象,Map 的 key 可以是任意类型,能精确区分。


6. Set 法(ES6,终极方案)

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error');
        return [];
    }
    return [...new Set(arr)];
}

原理: Set 是 ES6 新增的数据结构,其核心特性就是值唯一——重复值自动被忽略。内部实现基于类似 HashMap 的机制,插入和查找均为 O(1)。展开运算符 ... 将 Set 还原为数组。

复杂度: 时间复杂度 O(n),空间复杂度 O(n)。

一行代码解决战斗,简洁且性能优秀。唯一的「缺点」是 IE 不支持(但这已经不是缺点了)。对于绝大多数现代项目,这是首选的去重方案。


总结对比

方法时间复杂度空间复杂度代码量关键 API
双重循环O(n²)O(n)
indexOfO(n²)O(n)indexOf
filter + indexOfO(n²)O(n)filter + indexOf
排序 + 相邻O(n log n)O(n)sort
HashMapO(n)O(n)对象字面量
SetO(n)O(n)极少Set + 展开运算符

六种方法,本质上是三条学习曲线:

  1. 从「能写出来」到「写得简洁」——用 API 替代手动循环(1 → 2 → 3)
  2. 从「写得简洁」到「写得快」——引入算法思维,用排序降低复杂度(4)
  3. 从「写得快」到「用对工具」——理解数据结构的力量,用正确工具一行解决(5 → 6)

在日常开发中,直接使用 [...new Set(arr)] 即可。但面试场景往往要求手写兼容实现或考察对复杂度分析的意识,这时掌握 HashMap 法(方法 5)是最佳选择——既展示了对数据结构的理解,又给出了 O(n) 的优雅解。