JS数组——花式去重

137 阅读13分钟

前言

关于数组去重这个问题,在我们的日常学习或者开发项目的时候经常遇到,每门编程语言都有它们不同的去重方法,下面我们来看看在JavaScript这门编程语言中是如何进行数组去重的,下面标题分别是方法和时间复杂度。

1. 双层for循环 —— O(n^2)

关于数组去重这个问题,它可以勉强说是一个算法题,在我们做算法题的时候首先会想到的是直接暴力,然后从中找出特点从而降低时间复杂度,下面我们来按照做算法题的思想来解决一下数组去重这个问题。

首先我们看题目:去除数组中重复的那个元素,元素重复的个数不确定

下面来看了一下样例:

const arr = [1, 2, 3, 4, 2, 1]
//去重后
arr = [1, 2, 3, 4]

根据上面的样例可以知道我们只需要找到数组中重复的元素把它删除或者返回一个新数组即可,那么我们如何找到这个数字呢?

首先我们得先遍历一遍数组,然后在遍历的过程中进行判断该元素是否出现重复,如果出现重复则把它删除。想要判断该元素是否在数组中重复出现的话,我们可以将遍历到的元素存储到一个新数组newArr中,然后每遍历arr中的一个元素时,将arr[i]newArr中的每一个元素进行比较,如果遍历完了newArr之后,该元素没有重复则将arr[i]插入newArr中,否则就break,进而判断arr数组中的下一个元素。

下面来看一下代码:

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
    }
    
    // 下面判断条件的意思是j与newArr长度一致代表遍历完了newArr并且arr[i]没有出现在NewArr中
    // 否则的话就代表没有遍历完newArr中有和arr[i]重复的元素
    if (j === newArr.length) newArr.push(arr[i])
  }
  return newArr
}

console.log(unique(arr));

// 输出:
// [1, 2, 3, 4]

tip:上面代码for循环中的j得用var定义不可以用let否则for循环外面访问不到j

时间复杂度

下面我们来看一下这段代码的时间复杂度,由于这段代码用了两个for循环嵌套使用,arr中有n个元素,当newArr中元素也为n时,进行双重遍历那么就需要O(n^2)的时间复杂度

2. for + indexOf —— O(n^2)

在看完暴力解决数组去重后,我们会想这代码写的一大坨,那能不能让代码更简洁一点?可以的,下面我们来看看for循环和indexOf组合的方式。

在js这门语言当中,官方为了方便我们查找某个变量时,特意为我们打造了一个indexOf方法,这个方法可以用来判断数组中元素是否存在,下面我们来简单看看使用:

indexOf使用:如果找到该元素则返回下标,否则返回-1

下面根据这一特点可以将双重for循环的那个内存循环给优化,那么如何优化呢?

首先我们来看看内存循环的本质:就是为了查找newArr数组中是否有arr[i],有则break,没有则插入,所以我们可以用indexOf来进行判断,下面来看看优化后的代码:

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

function unique(arr) {
  let newArr = []
  for (let i = 0; i < arr.length; i++) {
    // 当arr[i]不存在newArr中时indexOf返回-1
    if (newArr.indexOf(arr[i]) === -1) newArr.push(arr[i])
  }
  return newArr
}

console.log(unique(arr));
// 输出:
// [1, 2, 3, 4]

时间复杂度

这时候有同学可能会想,我们用indexOf()方法将内层循环给优化了,那么时间复杂度是不是就变成O(n)了呢? 不是的,我们得知道indexOf方法是如何执行的,其实它也是将数组遍历一遍从而查找到该元素是否存在于数组中,而这个过程就相当于内层又加了一层for循环,所以时间复杂度还是O(n^2)

3. for + includes —— O(n^2)

随着时代的发展,我们所使用的方法也在更新迭代,而随着es6的更新,数组中除了上文中的indexOf方法可以用来判断元素是否存在于数组中,官方又新增了一个includes()方法同样可以用来判断数组中是否存在该元素。

includes()使用:如果存在该元素则返回true,否则返回false

下面我们对for + indexOf这个方法修改一下,就是换汤不换药,还是原来的配方还是熟悉的味道:

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

function unique(arr) {
  let newArr = []
  for (let i = 0; i < arr.length; i++) {
    // 如果不存在newArr则插入,这时候includes会返回false得用取反才会为真
    if (!newArr.includes(arr[i])) newArr.push(arr[i])
  }
  return newArr
}
console.log(unique(arr));
// 输出:
// [1, 2, 3, 4]

