数组去重?就这

801 阅读5分钟

Tip:下面一些方法的缺点有些我写到了不能过滤引用数据类型的数据,这里主要指的是地址不同的情况。不过这种情况一般不需要考虑,如果真的想要去掉,可以自己尝试使用递归进行判断

  1. 利用Set数据结构

Set是ES6为我们提供的数据结构,它类似于数组,但是其具有唯一性,与数组最大的区别就是成员的值都是唯一的,没有重复的值。所以经常会被利用此特性进行数组去重

  • 使用方法:首先通过new Set(arr)的方式创建一个set数据结构,我们只需要将需要进行去重的数组作为参数传递给Set构造函数即可,Set结构中会自动帮我们清除掉重复的元素,在它的所有元素中是不含重复元素的;随即结合使用扩展运算符(可以使Set结构转成普通的数组)就可以得到去重过的数组
  • 该方法并不会改变原数组
  • 优点:操作简洁方便,是开发中常用的方式。该方法可以顾虑到重复的基本数据类型(undefined、null、number、boolean、string)对应的数据以及NaN,返回的是去重后的新数组
  • 缺点:存在兼容性问题,不支持IE,但其他主流浏览器都支持,但不能过滤重复的引用数据类型的数据
const newArr = new Set([1,1,2,2,3,3,3,5,6])
console.log([...newArr]);
  1. 利用forEach循环遍历数组
  • 使用方法,利用数组的forEach方法结合Es6includes方法
  • 这种方法的思想就是新建一个数组,循环遍历原数组,当值判断出不在新数组中时就将该值push进新数组中
  • 优点:该方法可以顾虑到重复的 基本数据类型对应的数据外加NaN,返回的是去重后的新数组
  • 缺点:不能过滤掉引用数据类型的数据
const arr = [1, 2, 3, 3, 4, 4, 5, 8, 8, 9]
const newArr = []
arr.forEach((item, index) => {
  if (!newArr.includes(item)) { // includes方法用于判断元素是否存在于数组中,如果存在则返回true,否则返回false
    newArr.push(item)
  }
})
console.log(newArr);

// 当然循环的方法不只有forEach一种,还可以使用reduce进行遍历
const arr = [1, 2, 3, 3, 4, 4, 5, 8, 8, 9]
const newArr = arr.reduce((init, curr) => {
  if (!init.includes(curr)) { // includes方法用于判断元素是否存在于数组中,如果存在则返回true,否则返回false
    init.push(curr)
  }
  return init
}, [])
console.log(newArr); // [1, 2, 3, 4, 5, 8, 9]
  • 当然该判断条件也可以用ES5的知识比如说newArr.indexOf(array[i]) === -1或者newArr.indexOf(array[i]) === index来代替
  • 思想和上面使用includes方法类似,都是比较该元素有没有被放到新数组中,如果没有则push进去,如果有则跳过该元素
  • 注意:该方法类比includes,有个很大的区别就是不能过滤掉NaN,其他基本是一样的
  • 缺点:不能过滤掉引用数据类型的数据和NaN (indexOf找不到NaN的下标,所以会返回-1,遇到NaN就会把它加入到新数组中)
let arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN','NaN', 0, 0, 'a', 'a',{},{}];
function distinct (arr) {
  let newArr = []
  arr.forEach((item, index) => {
    if(newArr.indexOf(item) === -1) { // indexOf方法可以返回数组中第一个与参数相等的元素对应的下标值,如果找不到则返回-1
      newArr.push(item)
    } 
  });
  return newArr
}
console.log(distinct(arr)) //  [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]

// 使用ES5的形式进行数组的遍历
const uniqueArr = arr => {
  const newArr = arr.reduce((init, curr) => {
    if (init.indexOf(item) === -1)) { // indexOf方法会返回数组中第一个与item相等元素的下标,这样只有第一个不重复的元素才能匹配到并push进新数组中
      init.push(item)
    }
    return init
  });
  return newArr
}

console.log(uniqueArr(arr));//  [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]
  1. 双重for循环实现数组去重
  • 实现思路:利用双重for循环结合数组的splice方法实现去重,当判断到有重复的元素时,则需要删除后面那个元素,这样循环了len - 1轮之后,所有的元素都被去重完毕了
  • 优点:这个方法比较传统但是其兼容性较好,可以顾虑到重复的 StringBooleanNumberundefinednull,返回的是去重后的原数组
  • 缺点:时间复杂度过高为O(n^3)(因为在双重for循环下,splice方法的时间复杂度又是O(n)),不能过滤掉 NaNObject。原因:NaN === NaN返回的是false,重复的NaN自然不会被splice方法过滤掉;类型为object的引用值比较的是地址,自然也不会被过滤掉

