今天,我们来详细解析一下算法中的一道经典题型。给你一个数组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));
这样我们就解决了这个问题了。我们来看看输出结果:
确实去重成功了。请问这段代码的时间复杂度和空间复杂度是多少呢?
时间复杂度看你执行了多少次。双重for循环,外面执行一次,里面执行n次,所以复杂度为。
空间复杂度看你是否开辟了新的空间。我们定义了一个新数组,所以复杂度为n。
2. for + indexOf
那还有没有更简单一点的方法呢?我们再来看一种新的方法。
我们来看一下数组上的这个方法:indexOf。它是用来干什么的呢?
它可以用来返回一个数组值的下标。
而当数组中不存在这个值时,它会返回-1。
所以我们是不是能用这个方法去改进我们的上一段代码。
在第一种方法中,我们写第二层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++,进入下一次循环。
我们也来看看执行结果:
那此时的时间复杂度和空间复杂度是多少呢?其实和上一种方法一样。时间复杂度为,空间复杂度为n。因为indexof方法也会去遍历新数组,外面执行一次,里面执行n次。但这样能让我们的代码变得更好看、更简洁。
3. for + includes
我们再来看第三种方法。数组上还有这样一个方法:includes。我们来看看它是怎么用的。
其实它和indexOf差不多。indexOf返回的是当前值在数组中的下标。而includes返回的是一个布尔值。当这个值在数组存在时,返回true,否则返回false。
所以我们直接拿它去替换上面的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操作。
执行结果也是:
这个时间复杂度和空间复杂度就和indexOf一样。
4. filter + sort
我们再来看第四种方法。filter加上sort的结合。
我们来这样想一想:我们如果先把这个数组排序呢?假如我们已经帮arr排好序得到这样一个数组:arr = [1, 1, 2, 2, 3, 4]。
我们先将数组的第一个值存入新数组中,再去判断后面的值与它前一个值是否相等,如果相等就跳过去判断下一个;如果不相等就将这个值push到新数组上。
那数组中有没有能进行排序的方法呢?那就轮到sort登场了。
sort是怎么使用的呢?我们来看一下:
直接用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 ,它就能进行降序排序。
了解完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);
})
我们去打印输出三个参数,就能遍历这个数组。
它还有这样的语法:当我们只要数组中小于等于2的值时,我们可以在函数体内return item<=2,它会返回一个新数组。
const arr = [1, 2, 3, 4, 2, 1]
arr.filter((item, index, arr) => {
return item <= 2
})
console.log(arr2);
它成功的返回了数组中小于等于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]语句,去判断此时的值是否与自己的前一个值相等,如果不相等我们就留下了;如果相等判断下一个值。
这样我们就写完了这段代码,变得更简洁了一点。我们运行看看结果:
其实这段代码还能变得更简洁。对于箭头函数,如果我们不加上{}括号,它就会自带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,看看输出结果:
我们发现'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其实无所谓,在这里我们就存下标。我们来看一下输出结果:
我们成功去除了数组中的重复值。但有一个小问题:数组中的值变成了字符串,但我们要的是数字。数组身上还有个方法,能将数组中字符串值是数字的转换成数字。用map方法,在参数里面发一个Number。
let obj = {
1: 0,
2: 1,
3: 2,
4: 3,
2: 4,
1: 5
}
console.log(Object.keys(obj).map(Number));
这样就达到了我们想要的效果了。好,我们可以开始着手写这段代码了。
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));
这样我们就用对象完成了数组去重。输出结果照样是:
这段代码还能再简洁一点,我们前面还介绍了一种遍历数组的方法,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);
它也是用new去调用得到一个实例对象,得到的是一个set类型的空对象。
如果我们想往它身上添加值,我们这样添加。
let s = new Set()
s.add(1)
s.add(2)
s.add(3)
console.log(s);
我们用add方法添加。
它是一种特殊的对象,既像数组又像对象。这个对象里只有key没有value。它也叫类数组。它只能存值不能取值,但是它可以判断是否存在某一个值。用has方法。例如:
let s = new Set()
s.add(1)
s.add(2)
s.add(3)
console.log(s.has(1));
它可以返回一个布尔值。
它还有一个特点:它的成员是唯一的。例如:
let s = new Set()
s.add(1)
s.add(2)
s.add(3)
s.add(1)
console.log(s);
我们又往set对象上添加一个1,看看是否能添加上去。
我们发现并不能。此时我们就想到了,我们应该能将它运用到数组去重问题上吧。
它还可以接受一个数组,将它转换成set对象。如果我们将数组arr传给它,它就能帮我们去掉重复的值。
let s = new Set([1, 2, 3, 4, 2, 1])
console.log(s);
确实得到了我们想要的结果。所以我们可以将它运用到数组去重上。
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对象中的值转换成单独的值再存放到一个新数组中去,这样就解决了。
这是我们能做到最简单的方法了。借用set对象的特点解决这个问题。
7. 总结
我们介绍了六种能解决数组去重的方法:
-
双层for循环
-
for + indexOf
-
for + includes
-
filter + sort
-
filter + {}
-
Set
当我们在实际工作中碰到了此类问题,不妨想一想能否用上面六种方法解决。