著名面试题:数组去重

158 阅读4分钟

我正在参加「掘金·启航计划」

著名面试题:
如何实现数组去重?
假设有数组 array = [1,5,2,3,4,2,3,1,3,4]
你要写一个函数 unique,使得
unique(array) 的值为 [1,5,2,3,4]
也就是把重复的值都去掉,只保留不重复的值。

要求写出两个答案:

  1. 一个答案不使用 Set、Map 等数据结构实现(6分)
  2. 另一个答案使用数据结构(4分)
  3. 说出每个方案缺点的,再额外每个方案加 2 分。

方案一:不使用 JS 提供的数据结构

方法1:Array.prototype.includes

优点:'1' 和 1 、undefined 和 'undefined' 等等可以区分。

缺点:对于{ value: 'value', key: 'key' }, { value: 'value', key: 'key' }无法去重。

let array = [1, 5, 2, 3, 4, 2, 3, 1, 3, 4]
let unique = function (arr) {
    let target = []
    arr.forEach((item, i) => {
        if (target.includes(item) === false) {
            target.push(item)
        }
    })
    return target
}

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

方法2:对象键值对(改良版)

利用了对象的 key 不可以重复的特性来进行去重。

优点:

  • 能区分:基本数据类型与其对应的字符串值,比如 '1' 和 1、NaN 和 'NaN'。
  • 对于{ value: 'value', key: 'key' }, { value: 'value', key: 'key' }可以去重。

缺点:

  • 对于{ value: 'value', key: 'key' }, { key: 'key', value: 'value' }会认为是两个对象,并没有去重。
