JavaScript 数组去重的 6 种实现方式:从 O(n²) 到 O(n) 的进化之路

JavaScript 数组去重的 6 种实现方式:从 O(n²) 到 O(n) 的进化之路

数组去重是前端面试中的高频题目。本文通过 6 种不同的实现方式,带你从暴力双重循环一路进化到 ES6 的 Set 一行代码,同时深入理解时间复杂度与空间复杂度的权衡。

前言

今天在课程中,老师带我们用 6 种不同的方式 解决了同一道题------数组去重。从最基础的双重循环,到利用数组 API,再到 ES6 的 Set,每种方法都有其独特的思路和适用场景。

更重要的是,通过这道题,我真正理解了时间复杂度空间复杂度的概念,以及"空间换时间"的算法思想。


一、编码规范:写好函数的第一步

在开始之前,先聊聊代码规范。老师在课上反复强调:

1.1 注释是代码的一部分

javascript 复制代码
/**
 * @func 数组去重
 * @param {Array} arr 数组
 * @return {Array} 去重后的数组
 * @author hzs
 * @date 2026-05-25
 */
function unique(arr) {
    // ...
}

代码的开发者和使用者可能不是同一个人,你可能忘记当时为什么这么写。注释会提高代码的可读性,是代码的一部分。

1.2 函数设计三原则

原则 说明
一个函数一个功能 单一职责,便于维护和测试
封装复杂功能 调用者不需要了解内部实现
健壮性------校验参数 对输入进行类型检查,避免异常

1.3 参数校验模板

javascript 复制代码
function unique(arr) {
    // Array.isArray() 是数组的静态方法,无需实例化即可调用
    if (!Array.isArray(arr)) {
        console.log('type error');
        return [];
    }
    // ...具体逻辑
}

💡 以下 6 种实现都会包含这个参数校验,后续代码中不再重复说明。


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

思路

维护一个结果数组 res,遍历原数组,对每个元素检查是否已存在于 res 中。

javascript 复制代码
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++) {
            // === 恒等:值相等且类型相等
            // 1 === '1' → false(弱类型语言中的严格比较)
            if (arr[i] === res[j]) {
                flag = false;
                break;
            }
        }
        if (flag) {
            res.push(arr[i]);
        }
    }
    return res;
}

console.log(unique([1, 2, 3, 4, 5, 5, 6]));
// [1, 2, 3, 4, 5, 6]

复杂度分析

scss 复制代码
时间复杂度:O(n²)
├── 外层循环 n 次
└── 内层循环最多 n 次
    总计:n × n = n²

空间复杂度:O(n)
└── 结果数组 res 最多存储 n 个元素

📌 优点 :思路最直观,适合初学者理解。缺点:性能差,数据量大时明显卡顿。


三、方法二:indexOf 优化

思路

利用 Array.prototype.indexOf() 方法替代内层循环,判断元素是否已存在于结果数组中。

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

关键 API

javascript 复制代码
arr.indexOf(item)
// 返回 item 在 arr 中第一次出现的索引
// 找不到返回 -1

[1, 2, 3, 2].indexOf(2)   // 1
[1, 2, 3].indexOf(4)      // -1

复杂度分析

scss 复制代码
时间复杂度:O(n²)
├── 外层循环 n 次
└── indexOf 内部也是一次遍历 O(n)
    总计:仍然是 n²

空间复杂度:O(n)

📌 本质上和方法一相同 ,只是用 indexOf 替代了手写内层循环,代码更简洁,但时间复杂度没有改善。


四、方法三:filter + indexOf

思路

利用 Array.prototype.filter() 方法,配合 indexOf 进行过滤。

javascript 复制代码
function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error');
        return [];
    }
    return arr.filter(function(item, index) {
        // 只保留第一次出现的元素
        // indexOf 返回第一个索引,如果等于当前 index,说明是第一次出现
        return index === arr.indexOf(item);
    });
}

console.log(unique([1, 2, 3, 4, 5, 5, 6]));
// [1, 2, 3, 4, 5, 6]

关键 API

javascript 复制代码
arr.filter(function(item, index) {
    // 返回 true → 保留该元素
    // 返回 false → 过滤掉该元素
    return true | false;
});

工作原理

scss 复制代码
原数组:[1, 2, 3, 4, 5, 5, 6]
索引:    0  1  2  3  4  5  6

