如何答一道惊艳面试官的数组去重问题?

为何写这篇文章?

  1. 数组去重应该是面试必考问题之一。
  2. 虽然它是一道并不复杂的问题,可是也能看出面试者的广度和深度,还有考虑问题的全面性。
  3. 实际开发中咱们应该选择哪一种方式数组去重,本文告诉你。
  4. 你觉得的不必定你觉得,面试官不仅是让你去重一个数组,他想知道的有点多,包括你的思想。

做者简介:koala,专一完整的 Node.js 技术栈分享,从 JavaScript 到 Node.js,再到后端数据库,祝您成为优秀的高级 Node.js 工程师。【程序员成长指北】做者,Github 博客开源项目 github.com/koala-codin…javascript

当面试官问到时怎么回答?

首先:我知道多少种去重方式

双层 for 循环

function distinct(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);
                // splice 会改变数组长度,因此要将数组长度 len 和下标 j 减一
                len--;
                j--;
            }
        }
    }
    return arr;
}
复制代码

思想: 双重 for 循环是比较笨拙的方法,它实现的原理很简单:先定义一个包含原始数组第一个元素的数组,而后遍历原始数组,将原始数组中的每一个元素与新数组中的每一个元素进行比对,若是不重复则添加到新数组中,最后返回新数组;由于它的时间复杂度是O(n^2),若是数组长度很大,效率会很低html

Array.filter() 加 indexOf

function distinct(a, b) {
    let arr = a.concat(b);
    return arr.filter((item, index)=> {
        return arr.indexOf(item) === index
    })
}
复制代码

思想: 利用indexOf检测元素在数组中第一次出现的位置是否和元素如今的位置相等,若是不等则说明该元素是重复元素java

Array.sort() 加一行遍历冒泡(相邻元素去重)

function distinct(array) {
    var res = [];
    var sortedArray = array.concat().sort();
    var seen;
    for (var i = 0, len = sortedArray.length; i < len; i++) {
        // 若是是第一个元素或者相邻的元素不相同
        if (!i || seen !== sortedArray[i]) {
            res.push(sortedArray[i])
        }
        seen = sortedArray[i];
    }
    return res;
}
复制代码

思想: 调用了数组的排序方法 sort(),V8引擎 的 sort() 方法在数组长度小于等于10的状况下,会使用插入排序,大于10的状况下会使用快速排序(sort函数在我以前高阶函数那篇文章有详细讲解【JS必知必会】高阶函数详解与实战)。而后根据排序后的结果进行遍历及相邻元素比对(其实就是一行冒泡排序比较),若是相等则跳过该元素,直到遍历结束。git

ES6 中的 Set 去重

function distinct(array) {
   return Array.from(new Set(array));
}
复制代码

甚至能够再简化下:程序员

function unique(array) {
    return [...new Set(array)];
}
复制代码

还能够再简化下:github

let unique = (a) => [...new Set(a)]
复制代码

思想: ES6 提供了新的数据结构 Set,Set 结构的一个特性就是成员值都是惟一的,没有重复的值。(同时请你们注意这个简化过程)面试

Object 键值对

