数组去重专题 ——关于算法的学习

220 阅读14分钟

今天,我们来详细解析一下算法中的一道经典题型。给你一个数组arr[1, 2, 3, 4, 2, 1],请你去掉数组中的重复的值。

我们多用几种方法来解决,让我们对数组去重理解得更深刻一点。

1. 双层for循环

const arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    
}
console.log(unique(arr));

我们定义一个函数unique,接收一个形参arr,能去掉arr中的重复的值。

应该怎么写呢?

我们来这样想一想:我们定义一个新的空数组newArr,然后将数组arr中的第一个值先存进去,然后在存arr中第二个值的时候我们先拿这个值去和新数组中的每一个值去比较,如果不相等就存进去;如果相等就去比较arr中第三个值。如此循环。是不是用一个双重for循环可以解决。

我们来写:

const arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    let newArr = []
    for (let i = 0; i < arr.length; i++) {
        for (var j = 0; j < newArr.length; j++) {
            
        }
    }
    return newArr
}
console.log(unique(arr));

我们写两个for循环。第一层:let i = 0; i < arr.length; i++;第二层:for (var j = 0; j < newArr.length; j++)

然后我们就去拿arr中的第i个值去遍历比较新数组的每一个值。一旦出现相等的值,我们就break,结束当前的for循环。

const arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    let newArr = []
    for (let i = 0; i < arr.length; i++) {
        for (var j = 0; j < newArr.length; j++) {
            if (arr[i] === newArr[j]) {
                break
            }
        }
    }
    return newArr
}
console.log(unique(arr));

双重for循环,外面的循环执行一次,里面的循环执行全部,所以新数组中的每个值都能与arr中的值去比较。

我们将判断条件设为了相等就break。如果不相等呢?我们是不是就要去将此时的arr[i]添加到newArr上去。

const arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    let newArr = []
    for (let i = 0; i < arr.length; i++) {
        for (var j = 0; j < newArr.length; j++) {
            if (arr[i] === newArr[j]) {
                break
            }          
        }
        newArr.push(arr[i])
    }
    return newArr
}
console.log(unique(arr));

那直接这样写对吗?直接在第二层for循环并列写了一条newArr.push(arr[i])语句。那要是判断相等了break跳出当前for循环是不是也会执行newArr.push(arr[i])语句啊。

所以我们得在这里加一个判断条件,只有在第二层循环全都执行完毕时都没有在新数组中找到与此时arr[i]相等的值,我们就把这个值添加到新数组上去,而此时j就应该等于newArr.length了,因为第二层循环全都执行了,所以j就等于newArr.length,这样第二层循环就会结束。而一旦发现相等的值,跳出当前for循环时,此时j最大也只能为newArr.length-1,因为新数组的下标最大只能为newArr.length-1,所以不可能执行添加操作,于是i++,进行下一次循环。

所以我们还得添加一个判断条件:

const arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    let newArr = []
    for (let i = 0; i < arr.length; i++) {
        for (var j = 0; j < newArr.length; j++) {
            if (arr[i] === newArr[j]) {
                break
            }          
        }
        if (j === newArr.length) {
            newArr.push(arr[i])
        }
    }
    return newArr
}
console.log(unique(arr));

这样我们就解决了这个问题了。我们来看看输出结果:

image.png

确实去重成功了。请问这段代码的时间复杂度和空间复杂度是多少呢?

时间复杂度看你执行了多少次。双重for循环,外面执行一次,里面执行n次,所以复杂度为n2n2

空间复杂度看你是否开辟了新的空间。我们定义了一个新数组,所以复杂度为n。

2. for + indexOf

那还有没有更简单一点的方法呢?我们再来看一种新的方法。

我们来看一下数组上的这个方法:indexOf。它是用来干什么的呢?

它可以用来返回一个数组值的下标。

image.png

而当数组中不存在这个值时,它会返回-1。

image.png

所以我们是不是能用这个方法去改进我们的上一段代码。

在第一种方法中,我们写第二层for循环的目的就是为了判断新数组中是否有arr[i]这个值,如果没有就添加到新数组上去。所以我们能用这个方法去代替第二层for循环。

const arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    let newArr = []
    for (let i = 0; i < arr.length; i++) {
       if (newArr.indexOf(arr[i]) === -1) {  
            newArr.push(arr[i])
        }
    }
    return newArr
}
console.log(unique(arr));

我们去判断newArr是否具有此时的arr[i],如果返回-1,说明没有,就push到新数组上;如果不返回-1,说明此时新数组中有这个值,我们就什么都不干,让i++,进入下一次循环。

我们也来看看执行结果:

image.png

那此时的时间复杂度和空间复杂度是多少呢?其实和上一种方法一样。时间复杂度为n2n2,空间复杂度为n。因为indexof方法也会去遍历新数组,外面执行一次,里面执行n次。但这样能让我们的代码变得更好看、更简洁。

3. for + includes

我们再来看第三种方法。数组上还有这样一个方法:includes。我们来看看它是怎么用的。

其实它和indexOf差不多。indexOf返回的是当前值在数组中的下标。而includes返回的是一个布尔值。当这个值在数组存在时,返回true,否则返回false。

image.png

所以我们直接拿它去替换上面的indexOf就行了。

const arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    let newArr = []
    for (let i = 0; i < arr.length; i++) {
       if (!newArr.includes(arr[i])) {  
            newArr.push(arr[i])
        }
    }
    return newArr
}
console.log(unique(arr));

我们将判断条件设置为!newArr.includes(arr[i]),“!”用来取反。

当新数组中存在这个值时,返回true,然后取反变成false,就不执行push操作。当新数组中不存在这个值时,返回false,取反变成true,于是就执行push操作。

执行结果也是:

屏幕截图 2024-11-26 231027.png

这个时间复杂度和空间复杂度就和indexOf一样。

4. filter + sort

我们再来看第四种方法。filter加上sort的结合。

我们来这样想一想:我们如果先把这个数组排序呢?假如我们已经帮arr排好序得到这样一个数组:arr = [1, 1, 2, 2, 3, 4]。

我们先将数组的第一个值存入新数组中,再去判断后面的值与它前一个值是否相等,如果相等就跳过去判断下一个;如果不相等就将这个值push到新数组上。

那数组中有没有能进行排序的方法呢?那就轮到sort登场了。

sort是怎么使用的呢?我们来看一下:

image.png

直接用arr去调用sort就行了。请注意,sort是会影响原数组的,它会直接在原数组上进行排序,也叫原地排序,所以它是没有空间复杂度的,它没有开辟一个新的空间。

它还能进行降序排序,sort能接收一个回调函数,参数设为a和b。

const arr = [1, 2, 3, 4, 2, 1]

arr.sort((a, b) => {
    return b - a
});
console.log(arr);

在函数里面,当返回 a - b 时,它就是升序排序,sort默认就是升序排序,所以不用写。当我们想让它降序排序时,我们就返回 b - a ,它就能进行降序排序。

image.png

了解完sort这个方法后,我们再来介绍一下这个方法:filter。它又有些什么妙用呢?

在第一种方法中,我们使用了for循环去遍历数组中的每一个值。那不用for循环,数组自己身上有没有这样一个方法能进行遍历呢?那就是我们刚刚提到的filter了。它是怎么使用的呢?

它的语法也是接收一个回调函数,里面放三个参数:item, index, arr。item代表此时的数组值,index代表此时数组值的下标,arr代表原数组。

const arr = [1, 2, 3, 4, 2, 1]

arr.filter((item, index, arr) => {
    console.log(item, index, arr);
})

我们去打印输出三个参数,就能遍历这个数组。

image.png

它还有这样的语法:当我们只要数组中小于等于2的值时,我们可以在函数体内return item<=2,它会返回一个新数组。

const arr = [1, 2, 3, 4, 2, 1]

arr.filter((item, index, arr) => {
    return item <= 2
})
console.log(arr2);

屏幕截图 2024-11-27 092637.png

它成功的返回了数组中小于等于2的值。