filter 遍历过程:
┌──────┬───────┬────────────────┬────────┐
│ item │ index │ indexOf(item)  │ 保留?  │
├──────┼───────┼────────────────┼────────┤
│  1   │   0   │       0        │  ✅    │
│  2   │   1   │       1        │  ✅    │
│  3   │   2   │       2        │  ✅    │
│  4   │   3   │       3        │  ✅    │
│  5   │   4   │       4        │  ✅    │
│  5   │   5   │       4        │  ❌    │  ← 第二个 5 被过滤
│  6   │   6   │       6        │  ✅    │
└──────┴───────┴────────────────┴────────┘

复杂度分析

scss 复制代码
时间复杂度:O(n²)
├── filter 遍历 n 次
└── 每次 indexOf 遍历 O(n)
    总计:仍然是 n²

空间复杂度:O(n)

📌 函数式编程风格,代码最简洁优雅,但性能上仍然是 O(n²)。


五、方法四:排序后相邻比较

思路

先对数组排序,然后只需比较相邻元素是否相同。

javascript 复制代码
function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error');
        return [];
    }
    // O(n²) → O(nlogn)
    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;
}

为什么更快?

css 复制代码
排序前:[3, 1, 4, 1, 5, 9, 2, 6, 5]
排序后:[1, 1, 2, 3, 4, 5, 5, 6, 9]
              ↑         ↑
           相邻比较即可,无需两两比较

复杂度分析

scss 复制代码
时间复杂度:O(nlogn)
├── sort() 排序:O(nlogn)
└── 遍历比较:O(n)
    总计:O(nlogn) + O(n) = O(nlogn)  ← 显著提升!

空间复杂度:O(n)

📌 性能提升明显 ,从 O(n²) 降到 O(nlogn)。但注意:sort() 默认按字符串排序,对数字数组需要传入比较函数 arr.sort((a, b) => a - b)


六、方法五:对象字面量 / HashMap(空间换时间)

思路

利用 JavaScript 对象字面量作为 HashMap,以数组元素为 key,实现 O(1) 的查找。

javascript 复制代码
function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error');
        return [];
    }
    let res = [],
        obj = {};  // 对象字面量充当 HashMap
    for (let i = 0; i < arr.length; i++) {
        // obj[variable] --- 变量作为 key(动态属性访问)
        // obj.name --- 常量作为 key(点号访问)
        if (!obj[arr[i]]) {
            res.push(arr[i]);
            obj[arr[i]] = 1;  // 标记为已存在
        } else {
            obj[arr[i]]++;    // 记录出现次数
        }
    }
    return res;
}

核心原理

css 复制代码
对象字面量充当 HashMap

遍历 [1, 2, 3, 2, 4, 3]

Step 1: obj = {},  res = [1]     obj[1] = 1
Step 2: obj = {1:1}, res = [1,2] obj[2] = 1
Step 3: obj = {1:1,2:1}, res = [1,2,3] obj[3] = 1
Step 4: obj[2] 已存在!跳过
Step 5: obj = {1:1,2:1,3:1}, res = [1,2,3,4] obj[4] = 1
Step 6: obj[3] 已存在!跳过

结果:[1, 2, 3, 4]

复杂度分析

scss 复制代码
时间复杂度:O(n)
├── 只需遍历一次数组
└── 对象属性查找是 O(1)
    总计:O(n) × O(1) = O(n)  ← 最优!

空间复杂度:O(n)
├── 结果数组 O(n)
└── HashMap 对象 O(n)
    总计:O(n)  ← 用空间换时间

📌 经典的空间换时间策略 。JavaScript 早期没有 HashMap,对象字面量就是最好的替代方案。注意:如果数组元素是对象,需要用 JSON.stringify() 转换为字符串作为 key。


七、方法六:ES6 Set(终极方案)

思路

利用 ES6 新增的 Set 数据结构------天生不重复的集合

javascript 复制代码
function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error');
        return [];
    }
    return [...new Set(arr)];  // Set 转换为数组
}

一行代码搞定

javascript 复制代码
const unique = arr => [...new Set(arr)];

Set 是什么?

scss 复制代码
Set 的特性
├── 不重复的数据容器
├── 内部使用 HashMap 实现
├── 查找/插入的时间复杂度 O(1)
└── ES6 新增的数据结构

复杂度分析