function distinct(array) {
    var obj = {};
    return array.filter(function(item, index, array){
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}
复制代码

这种方法是利用一个空的 Object 对象,咱们把数组的值存成 Object 的 key 值,好比 Object[value1] = true,在判断另外一个值的时候,若是 Object[value2]存在的话,就说明该值是重复的,可是最后请注意这里obj[typeof item + item] = true没有直接使用obj[item],是由于 由于 123 和 '123' 是不一样的,直接使用前面的方法会判断为同一个值,由于对象的键值只能是字符串,因此咱们可使用 typeof item + item 拼成字符串做为 key 值来避免这个问题。算法

reduce 实现对象数组去重复

var resources = [
            { name: "张三", age: "18" },
            { name: "张三", age: "19" },
            { name: "张三", age: "20" },
            { name: "李四", age: "19" },
            { name: "王五", age: "20" },
            { name: "赵六", age: "21" }
        ]
     var temp = {};
     resources = resources.reduce((prev, curv) => {
         // 若是临时对象中有这个名字,什么都不作
         if (temp[curv.name]) {
         }
         // 若是临时对象没有就把这个名字加进去,同时把当前的这个对象加入到prev中
         else {
             temp[curv.name] = true;
             prev.push(curv);
         }
         return prev
     }, []);
     console.log("结果", resources);
复制代码

这种方法是利用高阶函数 reduce 进行去重, 这里只须要注意initialValue得放一个空数组[],否则无法push。关于高阶函数 reduce 的详细讲解能够看我以前这篇文章【JS必知必会】高阶函数详解与实战数据库

而后:询问面试官具体场景

(若是你考虑的这些和你问的,面试官不觉得然,可能本身都没想,随便让你数组去重,可能这个面试官也...)后端

性能考虑(是想要最快的速度查到数据吗?)

为了测试这些解法的性能,我写了一个测试模版,用来计算数组去重的耗时。 模版代码以下:

// distinct.js

let arr1 = Array.from(new Array(100000), (x, index)=>{
    return index
})

let arr2 = Array.from(new Array(50000), (x, index)=>{
    return index+index
})

let start = new Date().getTime()
console.log('开始数组去重')

let arr = a.concat(b);

function distinct(arr) {
    // 数组去重
}

console.log('去重后的长度', distinct(arr).length)

let end = new Date().getTime()
console.log('耗时', end - start)
复制代码

上面的多种数组去后,计算耗费时间

双重 for 循环 > Array.filter()加 indexOf > Array.sort() 加一行遍历冒泡 > Object 键值对去重复 > ES6中的Set去重

注意:这里只是本人测试的结果,具体状况可能与场景不一样,好比排序过的数组直接去重,直接使用冒泡相邻比较性能可能更好。你们也能够本身尝试一下,有问题欢迎一块儿讨论指出。

兼容性与场景考虑(数组中是否包含对象,NaN等?)

咱们要考虑这个数组中是否有null、undefined、NaN、对象若是两者都出现,上面的全部数组去重方法并非都是适用哦,下面详细说一下。

先说一下 == 和 === 区别

=== 严格相等,会比较两个值的类型和值 == 抽象相等,比较时,会先进行类型转换,而后再比较值 想更详细了解转换过程的能够看这篇文章js 中 == 和 === 的区别

说一下我说的几个类型的相等问题

let str1 = '123';
let str2 = new String('123');

console.log(str1 == str2); // true
console.log(str1 === str2); // false

console.log(null == null); // true
console.log(null === null); // true

console.log(undefined == undefined); // true
console.log(undefined === undefined); // true

console.log(NaN == NaN); // false
console.log(NaN === NaN); // false

console.log(/a/ == /a/); // false
console.log(/a/ === /a/); // false

console.log({} == {}); // false
console.log({} === {}); // false
复制代码

几种去重函数针对带有特殊类型的对比

indexOf 与 Set 的一点说明

上面代码中console.log(NaN === NaN); // false, indexOf 底层使用的是 === 进行判断,因此使用 indexOf 查找不到 NaN 元素

// demo1
var arr = [1, 2, NaN];
arr.indexOf(NaN); // -1
复制代码

Set能够去重NaN类型, Set内部认为尽管 NaN === NaN 为 false,可是这两个元素是重复的。

// demo2
function distinct(array) {
   return Array.from(new Set(array));
}
console.log(unique([NaN, NaN])) // [NaN]
复制代码

具体去重比较

将这样一个数组按照上面的方法去重后的比较:

var array = [1, 1, '1', '1', null, null, undefined, undefined, new String('1'), new String('1'), /a/, /a/, NaN, NaN];
复制代码
方法 结果 说明
双层 for 循环 [1, "1", null, undefined, String, String, /a/, /a/, NaN, NaN] 对象和 NaN 不去重
Array.sort()加一行遍历冒泡 [/a/, /a/, "1", 1, String, 1, String, NaN, NaN, null, undefined] 对象和 NaN 不去重 数字 1 也不去重
Array.filter()加 indexOf [1, "1", null, undefined, String, String, /a/, /a/] 对象不去重 NaN 会被忽略掉
Object 键值对去重 [1, "1", null, undefined, String, /a/, NaN] 所有去重
ES6中的Set去重 [1, "1", null, undefined, String, String, /a/, /a/, NaN] 对象不去重 NaN 去重

内存考虑(去重复过程当中,是想要空间复杂度最低吗?)

虽说对于 V8 引擎,内存考虑已经显得不那么重要了,并且真的数据量很大的时候,通常去重在后台处理了。尽管如此,咱们也不能放过任何一个能够证实本身优秀的,仍是考虑一下,嘿嘿。

以上的全部数组去重方式,应该 Object 对象去重复的方式是时间复杂度是最低的,除了一次遍历时间复杂度为O(n) 后,查找到重复数据的时间复杂度是O(1),相似散列表,你们也可使用 ES6 中的 Map 尝试实现一下。

可是对象去重复的空间复杂度是最高的,由于开辟了一个对象,其余的几种方式都没有开辟新的空间,从外表看来,更深刻的源码有待探究,这里只是要说明你们在回答的时候也能够考虑到时间复杂度还有空间复杂度

另外补充一个误区,有的小伙伴会认为 Array.filter()indexOf 这种方式时间复杂度为 O(n) ,其实不是这样,我以为也是O(n^2)。由于 indexOf 函数,源码其实它也是进行 for 循环遍历的。具体实现以下

String.prototype.indexOf = function(s) {
    for (var i = 0; i < this.length - s.length; i++) {
        if (this.charAt(i) === s.charAt(0) &&
            this.substring(i, s.length) === s) {
            return i;
        }
    }
    return -1;
};
复制代码

补充说明第三方库lodash

lodash 如何实现去重

简单说下 lodashuniq 方法的源码实现。

这个方法的行为和使用 Set 进行去重的结果一致。

当数组长度大于等于 200 时,会建立 Set 并将 Set 转换为数组来进行去重(Set 不存在状况的实现不作分析)。当数组长度小于 200 时,会使用相似前面提到的 双重循环 的去重方案,另外还会作 NaN 的去重

总结

面试时回答面试官的问题,除了你能把代码编出来运行出正确的结果,正确还包含对问题的独到看法,还须要考虑下面的问题:

  • 优化
  • 代码规范
  • 容错性

其实若是是很是难的问题,对你的竞争对手来讲,也是难的,关键在于你所表达出的解决问题的思路,甚至经过表达解题思路的方向,以及你考虑问题的全面性,其实不只仅这一道简单面试题,算法题是如此。我是koala,今天这篇先写这么多,一块儿加油。

参考文章

MDN中一些函数讲解

深刻分析数组去重

JavaScript专题之数组去重

排序算法学习总结

关注我

  • 欢迎加我微信【coder_qi】,拉你进技术群,长期交流学习...
  • 欢迎关注「程序员成长指北」,一个用心帮助你成长的公众号...

Node系列原创文章

深刻理解Node.js 中的进程与线程

想学Node.js,stream先有必要搞清楚

require时,exports和module.exports的区别你真的懂吗

源码解读一文完全搞懂Events模块

Node.js 高级进阶之 fs 文件模块学习

相关文章
相关标签/搜索