面试高频考点——数组去重和数组扁平化

有一个多月没写文章了,关注者数量突破了100,在文章开始之前,衷心感谢这100多位关注者!

数组去重和数组扁平化都是实际开发和面试中会遇到的问题,今天我就小小总结一下解决方法!

一、数组去重

1. Set

使用Set数据结构:Set是ES6中的一种数据结构,它只存储唯一的值。通过将数组转换为Set,然后再将Set转换回数组,就可以实现数组去重。可去原始数据类型的重复值,对引用类型的数据无能为力。

js 复制代码
// 原始类型
const arr = [1, 2, 2, 5, 6, 6, 6, "abc", "abc", null, null, undefined, undefined];
const uniqueArray = [...new Set(arr)];
const uniqueArray2 = Array.from(new Set(arr));
console.log(uniqueArray); // [1, 2, 5, 6, 'abc', null, undefined]
console.log(uniqueArray2); // [1, 2, 5, 6, 'abc', null, undefined]

// 引用类型
const arr2 = [{a:1, b:2}, {a:1, b:2}, [1,2,3], [1,2,3]];
const uniqueArray3 = [...new Set(arr2)];
const uniqueArray4 = Array.from(new Set(arr2));
console.log(uniqueArray3); // [{ a: 1, b: 2 }, { a: 1, b: 2 }, [ 1, 2, 3 ], [ 1, 2, 3 ]]
console.log(uniqueArray4); // [{ a: 1, b: 2 }, { a: 1, b: 2 }, [ 1, 2, 3 ], [ 1, 2, 3 ]]

2. 双重for循环

最朴实无华的方法,不借助什么数组API或数据结构,其中判断条件可以是arr[i] === arr[j]Object.is(arr[i], arr[j])。可去原始数据类型的重复值,对引用类型的数据无能为力。

js 复制代码
const arr = [1, 2, 2, 5, 6, 6, 6, "abc", "abc", null, null, undefined, undefined];
const arr2 = [{a:1, b:2}, {a:1, b:2}, [1,2,3], [1,2,3]];
const removeRepeat = (arr) => {
  for(let i = 0; i < arr.length; i++){
    for(let j = i + 1; j < arr.length; j++){
      if(arr[i] === arr[j]){
        arr.splice(j, 1);
        j--;
      }
    }
  }
  return arr;
}
console.log(removeRepeat(arr)); // [1, 2, 5, 6, 'abc', null, undefined]
console.log(removeRepeat(arr2));// [{ a: 1, b: 2 }, { a: 1, b: 2 }, [ 1, 2, 3 ], [ 1, 2, 3 ]]

3. Object.keys()

通过遍历数组,将值添加到对象的属性中,由于对象不能添加相同属性,因此可以自动去重。最后通过Object.keys()返回对象可枚举属性组成的数组。最后输出的键名都是字符串类型。

js 复制代码
// 原始类型
const arr = [1, 2, 2, 5, 6, 6, 6, "abc", "abc", null, null, undefined, undefined];
let obj = {};  
for (let i = 0; i < arr.length; i++) {  
  obj[arr[i]] = arr[i];
}  
let uniqueArray = Object.keys(obj);
console.log(uniqueArray); // ['1', '2', '5', '6', 'abc', 'null', 'undefined']

// 引用类型
const arr2 = [{a:1, b:2}, {a:1, b:2}, [1,2,3], [1,2,3]];
let obj2 = {};  
for (let i = 0; i < arr2.length; i++) {  
  obj2[arr2[i]] = arr2[i];
}  
let uniqueArray2 = Object.keys(obj2);
console.log(uniqueArray2); // ['[object Object]', '1,2,3']

4. filter + indexOf

使用 Array.prototype.filter() 方法:使用filter方法遍历数组,通过判断元素第一次出现的索引是否与当前索引相等,来保留第一次出现的元素。因为indexOf方法只会返回首次符合条件元素的下标。可去原始数据类型的重复值,对引用类型的数据无能为力。