正序遍历循环:

  • 注意:每次在循环中删除掉元素之后,由于splice方法会改变原数组,所以其长度会减一,此时j需要自减,下一个循环才能够正确对应到要审查的元素
const arr = [1, 2, 2, 4, 5, 8, 8, 8, 8, 9, 9, 10, 2, 4, 4, 9]
for (let i = 0; i < arr.length - 1; i++) { // 这里arr.length没有放到循环开始时定义是因为数组的长度可能会发生改变,需要避免一些没有意义的循环
  for (let j = i + 1; j < arr.length; j++) {
    if (arr[i] === arr[j]) {
      arr.splice(j, 1) // 注意:splice方法是会改变原数组的这里每次删除完一个元素之后,后面的元素都会往前移一位
      j-- // 因为下次循环前j要自增,如果现在j不自减的话,那么下次遍历的时候就会跳过一个要审查的元素
    }
  }
}

console.log(arr);

逆序遍历循环:

注意:和正序遍历有所不同,因为是从数组的后方发起的,当splice方法生效后,下一次循环对应的恰好为要审查的元素,所以这里就不需要让j自减,但是需要让i自减

const arr = [1, 2, 2, 4, 5, 8, 8, 8, 8, 9, 9, 10, 2, 4, 4, 9]
for (let i = arr.length - 1; i > 0; i--) {
  for (let j = i - 1; j > -1; j--) {
    if (arr[i] === arr[j]) {
      arr.splice(j, 1)
      i--
    }
  }
}
console.log(arr);
  1. 利用ES6的Map数据结构的特性进行去重
  • 使用方法: 我们需要提前创造一个Map对象和一个空数组,我们需要存储数组中每一个元素到Map结构的key中,由于其key具有唯一性,所以在key中的值都是唯一的,每次循环时只需要调用其has方法判断Map中有没有对应的key就可以知道该元素需不需要push到新数组中
  • 优点:该方法可以顾虑到重复的基本数据类型 String、Boolean、 Number、undefined、null外加让人头疼的NaN,返回的是去重后的新数组
  • 缺点:不能过滤重复的Object

注意:如果使用Map存储了引用值,那么其存储的只是其引用即内存地址

let arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN','NaN', 0, 0, 'a', 'a',{},{}];
function distinct(arr) {
  let map = new Map() // Map的key具有唯一性,所以我们可以利用该特性进行数组去重,而且它还可以识别出相同的NaN
  let newArr = []
  for(let i = 0; i < arr.length; i++) {
    if(!map.has(arr[i])) {
      map.set(arr[i])
      newArr.push(arr[i])
    }
  }
  return newArr
}
console.log(distinct(arr)) // [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]
  1. 利用对象的属性key唯一的特性去重
  • 缺点:针对 NaN和'NaN'会视为一个key,类似的还有true和'true',undefined和'undefined'等等,所以这两些不同的元素最终只能剩下一个,属于误判; 对象的key也会视为一个key,无论你放进去的是什么对象,它都会先转成字符型[object Object],所以其实该种方法也没有达到真正的去重
let arr = [1,1,'true','true',true,true,15,15,false,false, 'undefined',undefined, null,null, NaN, NaN,'NaN','NaN', 0, 0, 'a', 'a',{},{}];
function distinct(arr) {
  let obj = {}
  let newArr = []
  for(let i = 0; i < arr.length; i++) {
    if(!obj[arr[i]]){
      obj[arr[i]] = 1
      newArr.push(arr[i])
    }
  }
  return newArr
}
console.log(distinct(arr)) // [1, "true", 15, false, undefined, null, NaN, 0, "a", {…}] 
  1. 利用filter循环数组进行过滤
  • 使用方法:使用filter遍历数组,在循环中结合indexOf方法判断元素有无重复,如果与index的值不等,则说明其是重复元素,返回false,不会加入到新数组中;反之亦然
  • 缺点:indexOf源码中使用的===符号进行比较,所以其不能得到NaN的下标,所以如果原数组中存在有NaN,那么其将会损失NaN这个元素
let arr = [1,1,'true','true',true,true,15,15,false,false, 'undefined',undefined, null,null, NaN, NaN,'NaN','NaN', 0, 0, 'a', 'a',{},{}];
function distinct(arr) {
 return arr.filter((item, index) => {
  return arr.indexOf(item) === index;
});
}
console.log(distinct(arr)) // [1, 'true', true, 15, false, 'undefined', undefined, null, 'NaN', 0, 'a', {…}, {…}]