数组去重

又在不经意之间看到了一个面试题,应该算是比较久远的了

数组去重

为了培养 JavaScript 的语感,今天就把能想到的方法都梳理一遍

没有编程基础的人的思维

比如说现在有一个数组

var a = [1,2,2,3,3,4,4,5]

我猜按照正常的思维逻辑,肯定会这么想

  • a 中的第一项拿出来放到空数组 b
  • a 中的第二项拿出来和数组 b 的所有成员比一下,如果有重复的就跳过,如果没有重复的就把这一项加载到 b 里去
  • a 中的第三项拿出来和数组 b 的所有成员比一下,如果有重复的就跳过,如果没有重复的就把这一项加载到 b 里去
  • 。。。
  • 一直到最后一项

好了,接下来我们来将思路码出来

var a = [1,2,2,3,3,4,4,5]
var b = [] // 声明一个空数组

// 以下为伪代码
if(a[0] !== b[0]) { // 一开始 b 并没有成员,所以这条语句的意思就是把 a 的第一项添加到 b 上
    b.push(a[0])
}

if(a[1] !== b[0])  {
    b.push(a[1])
}

if(a[2] !== b[0] && a[2] !== b[1]){
    b.push(a[2])
}

....

能感受到一点规律么 if() {} 这一块可以用 for 循环搞定 而 if 后面的 () 里的内容可以用另一个 for 循环搞定

赶紧把想法写出来

var a = [1,2,2,3,3,4,4,5]
var b = [] // 声明一个空数组

for(var i = 0; i < a.length; i++) {
    var item = a[i]
    for(var j = 0; j < b.length; j++) {
        if(b[j] !== item) {
            // 天哪,写不下去了,我们并不能够用循环模拟出一个 && 的作用啊
        }
    }
}

代码的思路断了,不过这并不能难倒我,我们可以计数嘛

先初始化一个 k,只要 b[j] !== itemk 就加 1,最后统计一下每过一轮 for(var i...) 的循环后 k 的值就能得知到底有没有重复的了,好聪明

var a = [1,2,2,3,3,4,4,4,4,4,4,5]
var b = [] // 声明一个空数组

for(var i = 0; i < a.length; i++) {
    var item = a[i]
    var k = 0
    for(var j = 0; j < b.length; j++) {
        if(b[j] !== item) {
            k = k + 1
        }
    }
    if(k === b.length || i === 0) { // 加 || 运算符后面这句话是因为当最外层的循环 i = 0 的时候,内层的循环并不会执行
        b.push(item)
        
    }
}
console.log(b) // [1, 2, 3, 4, 5]

好了,这就是数组去重的方法,总结一下思路

  • 初始化一个空数组 b
  • 逐个将待处理数组 a 的成员与 b 数组中的每一个成员比较
  • 如果都不相等,才将这个 a 数组里的成员放到 b 数组

以上就是符合没有学过编程的人但想要解答这道题的基本思路,不知道有没有认同的

但是我们是程序员啊,不能就这么算了,看看能不能把这段代码再优化优化

我觉得计数太麻烦了,能不能不计数,想一想

这时候,我看到了上面总结的思路的最后一句话

如果都不相等,才将这个 a 数组里的成员放到 b 数组

换言之

只要有一个相等,数组 b 就不会被添加新成员

有了思路就马上实施

我们把

if(b[j] !== item) {...}

改成

if(b[j] === item) {...}

如果数组 b 的成员只要有一个和 item 相等,那么就不会 b.push(item)

var a = [1,2,2,3,3,4,4,4,4,4,4,5]
var b = [] // 声明一个空数组

for(var i = 0; i < a.length; i++) {
    var item = a[i]
    for(var j = 0; j < b.length; j++) {
        if(b[j] == item) {
            break; // 只要有相等的情况,就跳出这个 for 循环
        }
    }
    if(j === b.length || i === 0) { // 加 || 运算符后面这句话是因为当最外层的循环 i = 0 的时候,内层的循环并不会执行
        b.push(item)
    }
}

console.log(b) // [1, 2, 3, 4, 5]

是的,比一开始的代码稍微简洁那么一点。。。

先排个序呢

我们知道,怎么给数组排序

var a = [2,2,1,4,4,4,3,3,5]
a.sort(function(a, b) {
  return a - b
})
console.log(a) // [1, 2, 2, 3, 3, 4, 4, 4, 5]

对于一个还排序的数组去重的方法,我好像有了一个新思路

从数组的第二个成员开始,与其前一项做对比,如果相等,那么说明有重复,删除之

说干就干!