大家可以看看上面代码,和for + indexOf这种方法几乎一模一样只是换了个判断条件罢了。

时间复杂度

大家可能会想这个方法不就是indexOf换了个衣服,换了个返回形式吗,那时间复杂度是不是也一样的呢?其实大家想的没错,它同样也是对数组进行遍历然后返回结果,所以它的时间复杂度同样也是O(n^2)

4. filter + sort —— O(nlogn)

在了解完了前面数组中的两种方法用来去重之后,我们想想它的时间复杂度能不能再进行缩短一点呢,怎么一直是O(n^2),我们下面来想想它时间复杂度这么高的本质。

上面几种方法时间复杂度之所以这么高,主要是两个for循环嵌套。当数据足够大的时候,它会循环很多次,那么如果我们只使用两次遍历就得出结果,时间复杂度不就可以降低了吗。那么我们如何才能只使用几次遍历就得出结果呢,这时候我们就不能用原来的暴力思想了,我们来换种思路,毕竟条条大路通罗马嘛,有捷径为啥不走呢。

下面我们来想想,因为原数组是无序的,所以我们每次查找元素都需要从头查找,那么我们如果是有序排列呢,下面来看看如果将数组有序排列之后的样子:

// 排列前
const arr = [1, 2, 3, 4, 2, 1]
// 排列后
const arr = [1, 1, 2, 2, 3, 4]

当我们把数组排列之后我们可以发现,如果我们再要对数组进行遍历查找重复元素时,我们只需要判断当前元素与后面那个元素是否相等。如果arr[i] === arr[i + 1],那就是重复了,直接跳过。如果arr[i] !== arr[i + 1],那就是没有重复,就将arr[i]放入新数组中即可。那这时,我们不就省去了里面那层for循环降低了时间复杂度嘛。

有了这个思路之后,我们先来简单了解一下js官方为数组所创建的排序方法sort()和数组的过滤filter()

sort():sort不传入参数那么就默认将原数组升序排列,如果想要对数组进行降序排列那就要传入回调函数sort((a, b) => b - a)。如果想要升序排序建议还是传入回调函数sort((a, b) => a - b),当数据过多不传入回调则会排序出错。

filter():filter同样可以传入一个回调函数,并且会按照符合回调函数return条件的元素返回一个新数组。 它的回调函数中有三个参数item(元素),index(下标),array(原数组),下面我们来看一下如果要用filter返回数组中小于1的元素该如何写arr.filter(item => item < 1)。参数如果需要的话可以写,不要可以不写,但是第一个参数item(也可以自定义变量名)一定要写。

在了解完了这两个方法之后,下面我们根据上面的思路先对数组用sort()进行排序,然后用filter()对没有重复出现的元素进行返回,下面来看实现代码:

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

function unique(arr) {
  // 用新数组来接收原数组的内容从而不对原数组进行修改
  let newArr = [...arr](...是es6新增的展开运算符,可以展开arr)
  return newArr.sort((a, b) => a - b).filter((item, index, array) => {
    // 当数组排序之后,第一个元素肯定是可以存入的不管重复与否,
    // 然后在遍历后面的元素时,如果当前元素item与前一个元素array[index - 1]不等就代表不重复可以存入
    return index === 0 || item !== array[index - 1]
  })
}
console.log(unique(arr));

// 输出:
// [1, 2, 3, 4]

我们通过观察上面代码可以发现newArr可以直接替换为[...arr],根据这一特点我们可以对代码再次进行简化:

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

function unique(arr) {
  return [...arr].sort((a, b) => a - b).filter((item, index, array) => {
    // 这里把index === 0替换成了!index效果是一样的,因为如果index === 0那么取反就为真
    return !index || item !== array[index - 1]
  })
}
console.log(unique(arr));

// 输出:
// [1, 2, 3, 4]

时间复杂度

这段代码有的同学可能会说这不是O(n)吗,只遍历了一次,但是我们得记住,在遍历之前我们还用sort进行了排序,而sort主要是采取了快速排序sort排序的时间复杂度为O(nlogn),所以后面filter虽然是O(n),但是我们还是将总的时间复杂度记为较高的那个,所以这种方法的时间复杂度是O(nlogn)