scss 复制代码
时间复杂度:O(n)
├── new Set(arr):遍历数组构建 Set,O(n)
└── ...展开运算符:遍历 Set 转数组,O(n)
    总计:O(n)

空间复杂度:O(n)
└── Set 容器存储 n 个元素

🎯 生产环境推荐方案:代码最简洁、性能最优、语义最清晰。


八、六种方法全面对比

8.1 复杂度对比

方法 时间复杂度 空间复杂度 核心思路
① 双重循环 O(n²) O(n) 暴力枚举
② indexOf O(n²) O(n) API 替代内层循环
③ filter+indexOf O(n²) O(n) 函数式风格
④ 排序+相邻比较 O(nlogn) O(n) 先排序降低比较次数
⑤ 对象字面量/HashMap O(n) O(n) 空间换时间
⑥ ES6 Set O(n) O(n) 利用 Set 天生去重

8.2 复杂度直观感受

scss 复制代码
执行时间对比(假设 n = 10000)

O(n²)     :100,000,000 次操作  😱
O(nlogn)  :    132,877 次操作  😊
O(n)      :     10,000 次操作  🚀

差距巨大!算法选择直接影响程序性能

8.3 适用场景

场景 推荐方法 原因
生产环境 ⑥ Set 简洁、高效、现代
面试手写 ⑤ HashMap 展示算法思维
学习理解 ①②③④ 理解基本原理
大数据量 ④⑤⑥ 避免 O(n²)
兼容旧浏览器 ④⑤ 不依赖 ES6

九、涉及的核心数组 API

速查表

API 类型 作用 示例
Array.isArray() 静态方法 判断是否是数组 Array.isArray([1])true
arr.indexOf(item) 实例方法 返回首次出现的索引 [1,2,3].indexOf(2)1
arr.filter(fn) 实例方法 过滤数组,返回新数组 arr.filter(x => x > 0)
arr.sort() 实例方法 排序(原地修改) arr.sort((a,b) => a-b)

十、知识图谱

scss 复制代码
📚 数组去重知识图谱

编码规范
├── JSDoc 注释规范
├── 一个函数一个功能
├── 参数校验(健壮性)
└── === 严格相等

六种实现方式
├── O(n²) 暴力法
│   ├── 双重循环
│   ├── indexOf
│   └── filter + indexOf
│
├── O(nlogn) 排序法
│   └── sort + 相邻比较
│
└── O(n) 哈希法
    ├── 对象字面量 / HashMap
    └── ES6 Set

核心概念
├── 时间复杂度(执行效率)
├── 空间复杂度(内存占用)
├── 空间换时间(算法权衡)
└── HashMap 原理(O(1) 查找)

结语

一道简单的数组去重题,从 O(n²) 到 O(n),从暴力循环到 Set 一行代码,背后是算法思维的进化

面试中,面试官考的不仅是你能不能写出答案,更是你能否分析不同方案的时间复杂度和空间复杂度 ,能否根据实际场景选择最优方案

记住这六个方法,理解背后的原理,你就能在面试中游刃有余。

希望这篇文章对你有帮助!如果有任何问题,欢迎在评论区交流。


📌 参考资源


📌 文章标签 JavaScript 算法 数组去重 前端面试 时间复杂度 学习笔记


如果这篇文章对你有帮助,别忘了点赞、收藏、关注三连支持一下~你的鼓励是我持续输出的动力! 💪

相关推荐
晴天彩虹雨17 小时前
大厂 Flink 面试 100 题
大数据·面试·flink
幸运小圣17 小时前
SheetJS(xlsx)导出 Excel 全流程(新手版)【SheetJS】
javascript·excel
Moment17 小时前
面试官:上下文过长导致语义偏移,工程上怎么优化
前端·后端·面试
怕浪猫17 小时前
# Electron 开发实战(三):基础UI开发与布局全解
前端·javascript·electron
大可-17 小时前
CSDN博客-星火知识库教程
前端·javascript·vue.js·elementui·html
fhqlongteng17 小时前
RK3576上electron调用GPU的功能设置方法
前端·javascript·electron·gpu·rk3576
冰暮流星18 小时前
javascript之window对象方法
开发语言·javascript·ecmascript
川冰ICE18 小时前
JavaScript入门⑩|BOM与浏览器对象,localStorage_位置_历史记录
开发语言·javascript·ecmascript