var a = [2,2,1,4,4,4,3,3,5]
a.sort(function(a, b) {
  return a - b
})
for(var i = 1; i < a.length; i++) {
    if(a[i] === a[i - 1]) {
        a.splice(i, 1)
    }
}
console.log(a) // [1, 2, 3, 4, 4, 5]

额,并没有成功,有两个重复的4

这一小段代码很好分析,因为 a.splice(i, 1) 删掉了一个成员,但是 for 循环里面的 i 还是沿用没有删掉成员的值,所以

var a = [2,2,1,4,4,4,3,3,5]
a.sort(function(a, b) {
  return a - b
})
for(var i = 1; i < a.length; i++) {
    if(a[i] === a[i - 1]) {
        a.splice(i, 1)
        i -= 1 // 因为删掉了一个成员,成员数就得减1
    }
}
console.log(a) // [1, 2, 3, 4, 5]

这个方法有个副作用,就是数组的顺序变了

你听过对象的键名有重复的么

是啊,对象的键名是没有重复的,这给了我新思路

var a = [1, 2, 2, 3, 3, 4, 4, 4, 5]
var b = []
var o = {}
for(var i = 0; i < a.length; i++) {
    var item = a[i]
    if(!o[item]) {
        o[item] = true
        b.push(item)
    }
}
console.log(b) // [1, 2, 3, 4, 5]

这个方法有个缺陷,他分不清 1"1",因为键名会默认转换为字符串

var a = [1, '1', 2, 2, 3, 3, 4, 4, 4, 5]
var b = []
var o = {}
for(var i = 0; i < a.length; i++) {
    var item = a[i]
    if(!o[item) {
        o[item] = true
        b.push(item)
    }
}
console.log(b) // [1, 2, 3, 4, 5] // 分不清 1 和 '1'

这也有解决的办法

  • 如果键名不存在,毫无疑问,这个值是没有重复的
  • 如果键名存在,那么利用 typeof 判断一下他的类型
  • 如果这个类型之前出现过,不管
  • 如果这个类型之前没出现过,就把它放到新数组

试试

var a = [1, '1', 2, 2, 3, 3, 4, 4, 4, 5]
var b = []
var o = {}
for(var i = 0; i < a.length; i++) {
    var item = a[i]
    var type = typeof a[i]
    if(!o[item]) {
        o[item] = [type]
        b.push(item)
    }else if(o[item].indexOf(type) === -1) {
        o[item].push(type)
        b.push(item)
    }
}
console.log(b) // [1, "1", 2, 3, 4, 5] // 现在分得清 1 和 '1' 了

当然了,这方法还是有缺陷,因为

console.log(typeof []) // Object
console.log(typeof {}) // Object

也就是说如果数组里的成员是简单类型的那么好办,对于复杂类型的就无能为力了

先进的 ES5

ES5 新增了 indexOf() 方法,接受两个参数

  • 要查找的项
  • 查找位置起点的索引

返回查找到的项的位置,如果没找到返回 -1

看完这段介绍我似乎又有思路了

var a = [1, 2, 2, 3, 3, 4, 4, 4, 5]
var b = []

for(var i = 0; i < a.length; i++) {
    if(b.indexOf(a[i]) === -1){
        b.push(a[i])
    }
}
console.log(b) //  [1, 2, 3, 4, 5]

搞定

懒惰的 ES6

ES6 提供了新的数据结构 Set

他类似于数组,但是成员的值是唯一的,比如

const set = new Set([1,2,2,3,3,4,4,4,5])
console.log(set) // Set(5) {1, 2, 3, 4, 5}

这就简单了,再把这个数据结构转换成数组就行了么

ES6 又提供了一个新的运算符 ...,他的其中一个功能就是吧任何类似数组的对象转为真正的数组

const set = new Set([1,2,2,3,3,4,4,4,5])
console.log([...set]) // [1, 2, 3, 4, 5]

好了

程序员的三大美德

Perl 语言的发明人 Larry Wall 说,好的程序员有3种美德:

  • 懒惰
    • 是这样一种品质,它使得你花大力气去避免消耗过多的精力。它敦促你写出节省体力的程序,同时别人也能利用它们。为此你会写出完善的文档,以免别人问你太多问题。
  • 急躁
    • 是这样一种愤怒----当你发现计算机懒洋洋地不给出结果。于是你写出更优秀的代码,能尽快真正的解决问题。至少看上去是这样。
  • 傲慢
    • 极度的自信,使你有信心写出(或维护)别人挑不出毛病的程序。

这篇文章就展现了程序员的其中一个美德

懒惰

(完)


文档信息

  • 自由转载-非商用-非衍生-保持署名
  • 发表日期:2017年5月17日

推荐阅读更多精彩内容