现在,我们了解完了filter和sort的用法与特定,我们可以把它们用在数组去重上。

我们按照我们一开始的思路来,我们先将数组排序。我们一般不在原数组上进行排序,因为在真实的工作中,原数组可能在其它地方还有用。所以我们得创建一个新的数组和arr一样。

我们这样来写:

const arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    let newArr = [...arr]

    
}
console.log(unique(arr));

让新数组 newArr = [...arr] ,三个点加一个数组可以对数组进行解构,将数组变成单独的每一个值,再存放到我们定义的新数组里去。这样我们就创建了一个和原数组arr一样的新数组。

然后我们把新数组排好序,newArr.sort() ,我们再对排好序的数组用filter遍历。

const arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    let newArr = [...arr]
    return newArr.sort().filter((item, index, array) => {
    
     })
    
}
console.log(unique(arr));

里面的条件应该怎么写呢?

首先,既然排好序了,那数组的第一个值是不是一定得留下来,然后再判断第二个值是否与第一个相等,如果不相等,我们也留下了;如果相等,再去比较第三个值是否与第二个值相等。

const arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    let newArr = [...arr]
    return newArr.sort().filter((item, index, array) => {
        return index === 0 || item !== array[index - 1]
     })
    
}
console.log(unique(arr));

我们这样写:return index === 0 || item !== array[index - 1],当index为0时,说明它是第一个值,就留下来;当index不为0时,因为我们使用逻辑运算符或连接,它就会去执行item !== array[index - 1]语句,去判断此时的值是否与自己的前一个值相等,如果不相等我们就留下了;如果相等判断下一个值。

这样我们就写完了这段代码,变得更简洁了一点。我们运行看看结果:

image.png

其实这段代码还能变得更简洁。对于箭头函数,如果我们不加上{}括号,它就会自带return。然后我们还可以不用定义一个新数组newArr,直接拿着 [...arr] 去排序和遍历。

const arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    return [...arr].sort().filter((item, index, array) => !index || item !== array[index - 1])
}
console.log(unique(arr));

这样我们一行代码就解决了。

这就是filter加上sort对于解决数组去重问题的妙用。

5. filter + {}

我们再来看解决数组去重问题的第五种方法,用filter加上对象。

在对象上有一个特点:对象绝不会出现重复的key。

例如:

let obj = {
    name: 'John',
    name: 'Tom',
    age: 18,
    city: 'New York'
}
console.log(obj);

我们定义了两个相同的key,看看输出结果:

image.png

我们发现'John'直接被覆盖了。

那我们来想一想,可不可以将数组的这个特性运用到数组去重上。如果我们将数组的每一个值作为key存放到对象里,对象就会自动帮我们去掉重复的key,然后再把对象中的key转换成数组输出不就行了。

例如:

let obj = {
    1: 0,
    2: 1,
    3: 2,
    4: 3,
    2: 4,
    1: 5
}
console.log(Object.keys(obj));

Object.keys(obj) 这个方法能将对象中的key读取到并返回一个数组保存。

我们将这个数组的值作为key直接存放在对象中,value其实无所谓,在这里我们就存下标。我们来看一下输出结果:

image.png

我们成功去除了数组中的重复值。但有一个小问题:数组中的值变成了字符串,但我们要的是数字。数组身上还有个方法,能将数组中字符串值是数字的转换成数字。用map方法,在参数里面发一个Number。

let obj = {
    1: 0,
    2: 1,
    3: 2,
    4: 3,
    2: 4,
    1: 5
}
console.log(Object.keys(obj).map(Number));

image.png

这样就达到了我们想要的效果了。好,我们可以开始着手写这段代码了。

let arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    let obj = {}
    for (let i = 0; i < arr.length; i++) {
        
    }
}
console.log(unique(arr));

我们先定义一个空对象obj,然后用for循环去遍历数组arr。我们得想个办法将数组的值作为key存放到对象中去。应该怎么做呢?

我们这样做:我们直接去判断对象中有没有这个key值,如果有我们就跳到下一次循环;如果没有我们就将这个值作为key存进去,value值就做true。

let arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    let obj = {}
    for (let i = 0; i < arr.length; i++) {
        if (!obj[arr[i]]) {
            obj[arr[i]] = true
        }
    }
}
console.log(unique(arr));

我们写了一条if判断语句,里面放着!obj[arr[i]]。假如此时arr[i]为1,那对象中就没有这个key为1的值,于是返回false,然后感叹号取反,执行if里的语句,就在对象上添加这个key为1的值。当再次循环到1时,它就不会存进去。

其实这里不做判断都行,我们直接存进去,对象会帮我们去掉重复的值。

然后再将对象里的key读取到,作为数组返回出来。

let arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    let obj = {}
    for (let i = 0; i < arr.length; i++) {
        if (!obj[arr[i]]) {
            obj[arr[i]] = true
        }
    }
    return Object.keys(obj).map(Number)
}
console.log(unique(arr));

这样我们就用对象完成了数组去重。输出结果照样是:

屏幕截图 2024-11-27 125410.png

这段代码还能再简洁一点,我们前面还介绍了一种遍历数组的方法,filter。我们直接用filter来代替for循环。

let arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    let obj = {}
    arr.filter((item, index, arrat) => {
        
    })
}
console.log(unique(arr));

我们用filter代替for循环,里面也去判断此时对象中是否有这个key值。我们这样写:

let arr = [1, 2, 3, 4, 2, 1]

function unique(arr) {
    let obj = {}
    return arr.filter((item, index, arrat) => {
        return obj[item] ? false : (obj[item] = true)
    })
}
console.log(unique(arr));

我们用一个三元运算符。如果存在这个key值,就return false,去遍历下一个数组值;如果不存在,就return (obj[item] = true),将这个item作为key值存放在对象中。再将这个filter得到的数组返回。

这样我们就用filter加上数组解决了数组去重问题。

6. Set

我们再来介绍最后一种解决数组去重的方法,借用es6打造的一种新的数据结构:Set

它有些什么特点呢?

let s = new Set()
console.log(s);

image.png

它也是用new去调用得到一个实例对象,得到的是一个set类型的空对象。

如果我们想往它身上添加值,我们这样添加。

let s = new Set()
s.add(1)
s.add(2)
s.add(3)
console.log(s);

我们用add方法添加。

image.png

它是一种特殊的对象,既像数组又像对象。这个对象里只有key没有value。它也叫类数组。它只能存值不能取值,但是它可以判断是否存在某一个值。用has方法。例如:

let s = new Set()
s.add(1)
s.add(2)
s.add(3)
console.log(s.has(1));

image.png

它可以返回一个布尔值。

它还有一个特点:它的成员是唯一的。例如:

let s = new Set()
s.add(1)
s.add(2)
s.add(3)
s.add(1)
console.log(s);

我们又往set对象上添加一个1,看看是否能添加上去。

屏幕截图 2024-11-27 141322.png

我们发现并不能。此时我们就想到了,我们应该能将它运用到数组去重问题上吧。

它还可以接受一个数组,将它转换成set对象。如果我们将数组arr传给它,它就能帮我们去掉重复的值。

let s = new Set([1, 2, 3, 4, 2, 1])
console.log(s);

image.png

确实得到了我们想要的结果。所以我们可以将它运用到数组去重上。

let arr = [1, 2, 3, 4, 2, 1]
function unique(arr) {
    return [...new Set(arr)]
}
console.log(unique(arr));

我们直接这样写:return [...new Set(arr)]。我们new Set(arr) 是不是得到了一个去掉重复值的set对象。但我们要的是数组,所以我们就用解构方法,将set对象中的值转换成单独的值再存放到一个新数组中去,这样就解决了。

image.png

这是我们能做到最简单的方法了。借用set对象的特点解决这个问题。

7. 总结

我们介绍了六种能解决数组去重的方法:

  1. 双层for循环

  2. for + indexOf

  3. for + includes

  4. filter + sort

  5. filter + {}

  6. Set

当我们在实际工作中碰到了此类问题,不妨想一想能否用上面六种方法解决。