数组去重 | 前端手撕题

4 阅读3分钟

数组去重

ES6 Set (日常开发首选)

const arr = [1, 1, "2", "2", true, true, NaN, NaN];

// 1. 配合扩展运算符
const uniqueArr = [...new Set(arr)];

// 2. 配合 Array.from
const uniqueArr = Array.from(new Set(arr));

console.log(uniqueArr);

优点:语法简洁,执行效率高,能够正确是被并去重 NaN 缺点:无法去重引用类型数据(比如两个内容一模一样的对象 {a: 1} 和 {a: 1},因为它们的内存地址不同,Set 会认为它们是两个东西)。

传统做法

传统做法的主要逻辑就是:循环遍历数组,和数组当中出现这个元素的第一次位置进行比较。是:留下,不是:删除。

const arr = [1, 1, "2", "2", true, true];

const uniqueArr = arr.filter((item, index) => {
    // indexOf 永远只返回这个元素在数组中【第一次】出现的索引
    return arr.indexOf(item) === index;
});

类似的还有以下这些逻辑


//双重循环
const handleRemoveRepeat = (arr)=>{
    for(let i =0;len = arr.length;i<len;i++){
        for(let j =i+1;j<len;j++){
            if(arr[i] === arr[j]){
                arr.splice(j,1)
                j--;len--;
            }
        }
    }
    return arr;
}

// indexOf去重
const handleRemoveRepeat = (arr)=>{
    let uniqueArr = [];
    for(let i =0;len = arr.length;i<len;i++){
        if(uniqueArr.indexOf(arr[i]) === -1) uniqueArr.push(arr[i]);
    }
    return uniqueArr;
}

// includes去重
// 和上面的方法类似
const handleRemoveRepeat = (arr)=>{
    let uniqueArr = [];
    for(let i =0;len = arr.length;i<len;i++){
        if(!uniqueArr.includes(arr[i])) uniqueArr.push(arr[i]);
    }
    return uniqueArr;
}

这上面的几种方法是在一些老项目当中使用的。 优点:ES5 语法,兼容性好 缺点:

  1. 性能差,(filter 里嵌套了 indexOf,时间复杂度是 O(n2)O(n^2)
  2. 致命盲区:无法去重 NaN(因为 indexOf(NaN) 永远返回 -1,导致所有的 NaN 都会被漏掉)。

Hash Map 现代较为推荐的

如果数组内容极其庞大,上面的两种方法可能会造成卡顿,这时候建议用空间换时间

const arr = [1, 1, "1", "1", true, true, NaN, NaN];

function uniqueArrByMap(arr) {
    const map = {};
    const res = [];

    for (let i = 0; i < arr.length; i++) {
        const item = arr[i];
        // 为了防止数字 1 和字符串 '1' 发生键名冲突,我们把类型也拼接到键名里
        const key = typeof item + item;
        if (!map[key]) {
            // 没有登记过,放行,并登记造册
            res.push(item);
            map[key] = true;
        }
    }
    return res;
}

console.log(uniqueByMap(arr));
// 输出: [1, '1', true, NaN]

优点:遍历一次就可以完成任务,时间复杂度是极其优秀的 O(n)O(n)。 缺点: 稍微占用了额外的内存空间来存放 map 对象。

数组去重进阶

实际工作工程当中,业务往往较为复杂。如何根据对象的某个特定字段(比如 id)来进行“对象数组去重”

我们建一个专门的“花名册”(Map)。让数组里的对象排队挨个走过来,我们只看它的 id。如果花名册上没有这个 id,就把它放进结果队伍里,并在花名册上记下这个 id;如果查到 id 已经记过了,直接踢掉。

const users = [
    { id: 1, name: "Alice", age: 20 },
    { id: 2, name: "Bob", age: 25 },
    { id: 1, name: "Alice_v2", age: 21 }, // id 重复了,会被踢掉
    { id: 3, name: "Charlie", age: 22 },
];

function uniqueById(arr) {
    const res = new Map();
    // filter 会遍历数组,并且只保留返回 true 的元素
    return arr.filter((item) => {
        // 如果 Map 中没有这个 id
        if (!res.has(item.id)) {
            // 把 id 存入 Map 当作标记,值随便设一个(比如 1)
            res.set(item.id, 1);
            return true; // 放行
        }
        return false; // 拦截
    });
}

console.log(uniqueById(users));
// 输出只包含 id: 1, 2, 3 的三个对象,id 为 1 的第二个对象被成功过滤