let array = [{}, {}, { value: 'value', key: 'key' }, { key: 'key', value: 'value' }, { key: 'key', value: 'value' }, 1, 1, '1', '1', 'true', 'true', true, true, 16, 16, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', 0, 69.3];
function unique(arr) {
    let target = [];
    let obj = {};
    arr.forEach((item, i) => {
        let key = typeof item + JSON.stringify(item)
        if (!obj[key]) {
            target.push(item)
            obj[key] = 1
        } else {
            obj[key]++
        }
    })
    return target;
}
console.log(unique(array));

方案二:使用 JS 提供的数据结构

方法1:使用 Set

let array = [1, 5, 2, 3, 4, 2, 3, 1, 3, 4]
let unique = function (arr) {
    return Array.from(new Set(arr))
    // 或者 return [...new Set(arr)] 
}

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

方法2:使用 WeakMap

let obj1 = { key: 'key', value: 'value' }
let obj2 = { key: 'key', value: 'value' }
let array = [obj1, obj1, obj2, obj1, obj1]
function unique(array) {
    let wm = new WeakMap()
    let target = []
    array.forEach((item, i) => {
        if (!wm.has(item)) {
            wm.set(item, i)
            target.push(item)
        }
    })

    return target
}

// [{ key: 'key', value: 'value' }, { key: 'key', value: 'value' }]
console.log(unique(array)); 

方法3:使用 Map

let obj1 = { key: 'key', value: 'value' }
let obj2 = { key: 'key', value: 'value' }
let array = [obj1, obj1, obj2, NaN, 1, 5, 2, 3, 4, 2, 3, 1, 3, 4, NaN]
let unique = function (arr) {
    let map = new Map();
    let target = [];
    arr.forEach((item, i) => {
        if (!map.has(item)) {
            target.push(item);
            map.set(item, true);
        }
    })
    return target;
}

console.log(unique(array)); // [obj1,obj2,NaN,1,5,2,3,4]

简化版:

let obj1 = { key: 'key', value: 'value' }
let obj2 = { key: 'key', value: 'value' }
let array = [obj1, obj1, obj2, NaN, 'NaN', 1, 5, 2, 3, 4, 2, 3, 1, 3, 4, NaN]
let unique = function (arr) {
    let map = new Map();
    return arr.filter((item) => !map.has(item) && map.set(item, 1))
}

console.log(unique(array)); // [obj1,obj2,NaN,'NaN',1,5,2,3,4]

以上三种方法都存在这样的问题:如果数组的两个元素都是某个相同对象的引用,那么数组的元素是可以去重的。但是如果这两个元素是不同对象,并且这两个对象的键值完全一样,则无法去重。

// 可以去重
let obj1 = { key: 'key', value: 'value' }
let obj2 = { key: 'key', value: 'value' }
let obj3 = { value: "value", key: "key" }
let array = [obj1, obj1, obj2, obj3]

// 无法去重
let array = [
{ key: "key", value: "value" },
{ key: "key", value: "value" },
{ value: "value", key: "key" }]

怎么界定数组中的对象元素是否重复了呢?分情况讨论。

  • 两元素为同一对象的引用,判定两元素重复。
  • 两元素是不同对象。
    • 对象的所有键的值都相同,则判定两元素重复。
    • 对象的某几个键的值相同,则判定两元素重复。
    • 对象的某个键的值相同,则判定两元素重复。

方案三:支持对象去重

既支持基本数据类型的元素去重,又支持根据指定 key 或多个 key 进行对象类型的元素去重。

缺点:不支持混合类型元素的数组去重。

function unique(arr, keyOrFn) {
    if (keyOrFn === undefined) return [...new Set(arr)]
    const MAP = {
        'string': e => e[keyOrFn],
        'function': e => keyOrFn(e),
    }
    let fn = MAP[typeof keyOrFn]
    let obj = arr.reduce((o, cur) => {
        o[fn(cur)] = cur
        return o
    }, {})
    return Object.values(obj)
}

const list = [
    { id: 0, name: '小明', age: 13 },
    { id: 1, name: '小明', age: 99 },
    { id: 2, name: '小明', age: 23 },
    { id: 1, name: '小红', age: 42 },
    { id: 0, name: '小明', age: 13 },
    { id: 1, name: '小芳', age: 35 },
    { id: 0, name: '小明', age: 13 },
    { id: 1, name: '', age: 23 },
    { id: 1, name: 'lm', age: 22 },
    { id: 9, name: 'xh', age: 79 }
]

// console.log(unique(list, 'id')); // id 键的值相同,则判定重复
console.log(unique(list, e => e.id + e.name + e.age)); // 所有键的值相同,判定重复

终极方案:混合类型元素去重

这是方案三的改良版,支持混合有「基本数据类型元素」和「对象类型元素」的数组进行去重。

function unique(arr, keyOrFn) {
    if (keyOrFn === undefined) return [...new Set(arr)]
    const MAP = {
        'string': e => e[keyOrFn],
        'function': e => keyOrFn(e),
    }
    let fn = MAP[typeof keyOrFn]
    let map = new Map();
    let target_primitive = [];
    let obj = arr.reduce((o, cur) => {
        if (cur instanceof Object === false) {
            // 基本数据类型单独处理
            if (!map.has(cur)) {
                target_primitive.push(cur);
                map.set(cur, true);
            }
        } else {
            o[fn(cur)] = cur
        }
        return o
    }, {})
    return Object.values(obj).concat(target_primitive)
}

const list = [
    { id: 0, name: '小明', age: 13 },
    { id: 1, name: '小明', age: 99 },
    { id: 2, name: '小明', age: 23 },
    { id: 1, name: '小红', age: 42 },
    { id: 0, name: '小明', age: 13 },
    { id: 1, name: '小芳', age: 35 },
    { id: 0, name: '小明', age: 13 },
    { id: 1, name: '', age: 23 },
    { id: 1, name: 'lm', age: 22 },
    { id: 9, name: 'xh', age: 79 },
    1, 2, 4, 1, '1', '1', 1, 3, 1
]

console.log(unique(list, 'id'));
// console.log(unique(list, e => e.name + e.age));

参考资料