数组去重:从零开始,写一个靠谱的工具函数

一道经典面试题,七种实现思路,带你理解时间复杂度的演进

为什么需要数组去重?

日常开发中,我们经常遇到这样的场景:

  • 前端处理后端返回的数据,需要去掉重复的选项
  • 用户操作产生的一系列 ID,需要过滤重复项
  • 数据统计前需要清洗重复值

数组去重虽然基础,但不同的实现方式体现了对 JavaScript 语言理解的深度。

代码是写给人看的,只是顺便能在机器上运行

实际开发中,一段代码往往会被多个人维护。过了一周、一个月,你或者同事可能早就忘了当初为什么这么写。

好注释的价值:

  • 有利于协作 ------ 开发者和使用者不是一个人
  • 防止遗忘 ------ 注释是代码的一部分
  • 提高可读性 ------ 看注释就能理解意图

代码层面的原则:

  • 一个函数只做一个功能
  • 复杂逻辑要封装
  • 函数要有健壮性,主动检验参数

每次第一步:参数校验

JavaScript 是动态弱类型语言,调用者可能传进来任何东西:数组、字符串、数字、甚至是 null

参数校验是最容易被忽略的"基础功":

javascript

javascript 复制代码
// 错误示范:没有校验,直接使用
function unique(arr) {
    return [...new Set(arr)];  // 如果 arr 不是数组,直接报错
}

基础版本:双重循环

最直观的思路:维护一个新数组,遍历原数组,判断每个元素是否已经存在于新数组中。

javascript

ini 复制代码
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;
}

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

时间复杂度 :O(n²),两层循环嵌套。
缺点:数据量大时性能较差。

改进一:利用 indexOf

indexOf 替代内层循环,代码更简洁,但时间复杂度仍是 O(n²)。

javascript

ini 复制代码
function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error');
        return [];
    }

    const res = [];
    for (let i = 0; i < arr.length; i++) {
        if (res.indexOf(arr[i]) === -1) {
            res.push(arr[i]);
        }
    }
    return res;
}

改进二:filter + indexOf

利用数组的 filter 方法,代码更函数式。核心逻辑:当前元素第一次出现的位置等于当前位置才保留。

javascript

javascript 复制代码
function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error');
        return [];
    }

    return arr.filter((item, index) => {
        return arr.indexOf(item) === index;
    });
}

注意indexOf 会返回元素第一次出现的位置,如果当前索引不是第一次出现,说明是重复项,被过滤掉。

改进三:排序后去重

先排序,然后比较相邻元素是否相同。

javascript

ini 复制代码
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;
}

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

时间复杂度 :O(n log n),主要来自排序。
注意:排序会改变元素顺序,如果不需要保持原顺序,这是不错的选择。

改进四:空间换时间 ------ 对象字面量

利用对象属性的唯一性,将元素作为对象的 key,实现 O(n) 时间复杂度。

javascript

ini 复制代码
function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error');
        return [];
    }

    const res = [];
    const obj = {};
    
    for (let i = 0; i < arr.length; i++) {
        if (!obj[arr[i]]) {
            res.push(arr[i]);
            obj[arr[i]] = 1;
        } else {
            obj[arr[i]]++;
        }
    }
    return res;
}

时间复杂度 :O(n)
空间复杂度:O(n),用额外的对象存储已出现的元素。

这种方式是典型的"用空间换时间"策略。

改进五:Set ------ 最优雅的解法

ES6 新增的 Set 数据结构,天然保证元素不重复。一行代码解决问题。

javascript

javascript 复制代码
function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error');
        return [];
    }

    return [...new Set(arr)];
}

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

原理 :Set 类似于数组,但成员的值都是唯一的。展开运算符 ... 将 Set 转回数组。
时间复杂度 :O(n)
适用性:现代浏览器和 Node.js 环境均可使用。

方法对比

方法 时间复杂度 空间复杂度 是否保持顺序 代码复杂度
双重循环 O(n²) O(n)
indexOf O(n²) O(n)
filter+indexOf O(n²) O(n)
排序去重 O(n log n) O(n)
对象标记 O(n) O(n)
Set O(n) O(n) 最低

开发建议

  1. 日常开发 :首选 Set 方式,简洁且性能好
  2. 需要兼容旧浏览器:用对象标记法
  3. 不在意顺序:排序去重也是好选择
  4. 务必做参数校验:判断是否为数组,增强代码健壮性

javascript

javascript 复制代码
// 健壮性示例
if (!Array.isArray(arr)) {
    throw new TypeError('Expected an array');
}

总结

从双重循环到 Set,体现了编程思想的演进:

  • 学会用语言提供的 API(indexOf、filter)
  • 理解不同数据结构的特性(对象、Set)
  • 懂得权衡时间与空间
  • 关注代码的可读性与健壮性

一道简单的面试题,其实考察了很多底层能力。你学会了吗?

相关推荐
hans汉斯11 分钟前
基于改进交叉熵损失函数与Transformer的心电信号高风险分类研究
功能测试·深度学习·算法·yolo·目标检测·分类·transformer
Jerry13 分钟前
LeetCode 349. 两个数组的交集
算法
YuK.W1 小时前
Leetcode100: 70.爬楼梯、118.杨辉三角、198.打家劫舍
java·算法·leetcode
随意起个昵称1 小时前
状压dp-基础题目2([USACO12MAR] Cows in a Skyscraper G)
c++·算法·动态规划
Jerry2 小时前
LeetCode 1002. 查找共用字符
算法
无限的鲜花3 小时前
协程本质是函数加状态机——零基础深入浅出 C++20 协程
c++·算法·c++20
众人皆醒我独醉3 小时前
Pod 一直 Terminating,死活删不掉?三个凶手,每一个你都意想不到
面试·kubernetes
旖-旎3 小时前
《LeetCode 64 最小路径和 || LeetCode 174 地下城游戏》
c++·算法·leetcode·动态规划
森林古猿13 小时前
再论斜率优化
c++·学习·算法
万法若空4 小时前
【数学-简单数论】同余中的逆
线性代数·算法