js 复制代码
// 原始类型
const arr = [1, 2, 2, 5, 6, 6, 6, "abc", "abc", null, null, undefined, undefined];
const uniqueArray = arr.filter((value, index, self) => {
  return self.indexOf(value) === index;
});
console.log(uniqueArray); // [1, 2, 5, 6, 'abc', null, undefined]
console.log(arr.indexOf(2)); // 1
console.log(arr.indexOf(6)); // 4

// 引用类型
const arr2 = [{a:1, b:2}, {a:1, b:2}, [1,2,3], [1,2,3]];
const uniqueArray2 = arr2.filter((value, index, self) => {
  return self.indexOf(value) === index;
});
console.log(uniqueArray2); // [{ a: 1, b: 2 }, { a: 1, b: 2 }, [ 1, 2, 3 ], [ 1, 2, 3 ]]

5. reduce + includes

使用 Array.prototype.reduce() 方法:使用reduce方法遍历数组,reduce方法的回调函数有两个参数:累加器和当前元素。在这里,累加器是一个数组,初始值是一个空数组[]。在每次迭代时,我们检查当前元素是否已经存在于累加器数组中,如果不存在则将当前元素添加到累加器数组中。可去原始数据类型的重复值,对引用类型的数据无能为力。

js 复制代码
// 原始类型
const arr = [1, 2, 2, 5, 6, 6, 6, "abc", "abc", null, null, undefined, undefined];
const uniqueArray = arr.reduce((accumulator, current) => {
  if (!accumulator.includes(current)) {
    accumulator.push(current);
  }
  return accumulator;
}, []);
console.log(uniqueArray); // [1, 2, 5, 6, 'abc', null, undefined]

// 引用类型
const arr2 = [{a:1, b:2}, {a:1, b:2}, [1,2,3], [1,2,3]];
const uniqueArray2 = arr2.reduce((accumulator, current) => {
  if (!accumulator.includes(current)) {
    accumulator.push(current);
  }
  return accumulator;
}, []);
console.log(uniqueArray2); // [{ a: 1, b: 2 }, { a: 1, b: 2 }, [ 1, 2, 3 ], [ 1, 2, 3 ]]

6. 对象数组去重

前面说的都是普通数组,如果遇到对象数组,那么阁下又该如何应对呢?

js 复制代码
let user = {id: "001", name: "张三", age: 20};
let user2 = {id: "001", name: "张三", age: 20};

两个对象的地址不同,所以无论是两个等号还是三个等号还是Object.is去判断,结果都是false,可是很明显他们的主键一样,属性名和属性值都相同,就是同一个用户,应该被去重。

6.1 方法一

利用ES6的map数据结构

js 复制代码
const userArray = [
  { id: 1, name: 'Bill', age: 20 },
  { id: 2, name: 'Lucy', age: 18 },
  { id: 1, name: 'Bill', age: 20 },
  { id: 3, name: 'Jack', age: 21 },
  { id: 3, name: 'Jack', age: 21 },
];
function removeRepeat(arr){
  const result = [];
  const map = new Map();
  arr.forEach(item => {
    if(!map.has(item.id)){
      result.push(item);
      map.set(item.id,true);
    }
  });
  return result;
}
console.log(removeRepeat(userArray));
// [
//   { id: 1, name: 'Bill', age: 20 },
//   { id: 2, name: 'Lucy', age: 18 },
//   { id: 3, name: 'Jack', age: 21 }
// ]

利用reduce、map、includes等数据API

js 复制代码
const uniqueArray = userArray.reduce((accumulator, current) => {
  const ids = accumulator.map(item => item.id);
  if (!ids.includes(current.id)) {
    accumulator.push(current);
  }
  return accumulator;
}, []);
console.log(uniqueArray);
// [
//   { id: 1, name: 'Bill', age: 20 },
//   { id: 2, name: 'Lucy', age: 18 },
//   { id: 3, name: 'Jack', age: 21 }
// ]

