javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JavaScript数组去重实现方式

JavaScript数组去重的6种实现方式(从 O(n²) 到 O(n))

作者:Darling噜啦啦

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

前言

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

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

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

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

1.1 注释是代码的一部分

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

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

1.2 函数设计三原则

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

1.3 参数校验模板

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

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

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

思路

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

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]

复杂度分析

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

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

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

三、方法二:indexOf 优化

思路

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

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

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

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

复杂度分析

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

空间复杂度:O(n)

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

四、方法三:filter + indexOf

思路

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

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

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

工作原理

原数组:[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        │  ✅    │
└──────┴───────┴────────────────┴────────┘

复杂度分析

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

空间复杂度:O(n)

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

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

思路

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

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;
}

为什么更快?

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

复杂度分析

时间复杂度: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) 的查找。

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;
}

核心原理

对象字面量充当 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]

复杂度分析

时间复杂度: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 数据结构——天生不重复的集合

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

一行代码搞定

const unique = arr => [...new Set(arr)];

Set 是什么?

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

复杂度分析

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

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

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

八、六种方法全面对比

8.1 复杂度对比

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

8.2 复杂度直观感受

执行时间对比(假设 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)

十、知识图谱

📚 数组去重知识图谱

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

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

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

结语

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

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

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

以上就是JavaScript数组去重的6种实现方式(从 O(n²) 到 O(n))的详细内容,更多关于JavaScript数组去重实现方式的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文