数组去重

10 小时前
/ ,
2

数组去重

数组去重(Unique / Deduplication)指从数组中移除“重复元素”。关键点不在于“怎么删”,而在于:

  • 你如何定义“重复”?(值相等 / 引用相同 / 某些字段相同 / 自定义比较)
  • 你是否需要保留原始顺序?(一般去重保留第一次出现的元素)

下面按常见场景整理:基础类型、对象按字段、联合字段、自定义比较,并补充复杂度与坑点。

1) 基础类型去重:Set

适用:number/string/boolean/null/undefined/symbol/bigint 等“可直接比较的值”。

复杂度

  • 时间:O(n)O(n)
  • 空间:O(n)O(n)

关键语义(很容易考)

JavaScript 的 Set 使用 SameValueZero 语义:

  • NaNNaN 视为相等(能正确去掉多个 NaN
  • +0-0 视为相等
  • 对象/数组/函数等“引用类型”,只有在引用相同(同一个对象)时才算重复
export function uniquePrimitive<T>(arr: T[]): T[] {
    return [...new Set(arr)];
}

uniquePrimitive([1, 1, 2, NaN, NaN]); // [1, 2, NaN]
uniquePrimitive([{ a: 1 }, { a: 1 }]); // 不会去重(引用不同)

2) 对象数组按单字段去重:Map/Set + key

适用:数组元素为对象,按某个字段(如 id)去重。

  • 时间:O(n)O(n)
  • 空间:O(n)O(n)
/**
 * 对象数组按字段去重(保留第一次出现的项)
 */
export function uniqueByKey<T extends Record<string, any>>(
    arr: T[],
    key: keyof T
): T[] {
    const seen = new Set<any>();
    const result: T[] = [];
    for (const item of arr) {
        const value = item[key];
        if (!seen.has(value)) {
            seen.add(value);
            result.push(item);
        }
    }
    return result;
}

const users = [
    { id: 1, name: '张三', age: 25 },
    { id: 2, name: '李四', age: 30 },
    { id: 1, name: '张三(重复)', age: 26 }, // id 重复
    { id: 3, name: '王五', age: 28 }
];

uniqueByKey(users, 'id');
// => [{id:1,...},{id:2,...},{id:3,...}]

3) 联合字段去重:推荐 keyFn(更通用)

适用:需要用多个字段一起判定重复,比如 userId + productId

为什么不建议直接 join('|')

keys.map(k => item[k]).join('|') 可能产生“拼接碰撞”,例如:

  • ['a|b', 'c']['a', 'b|c'] 拼起来都可能是 a|b|c

更稳妥的方式:

  • keyFn 直接返回唯一 key(你自己保证唯一)
  • 或对 key 数组做结构化序列化(如 JSON.stringify([v1, v2])
/**
 * 通用去重:用 keyFn 决定“重复”的判定 key
 */
export function uniqueBy<T, K>(arr: T[], keyFn: (item: T) => K): T[] {
    const seen = new Set<any>();
    const result: T[] = [];

    for (const item of arr) {
        const key = keyFn(item);
        if (!seen.has(key)) {
            seen.add(key);
            result.push(item);
        }
    }
    return result;
}

type Order = {
    userId: number;
    productId: string;
    date: string;
    amount: number;
};

const orders: Order[] = [
    { userId: 100, productId: 'P001', date: '2024-01-01', amount: 199 },
    { userId: 100, productId: 'P002', date: '2024-01-01', amount: 299 },
    { userId: 100, productId: 'P001', date: '2024-01-02', amount: 199 }, // 联合字段重复,但 date 不同
    { userId: 100, productId: 'P001', date: '2024-01-01', amount: 189 },
    { userId: 101, productId: 'P001', date: '2024-01-01', amount: 199 }
];

// 按 userId + productId 联合去重
const uniqueOrders = uniqueBy(orders, (o) => JSON.stringify([o.userId, o.productId]));

4) 自定义比较去重:findIndex + compareFn(最灵活但最慢)

适用:复杂判等(例如深比较、近似相等、忽略大小写、忽略空格等)。

注意:这种方式通常是 O(n2)O(n^2),数据量大时不建议。

export function uniqueByCompare<T>(
    arr: T[],
    compareFn: (a: T, b: T) => boolean
): T[] {
    return arr.filter((item, index, self) => {
        return index === self.findIndex((t) => compareFn(t, item));
    });
}

// 示例:忽略字符串大小写去重
uniqueByCompare(['A', 'a', 'B'], (a, b) => String(a).toLowerCase() === String(b).toLowerCase());
// => ['A','B']

常见坑点总结

  • 引用类型不会被 Set 自动去重[{a:1},{a:1}] 视为不同。
  • NaN 可以被 Set 去重:这是 SameValueZero 语义带来的好处。
  • 联合字段拼接可能碰撞:尽量用 keyFn 或结构化序列化。
  • 是否保序:上述实现都保留“第一次出现的元素”,顺序稳定。
  • 大数据量优先 O(n)O(n) 解法compareFnO(n2)O(n^2) 只适合小数组。

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...