以上两种方法都是去判断数据的主键是否相同,如果主键相同,则认为这两条数据是相同的。那么我想写一个更普遍适用的版本应该怎么做呢?

6.2 方法二

说白了,就是自己手写判断两个数据是否相同的规则,去遍历对象的每个属性和属性值,看他们是否相同,键名和键值都相同才认为他们相等。

js 复制代码
// 测试数据
const arr = [
  1,1,6,6,6,
  {a:1, b:{c:2}, d:undefined},
  {a:1, b:{c:2}, d:undefined},
  {a:1, b:2, c:undefined},
  {a:1, b:2, d:undefined},
  {a:1, b:2, c:3, d:4},
  {a:1, b:2, c:3, d:4},
  [1, 2, 3, [8, 666]],
  [1, 2, 3, [8, 666]]
]
js 复制代码
// 判断是否为原始数据类型
function isprimitive(val){
  if(val === null || (typeof val !== "object" && typeof val !== "function")){
    return true;
  }
  return false;
}

// 判断两个数据是否相同
function equals(val1, val2) {
  // 如有原始类型数据
  if(isprimitive(val1) || isprimitive(val2)){
    return Object.is(val1, val2);
  }
  // 获取键值对
  const entries1 = Object.entries(val1);
  const entries2 = Object.entries(val2);
  if(entries1.length !== entries2.length){
    return false;
  }
  // 比较键名和键值
  for(const [key, value] of entries1){
    if(!(key in val2) || !equals(value, val2[key])){
      return false;
    }
  }
  return true;
}

// 遍历数组,判断数组元素是否相同
function uniqueArray(arr){
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    let isDuplicate = false;
    for (let j = 0; j < result.length; j++) {
      if (equals(arr[i],result[j])) {
        isDuplicate = true;
        break;
      }
    }
    if (!isDuplicate) {
      result.push(arr[i]);
    }
  }
  return result;
}
console.log(uniqueArray(arr));
// [
//   1,
//   6,
//   { a: 1, b: { c: 2 }, d: undefined },
//   { a: 1, b: 2, c: undefined },
//   { a: 1, b: 2, d: undefined },
//   { a: 1, b: 2, c: 3, d: 4 },
//   [ 1, 2, 3, [ 8, 666 ] ]
// ]

二、数组扁平化

数组扁平化即将多维数组转换为一维数组。

1. flat

flat() 方法创建一个新的数组,并根据指定深度递归地将所有子数组元素拼接到新的数组中。

flat(depth),其中depth是可选参数,表示要提取嵌套数组的结构深度,默认值是1。如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。

js 复制代码
// flat(depth)
const arr1 = [1, 2, [3, 4]];
console.log(arr1.flat()); // [1, 2, 3, 4]

const arr2 = [1, 2, [3, 4, [5, 6]]];
console.log(arr2.flat()); // [1, 2, 3, 4, [5, 6]]

const arr3 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]];
console.log(arr3.flat(4));  // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
console.log(arr3.flat(Infinity)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

如果待展开的数组是稀疏的,flat() 方法会忽略其中的空槽。例如,如果 depth是1,那么根数组和第一层嵌套数组中的空槽都会被忽略,但在更深的嵌套数组中的空槽则会与这些数组一起保留。

js 复制代码
const arr4 = [1, 2, , 4, 5];
console.log(arr4.flat()); // [1, 2, 4, 5]

const arr5 = [1, , 3, ["a", , "b"]];
console.log(arr5.flat()); // [1, 3, 'a', 'b']

const arr6 = [1, , 3, ["a", ["b", , "c"]]];
console.log(arr6.flat()); // [ 1, 3, 'a', [ 'b', empty, 'c' ] ]

2. 递归

遍历数组每个元素,如果元素是数组,则递归扁平化后拼接到累积数组中;如果元素不是数组,则直接加入累积数组中,这里还可以设置扁平化的深度

js 复制代码
let arr = [1, [2, 3], [4, [5, 6, [7, 8]]]];
function flatten(arr, depth) {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i]) && depth > 0) {
      result = result.concat(flatten(arr[i], depth - 1));
    } else {
      result.push(arr[i]);
    }
  }
  return result;
}
console.log(flatten(arr, 1)); // [ 1, 2, 3, 4, [ 5, 6, [ 7, 8 ] ] ]
console.log(flatten(arr, 2)); // [ 1, 2, 3, 4, 5, 6, [ 7, 8 ] ]
console.log(flatten(arr, 3)); // [ 1, 2, 3, 4, 5, 6, 7, 8 ]