5. filter + {} —— O(n)

在js这门语言中对象是一个非常特殊的数据结构,它的key都是唯一的,当我们重复设置key时,它会进行覆盖。那么我们根据这一特点是不是也能把它用来进行数组去重,下面我们来看一段例子:

let obj = {
  name: 'zf',
  name:'dj',
  age: 18
}
console.log(obj.name);

// 输出:
// dj

我们可以看到后面name的值将前面的覆盖了,而在Object这个对象中有一个方法keys()可以将对象中的键全部取出,下面来看个例子:

const obj = {
  1: 0,
  2: 1,
  3: 2,
  1: 3,
  2: 4
}
console.log(Object.keys(obj));
//因为对象中不能有相同的key所以会覆盖,对象的key只能是字符串,所以会自动转换成字符串

image.png

上面代码我们可以看到这个方法会将key都储存在数组中并且返回,而key都是以字符串的形式所储存的,那么我们可不可以将其中的每个元素转化为数字呢?我们为了将其中的元素转化为数字,可以使用map方法来处理keys()返回的数组。下面我们来看看如何使用对象来进行数组去重:

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

function unique(arr) {
  let obj = {}// 用来储存不重复的元素
  
  for (let i = 0; i < arr.length; i++) {
    // 如果对象中该元素不存在则将其加入obj中,并且赋值为true
    if (!obj[arr[i]]) obj[arr[i]] = true
  }
  
  //先用keys()获取obj中的key,然后用map将元素转化为数字
  return Object.keys(obj).map(Number)//map中传入Number可以将数组中的元素给转变为Number类型

}
console.log(unique(arr));

image.png

我们可以发现用这种方法对原数组进行遍历同样可以对数组去重,但是我们这一小节的标题是filter,我们根据上文中判断obj中是否有该元素这个条件,可以将这个条件加入到filter中,下面我们来看看用filter + {}是如何实现的:

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

function unique(arr) {
  let obj = {}// 用来储存不重复的元素
  
  return arr.filter((item) => {
    //如果该元素在对象中有的话那就跳过,否则的话就将其加入新数组,并且放入obj中
    return obj[item] ? false : obj[item] = true
  })

}
console.log(unique(arr));

image.png

时间复杂度

上面代码我们可以看到对代码只进行了一次遍历,时间复杂度是不是O(n)呢?确实是O(n)的时间复杂度,这时候就有同学会问了,我们从obj中获取元素不会耗时吗?

在这里我们就得来简单聊聊js中的对象了,在js中对象(或者叫其为哈希表)的取值类似于数组,数组是根据索引取值,而对象是根据key来取值,它们的时间复杂度都是O(1)的。所以说我们就相当于是对其遍历一遍就行,这样的话时间复杂度就是O(n)

6. Set —— O(n)

在前面我们通过使用对象这种数据结构来实现数组去重之后,那么js中有没有一种数据结构可以实现其中只包含不重复的元素呢?js官方设计了一种数据结构Set(集合),在这个数据结构中不包含重复元素,下面我们来看看如何使用Set:

let s = new Set()
s.add(1)// Set通过add往其中添加元素
s.add(2)
s.add(3)
s.add(1)

console.log(s)

image.png

我们可以看到当我们使用new创建了一个Set集合并且往其中添加了1,2,3,1后,在打印的时候我们只打印出来了1,2,3。那个重复出现的1被删除了,根据这一特点我们可以用来进行数组去重。下面我们来看看如何利用Set进行去重:

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

image.png

在这里大家可以看到代码已经是非常的优雅,有同学就懵了。ber哥们,就一行就出来了?下面我们来解释一下。

当我们创建一个Set的时候,同时可以传入一个参数arr,这样我们新创建的Set内部就会有arr里的元素并且会对其进行去重操作。

当我们去重过后我们得到的并不是一个数组,而是Set这个数据结构自己的一种存储方式,这时候我们用展开运算符将其中的元素展开放入新的数组中并且返回。

时间复杂度

在我们用Set进行数组去重时,我们会先对数组依次遍历然后放入Set中,然后再对Set进行解构并且将解构后的元素依次放入新数组中,在这里我们可能会说它的时间复杂度是O(2n),当我们n趋近于无穷大时2n -> n,所以这种方式的时间复杂度是O(n)

以上六种方式就是数组去重的主要方式,感谢各位的观看!

1732338928918.jpg