我正在参加「掘金·启航计划」
著名面试题:
如何实现数组去重?
假设有数组 array = [1,5,2,3,4,2,3,1,3,4]
你要写一个函数 unique,使得
unique(array) 的值为 [1,5,2,3,4]
也就是把重复的值都去掉,只保留不重复的值。要求写出两个答案:
- 一个答案不使用 Set、Map 等数据结构实现(6分)
- 另一个答案使用数据结构(4分)
- 说出每个方案缺点的,再额外每个方案加 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));