3. reduce

使用reduce方法可以将数组逐个处理,并将结果累积到一个新数组中。在每次迭代中,如果当前元素是数组,则递归调用扁平化函数,并将结果连接到累积数组中;否则,将当前元素添加到累积数组中。

js 复制代码
let arr = [1, [2, 3], [4, [5, 6, [7, 8]]]];
function flatten(arr) {
  return arr.reduce((acc, cur) => {
    if (Array.isArray(cur)) {
      return acc.concat(flatten(cur));
    } else {
      return acc.concat(cur);
    }
  }, []);
}
console.log(flatten(arr)); // [1, 2, 3, 4, 5, 6, 7, 8]

4. 扩展运算符

使用扩展运算符可以将数组展开为一个新数组。在每次迭代中,如果当前元素是数组,则递归调用扁平化函数,并将结果展开;否则,将当前元素添加到结果数组中。

js 复制代码
let arr = [1, [2, 3], [4, [5, 6, [7, 8]]]];
function flatten(arr) {
  return arr.reduce((acc, cur) => {
    if (Array.isArray(cur)) {
      return [...acc, ...flatten(cur)];
    } else {
      return [...acc, cur];
    }
  }, []);
}
console.log(flatten(arr)); // [1, 2, 3, 4, 5, 6, 7, 8]

5. 非递归(栈)

如果想使用非递归的方式实现数组扁平化,可以使用循环和栈来处理数组的每个元素。

js 复制代码
const arr = [1, 2, [3, [4, 5, 6, [7, 8, [9, 10]]]]];
function flatten(arr) {
  const result = [];
  const stack = [...arr];
  while (stack.length) {
    const next = stack.pop();
    if (Array.isArray(next)) {
      stack.push(...next);
    } else {
      result.unshift(next);
    }
  }
  return result;
}
console.log(flatten(arr)); //[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

将原始数组的元素都放入栈中。然后,循环遍历栈,每次弹出栈顶元素。如果弹出的元素是数组,就将其展开并放入栈中;如果是普通元素,就将其添加到结果数组的开头。

三、最后的话

能力一般,水平有限,本文可能存在纰漏或错误,如有问题欢迎指正,感谢你阅读这篇文章,如果你觉得写得还行的话,不要忘记点赞、评论、收藏哦!祝生活愉快!

相关推荐
霸气小男几秒前
react + antDesign封装图片预览组件(支持多张图片)
前端·react.js
susu10830189111 分钟前
前端css样式覆盖
前端·css
学习路上的小刘2 分钟前
vue h5 蓝牙连接 webBluetooth API
前端·javascript·vue.js
&白帝&3 分钟前
vue3常用的组件间通信
前端·javascript·vue.js
小白小白从不日白14 分钟前
react 组件通讯
前端·react.js
罗_三金24 分钟前
前端框架对比和选择?
javascript·前端框架·vue·react·angular
Redstone Monstrosity31 分钟前
字节二面
前端·面试
东方翱翔38 分钟前
CSS的三种基本选择器
前端·css
Fan_web1 小时前
JavaScript高级——闭包应用-自定义js模块
开发语言·前端·javascript·css·html
yanglamei19621 小时前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask