手记

老生常谈:问你数组去重的时候你该怎么答?

行文之前,还是再三提醒自己,追求小而美的思想,长话短说。。。

之前我专门写了一篇关于类数组的介绍的文章 五分钟全面了解类数组 ,通过与数组的对比比较来让读者了解类数组的相关使用方法,既然都扯到了类数组上面了,顺带就把数组相关的知识点捋一捋,整好最近处理了些招聘的事宜,在面试中也不免会问一些这种考察基础的入门级面试问题--数组去重,但让人吃惊的是,在面试中很大一部分面试者实际上是答不好这个问题,因此,今天想在这里再次唠一唠数组去重那点子事。

双层循环

这也是面试者答的最多的方案,大致思想是采用双层循环的思想,定义一个变量数组 res 保存结果,遍历需要去重的数组,如果该元素已经存在在 res 中了,则说明是重复的元素,如果没有,则放入 res 中。

function unique(a) {
  var res = [];

  for (var i = 0, len = a.length; i < len; i++) {
    var item = a[i];

    for (var j = 0, jLen = res.length; j < jLen; j++) {
      if (res[j] === item)
        break;
    }

    if (j === jLen)
      res.push(item);
  }

  return res;
}

var arr = [1, 1, '1', '2', 1];
var result = unique(arr);
console.log(result); // => [1, "1", "2"]

这样确实实现了数组的去重,但是很不幸的是,面试官一般都会继续问你:还有呢?

好吧,不卖关子,其实针对循环思想的去重,我们至少还有以下几种衍生拓展方法:

循环变更及优化拓展

我们知道,在代码块中,循环是相对比较吃内存的,在上面的基本思想中,我们使用了双重循环,也就是将代码运行的复杂度变成了O(n^2),这显然是可以有后续优化的。

变更一

首先,我们可以减少循环的次数来优化代码:

function unique(arr) {
  if (arr && arr.length === 1) {
      return arr;
  }
  var res = [];

  for (var i = 0, len = a.length; i < len; i++) {
    var item = a[i];

    res.indexOf(item) === -1 && res.push(item);
  }

  return res;
}

var arr = [1, 1, '1', '2', 1];
var result = unique(arr);
console.log(result); // => [1, "1", "2"]

这样代码又简单了不少,并且,从性能角度,我们采用了Array原生的方法indexOf来替代内部循环,着实比双层循环性能高一些的。

变更二

还有呢?

既然我们已经将内部循环省去了,可能再把外层循环去掉吗? 答案是肯定的,你要记住,在面试中我们要尽量的将自己知道的多的解决方案表述出来,尽管这些思想在实际编码中不会用到,实际上,在这里,我们还可以将外层循环用ES5中数组自带的forEach代替:

function unique(array) {

    if (array && array.length <= 1) {
        return array;
    }

    var res = [];

    array.forEach(function (value, index) {
        res.indexOf(value) == -1 ? res.push(value) : '';
    })
    return res;

}

var arr = [1, 1, '1', '2', 1];
var result = unique(arr);
console.log(result); // => [1, "1", "2"]

变更三

既然提到了forEach,虽说不是我们自己写的循环,但实际上还是循环嘛,好吧,再换一下:

function unique(arr) {

  var res = arr.filter(function(item, index, array) {
    return array.indexOf(item) === index;
  });

  return res;
}

var a = [1, 1, '1', '2', 1];
var ans = unique(a);
console.log(ans); // => [1, "1", "2"]

不多解释,采用数组的另一个方法filter来替代循环,效果依然很好。

变更四

还有呢?

现在,我们将两层循环减少到了一层循环,那么再大胆一点,有没有不用循环的呢?是的,有!


function unique(arr) {

    return arr.sort().reduce((accumulator, current) => {
        const length = accumulator.length;
        if (length === 0 || accumulator[length - 1] !== current) {
            accumulator.push(current);
        }
        return accumulator;
    }, []);
}

var arr = [1, 1, '1', '2', 1];
var result = unique(arr);
console.log(result); // => [1, "1", "2"]

在这里,我们采用了ES5中Array原生方法reduce来代替循环,是不是觉得代码立刻上升了几个档次呢,实际上,这确实是展示自己脑容量的一个方案哦!

ES6实现数组去重的解决方案

说实话,这个也是代码量最少,最容易记住的一个方法,不得不说,ES6提供的语法糖确实给我们带来了极大的便利啊。。。


function unique(arr) {
    return [...New Set(arr)]
}

var arr = [1, 1, '1', '2', 1];
var result = unique(arr);
console.log(result); // => [1, "1", "2"]

Set运算会返回一个无重复字段的set类型(类似于一个只有键的对象类型:{1,'1','2'}),然后我们用过...运算符将其转换为数组,最终得到这么一个无重复字段的数组。

这里我们也可以用Array.from来替代...运算符:


function unique(arr) {
    return Array.from(New Set(arr))
}

var arr = [1, 1, '1', '2', 1];
var result = unique(arr);
console.log(result); // => [1, "1", "2"]

结果一样,不多做解释。

利用Object的思想去实现

上面提到了利用set类型实现去重的思路,类似于一个只包含键的对象,那么我们知道,对象的键是不会重复的,因此,我们可以利用这个思维再次实现数组的去重:


function unique(a) {
  var ret = [];
  var hash = {};

  for (var i = 0, len = a.length; i < len; i++) {
    var item = a[i];

    // 这里由于对象的键都是字符串类型,我们需要将值类型添加到一起来区分数字和字符串
    var key = typeof(item) + item;

    if (hash[key] !== 1) {
      ret.push(item);
      hash[key] = 1;
    }
  }

  return ret;
}

var arr = [1, 1, 3, 2, '4', 1, 2, 4, '1'];
var result = unique(arr);
console.log(result); // => [1, 3, 2, "4", 4, "1"]
特殊情况处理

我们知道,真正优质的代码体现在对细节的考虑周祥和不同情况的兼容性和健壮性,拿数组去重来讲,诚然,上面的方法各有各的优缺点,在不同场景下,有不同优势的体现,但是他们大多只能处理规范数据的运算,如果数据中出现了{},NaN这类的数据的情况的时候,上面的方法就会失效的。

要想处理这几种情况,那么我们需要知道他们各自的区分方法:

  • {}的区分可以采用 JSON.stringify({}) == '{}' 或结合 Array.prototype.toString.call() === '[object Object]'来区分
 function isEmptyObject(object) {
    var isEmpty = true;
    if (Object.prototype.toString.call(object) === "[object Object]") {
      for (var item in object) {
        // 存在属性或方法,则不是空对象
        isEmpty = false
      }
    } 

    return isEmpty;
  }
  • NaN的区分不能采用NaN === NaN的方式,因为NaN !== NaN, 因此,在特殊处理的时候,可以先用array[i] !== array[i]的方式检测是否是NaN,然后根据结果,进行后续操作。
function uniqueArr(_array) {
    var temp = [];
    var emptyFlag = true; // 标识位
    var NaNFlag = true; // 标识位
    console.log(_array.length)
    //遍历当前数组
    for (var a = 0, b = _array.length; a < b; a++) {

        // 标识位的作用就是用来判断是否存在NaN和空对象,第一次找到保留到新数组中
        // 然后标识位置改为false是为了再次找到的时候不推入数组

        console.log(_array[a]);
        if (isEmptyObject(_array[a])) {
            // 空对象{}检测
            emptyFlag && temp.indexOf(_array[a]) == -1 ? temp.push(_array[a]) : '';
            emptyFlag = false;
        } else if (_array[a] !== _array[a]) {
            // NaN检测
            NaNFlag && temp.indexOf(_array[a]) == -1 ? temp.push(_array[a]) : '';
            NaNFlag = false;
        } else {
            temp.indexOf(_array[a]) === -1 ? temp.push(_array[a]) : '';
        }
    }
    console.log(temp);
    return temp;
  }
总结

写这篇文章的本意其实想传递一种面试答题的思想,实际上我认为,什么问题不是最重要的,代码也不是最重要的,最重要的是通过一个简单的问题,是否能让你在面试的时候合理有序的给出你的答题思路,是否在这个过程中能够正确的反应出你的技术水平,我想这才是最重要的。

就拿文章中的数组去重为例,不同层次的人可以答出不同的解决方案,但是试想,如果你能在答题过程中,有理有条的给出类似于文中提到的方法和解决思路,甚至能够说出你每一个解决思路的优缺点,它们各自变体的思路来源,那么我想一定会给面试官一个很好的印象。。。

不早了,今天先到这吧, 晚安各位!

感谢这个时代,让我们可以站在巨人的肩膀上,窥探程序世界的宏伟壮观,我愿以一颗赤子心,踏遍程序世界的千山万水!愿每一个行走在程序世界的同仁,都活成心中想要的样子,加油

18人推荐
随时随地看视频
慕课网APP

热门评论

第一个解决办法真的有用么?

可能我基础不太好,前面的看不太明白,后面的反而能看懂一些。 好文点赞!

好文章,已收藏。

查看全部评论