编者荐语:
在前端面试中,手写flat
和手写数组去重
是非常基础的面试题,通常出现在笔试或者第一轮面试中,主要考察面试者基本的手写代码能力和JavaScript
的基本功。
今天就带大家从0了解flat
特性到手写实现flat
和数组去重
的N种思想,再到接住面试官的连环追问中重新学习一遍数组去重和扁平化
的技巧。
前言:
在开头,我们先来介绍Array.prototype.flat()
特性,到后面再为大家详细介绍它的原理手写。
介绍完数组扁平化技巧之后,接着为大家讲解数组去重的N
种思想。
PS:这两道题是面试高频手写题,希望大家耐心看完哦~
一段代码总结Array.prototype.flat()
特性
注:数组拍平方法
Array.prototype.flat()
也叫数组扁平化、数组拉平、数组降维。
let arr = [12, 23, [34, 56, [78, 90, 100, [110, 120, 130]]]];
console.log(arr.flat());
// [ 12, 23, 34, 56, [ 78, 90, 100, [ 110, 120, 130 ] ] ]
console.log(arr.flat(2));
// [ 12, 23, 34, 56, 78, 90, 100, [ 110, 120, 130 ] ]
console.log(arr.flat(Infinity));
// [12, 23, 34, 56, 78, 90, 100, 110, 120, 130]
console.log(arr.flat(0));
// [12, 23, [34, 56, [78, 90, 100, [110, 120, 130]]]];
console.log(arr.flat(-10));
// [12, 23, [34, 56, [78, 90, 100, [110, 120, 130]]]];
let arr2 = [12, 23, [34, 56, ,]]
console.log(arr.flat());
// [ 12, 23, 34, 56 ]
Array.prototype.flat()
特性总结
-
Array.prototype.flat()
用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。 -
不传参数时,默认“拉平”一层,可以传入一个整数,表示想要“拉平”的层数。
-
传入
<=0
的整数将返回原数组,不“拉平” -
Infinity
关键字作为参数时,无论多少层嵌套,都会转为一维数组 -
如果原数组有空位,
Array.prototype.flat()
会跳过空位。
面试官 N 连问:
第一问:如何对下面数组实现扁平化?
let arr = [
[1, 2, 2],
[3, 4, 5, 5],
[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10
];
小伙伴首先想到的肯定是用 ES6 的Array.prototype.flat
方法呀
方法一:flat
arr = arr.flat(2);
// [ 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, [ 12, 13, [ 14 ] ], 10 ]
当flat
中传入数字时,是扁平对应的层数,显然这不是我们想要的,因为它还没有完全展开。
这是,flat
函数中就为我们提供了一个参数Infinity
,译为无穷
的意思。
arr = arr.flat(Infinity);
/* [
1, 2, 2, 3, 4, 5, 5,
6, 7, 8, 9, 11, 12, 12,
13, 14, 10
] */
当我们不知道数组中嵌套了几维数组时,我们可以用Infinity
这个参数,帮我们全部展开。
第二问:还有其它的办法吗?因为它们在高版本浏览器并不兼容
方法二:转换为字符串,再把字符串对象用 , 转换成数组
可以先把多维数组
先转换为字符串
,再基于,
分隔符将字符串对象
分割成字符串数组
toString()
扁平化数组
arr = arr.toString();
// "1,2,2,3,4,5,5,6,7,8,9,11,12,12,13,14,10"
arr = arr.toString().split(',');
// ["1", "2", "2", "3", "4", "5", "5", "6", "7", "8", "9", "11", "12", "12", "13", "14", "10"]
arr = arr.toString().split(',').map(item => parseFloat(item));
// [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
除了上面的方法还有什么方法转换为字符串呢?
JSON.stringify()
扁平化数组
arr = JSON.stringify(arr);
// "[[1,2,2],[3,4,5,5],[6,7,8,9,[11,12,[12,13,[14]]]],10]"
arr = JSON.stringify(arr).replace(/(\[|\])/g, '');
// "1,2,2,3,4,5,5,6,7,8,9,11,12,12,13,14,10"
arr = JSON.stringify(arr).replace(/(\[|\])/g, '').split(',').map(item=>parseFloat(item));
// [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
方法三:循环验证是否为数组
基于数组的some
方法,只要数组里面有一项元素是数组就继续循环,扁平数组
核心:[].concat(...arr)
whilte (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
console.log(arr); // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
第三问:能自己实现一个 flat 扁平化吗?
先回答:能!!再跟着我分析思路:
如何实现呢,其实思路非常简单:在数组中找到是数组类型的元素,然后将他们展开
,这就是flat
方法的关键思路
实现思路:
- 循环数组里的每一个元素
- 判断该元素是否为数组
- 是数组的话,继续循环遍历这个元素——数组
- 不是数组的话,把元素添加到新的数组中
实现流程:
创建一个空数组,用来保存遍历到的非数组元素
创建一个循环遍历数组的函数,cycleArray
取得数组中的每一项,验证
Array.isArray()
数组的话,继续循环
非数组的话,添加到新数组中
- 返回新数组对象
ES5 实现 flat 扁平化方法
let arr = [
[1, 2, 2],
[3, 4, 5, 5],
[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10
];
function myFlat() {
_this = this; // 保存 this:arr
let newArr = [];
// 循环arr中的每一项,把不是数组的元素存储到 newArr中
let cycleArray = (arr) => {
for (let i=0; i< arr.length; i++) {
let item = arr[i];
if (Array.isArray(item)) { // 元素是数组的话,继续循环遍历该数组
cycleArray(item);
continue;
} else{
newArr.push(item); // 不是数组的话,直接添加到新数组中
}
}
}
cycleArray(_this); // 循环数组里的每个元素
return newArr; // 返回新的数组对象
}
Array.prototype.myFlat = myFlat;
arr = arr.myFlat(); // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
ES6 实现 flat 扁平化方法
const myFlat = (arr) => {
let newArr = [];
let cycleArray = (arr) => {
for(let i = 0; i < arr.length; i++) {
let item = arr[i];
if (Array.isArray(item)) {
cycleArray(item);
continue;
} else {
newArr.push(item);
}
}
}
cycleArray(arr);
return newArr;
}
myFlat(arr); // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
第四问:请使用reduce
实现flat
函数
相信很多面试官都会指定让面试者用reduce
方法实现flat
函数
其实思路也是一样的,在实现之前,先来看一下
它的核心:
[].concat(...arr)
但是它只能将数组元素展开一层,来看下面例子:
let arr2 = [12, 23, [34, 56, [78, 90, 100]]];
[].concat(...arr2);
// [ 12, 23, 34, 56, [ 78, 90, 100 ] ]
细心的同学可以发现[].concat(...arr)
只能展开一层数组元素,当有更深层次的,是无法展开的
接下来,我们来看看用reduce
怎么实现?
let arr = [12, 23, [34, 56, [78, 90, 100, [110, 120, 130, 140]]]];
const myFlat = arr => {
return arr.reduce((pre, cur) => {
return pre.concat(cur);
}, []);
};
console.log(myFlat(arr));
// [ 12, 23, 34, 56, [ 78, 90, 100, [ 110, 120, 130, 140 ] ] ]
const myFlat = arr => {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? myFlat(cur) : cur);
}, []);
};
console.log(myFlat(arr));
// [12, 23, 34, 56, 78, 90, 100, 110, 120, 130, 140]
上面代码中的Array.isArray(cur)
和myFlat(cur)
实际就好比与遍历数组每一项,看它是不是数组元素,
如果是的话,则继续递归遍历,不是的话直接数组合并非数组元素
。
第五问:使用栈的思想实现flat
函数
栈思想:后进先出的数据结构
实现思路:
不断获取并删除栈中最后一个元素
A
,判断A
是否为数组元素,直到栈内元素为空,全部添加到newArr
- 是数组,则
push
到栈中,继续循环栈内元素
,直到栈为空- 不是数组,则
unshift
添加到newArr
中
// 栈思想
function flat(arr) {
const newArr = [];
const stack = [].concat(arr); // 将数组元素拷贝至栈,直接赋值会改变原数组
//如果栈不为空,则循环遍历
while (stack.length !== 0) {
const val = stack.pop(); // 删除数组最后一个元素,并获取它
if (Array.isArray(val)) {
stack.push(...val); // 如果是数组再次入栈,并且展开了一层
} else {
newArr.unshift(val); // 如果不是数组就将其取出来放入结果数组中
}
}
return newArr;
}
let arr = [12, 23, [34, 56, [78, 90, 100, [110, 120, 130, 140]]]];
console.log(flat(arr));
// [12, 23, 34, 56, 78, 90, 100, 110, 120, 130, 140]
介绍完了数组扁平化
的几大技巧之后,来看看下面的数组去重
吧~
数组去重的几大思想
思想一: ES6,Set
去重
思想二: 基于indexOf/includes
判断数组内是否存在重复元素 【操作的都是原数组】
思想三: 新容器存储思想——对象键值对
思想四: 先排序,再相邻比较(基于正则)
PS:大家在面试的时候可以先和面试官解释,我们分别用过哪几种思想实现它,再为面试官分别解释它们写法的优劣
相信通过这两道面试题,足以让面试官对你好感倍增!
思想一:Set
提到数组去重,我们大多数人首先想到的是Set
去重
let setObj = new Set(arr); // Set(5) { 12, 23, 15, 25, 16 } 得到的是 set的实例对象
let newArr = Array.from(setObj); // [ 12, 23, 15, 25, 16 ]
console.log(newArr); // [ 12, 23, 15, 25, 16 ]
但是有些ES6方法,在早些浏览器是不兼容的,那我们来看看下面几个数组去重的思想
思想二:indexOf
思想二,大概有三个不同的方法,每个方法的写法自然也就不同,
但它们的思想都是相同的:遍历数组依次取得当前项和后面的项作比较,重复的项删除掉
【方法1】:当前项和后面项比较,重复则用splice删除
过程: 遍历数组,取出当前项和后面的元素相比较,如果数组后面的元素包含当前项,则用splice()
方法删除当前项
需要注意的几点事项:
- 遍历数组取当前项和后面比较的时候,不用取到数组最后一项,因为数组最后一项后面没有其它元素(故遍历数组元素,遍历到
arr.length - 1
时就好了)- 用
splice()
删除重复项的时候,会造成数组塌陷
问题,应删除后及时i--
,防止数组下标因数组长度变化产生差异
【补充】数组塌陷现象
在对数组进行操作的时候,会使数组的长度产生变化,同时操作的数组那个项的下一个索引会被跳过,从而造成数组的某项会被跳过,这种叫做数组塌陷现象
比如来看下面的例子:
let arr = [12, 23, 12, 15, 25, 23, 16, 25, 16];
for(let i = 0; i < arr.length - 1; i++) {
let item = arr[i]; // 取得当前数组中的每一项
let remainArgs = arr.slice(i+1); // 从 i+1项开始截取数组中剩余元素,包括i+1位置的元素
if (remainArgs.indexOf(item) > -1) {
// 数组的后面元素 包含当前项,应该把当前项删除掉
arr.splice(i, 1); // 从索引为i的位置,删除一个元素
}
}
console.log(arr); // [ 23, 12, 15, 23, 25, 16 ]
明显看出上面代码是有问题的,给大家画图解释一下:
从下图中和输出的数组可以分析出,23
这个重复元素并没有被删除掉,它的原因就是因为数组塌陷
问题导致数组长度更新,下标出现差异化,具体原因如下图:
所以解决办法,应该及时i--
完整版代码如下:
let arr = [12, 23, 12, 15, 25, 23, 16, 25, 16];
for(let i = 0; i < arr.length - 1; i++) {
let item = arr[i]; // 取得当前数组中的每一项
let remainArgs = arr.slice(i+1); // 从 i+1项开始截取数组中剩余元素,包括i+1位置的元素
if (remainArgs.indexOf(item) > -1) {
// 数组的后面元素 包含当前项,应该把当前项删除掉
arr.splice(i, 1); // 从索引为i的位置,删除一个元素
i--;
}
}
console.log(arr); // [ 12, 15, 23, 25, 16 ]
但是方法1,性能会不好,假设数组有一万个元素,当前项一旦删除,后面索引都要变。
【方法2】:重复项用null替代,最后统一过滤
流程:遍历数组当前项和后面项比较,如果当前项和后面项重复,则把当前项赋值为
null
,最后过滤数组
弊端:需要对数组进行过滤等二次操作
实现代码如下:
let arr = [12, 23, 12, 15, 25, 23, 16, 25, 16];
for(let i = 0; i < arr.length - 1; i++) {
let item = arr[i]; // 取得当前数组中的每一项
let remainArgs = arr.slice(i+1); // 从 i+1项开始截取数组中剩余元素,包括i+1位置的元素
if (remainArgs.indexOf(item) > -1) {
// 数组的后面元素 包含当前项,把当前项赋值 null
arr[i] = null; // [null, null, 12, 15, null, 23, null, 25, 16]
}
}
arr = arr.filter(item => item !== null);
console.log(arr); // [ 12, 15, 23, 25, 16 ]
但是这个方法还有个不好,就是需要对数组进行二次操作。
那我们来看方法3
【方法3】:数组最后一项元素替换掉当前项元素,并删除最后一项元素
流程:
遍历数组当前项和后面项比较,如果当前项和后面项重复
则把数组最后一项移动到当前项位置,删除数组的最后一项
为了防止
i++
,跳过从当前项比较时,应及时i--
,目的:仍然从当前项开始比较
let arr = [12, 23, 12, 15, 25, 23, 16, 25, 16];
for(let i = 0; i < arr.length - 1; i++) {
let item = arr[i]; // 取得当前数组中的每一项
let remainArgs = arr.slice(i+1); // 从 i+1项开始截取数组中剩余元素,包括i+1位置的元素
if (remainArgs.indexOf(item) > -1) { // 数组的后面元素 包含当前项
arr[i] = arr[arr.length - 1]; // 用数组最后一项替换当前项
arr.length--; // 删除数组最后一项
i--; // 仍从当前项开始比较
}
}
console.log(arr); // [ 16, 23, 12, 15, 25 ]
画图分析:
总结:
上面这三种方法的核心思想,都是
依次遍历数组当前项和后面的项作比较,重复的项就要删除掉
。 这三种方法都是操作的
原始数组
,不会涉及新的数组或新的容器等,节约内存
接下来,我们来分析一下另外的一种思想:
思想三:新容器存储思想——对象键值对
流程:
- 遍历数组中的每一项,向新容器中存储
- 存储之前,看新容器中是否有存储过了,存储过了,删除当前项
- 没存储过时,往新容器中添加数组中遍历的元素
思想:
把数组元素作为
对象属性
通过遍历数组,
判断数组元素是否已经是对象的属性
,如果对象属性定义过,则证明是重复元素,进而删除重复元素
例如:
let obj = { 10:10, 20:20, 25: 25 };
代码如下:
let obj = {};
for (let i=0; i < arr.length; i++) {
let item = arr[i]; // 取得当前项
if (typeof obj[item] !== 'undefined') {
// obj 中存在当前属性,则证明当前项 之前已经是 obj属性了
// 删除当前项
arr[i] = arr[arr.length-1];
arr.length--;
i--;
}
obj[item] = item; // obj {10: 10, 16: 16, 25: 25 ...}
}
obj = null; // 垃圾回收
console.log(arr); // [ 16, 23, 12, 15, 25 ]
思想四:相邻项的处理方案思想——基于正则
思想:先排序,再基于正则比较相邻项
流程:
- 先将无顺序的数组排序,
- 定义正则表达式的规则
- 通过
String.prototype.replace()
获取正则捕获的内容- 把正则规则捕获的内容,处理后添加到新数组中
在正式写代码之前,看下面这段代码正则后输出什么内容:
let arr = [12, 23, 12, 15, 25, 23, 16, 25, 16];
// 将数组按升序排列
arr.sort((a,b) => a-b); // [12, 12, 15, 16, 16, 23, 23, 25, 25]
let arrStr = arr.join('@') + '@'; // 12@12@15@16@16@23@23@25@25@
let reg = /(\d+@)\1*/g; // \d: 任何数字 \d+@: 15@ \1:前一个分组内容重复出现 *: 0~无数次
/**
* val:reg正则捕获的内容(有重复情况)12@12@
* group1:分组捕获的内容的第一组内容 12@
*/
arrStr.replace(reg, (val, group1)=> {
console.log(val, group1);
/*
12@12@ 12@
15@ 15@
16@16@ 16@
23@23@ 23@
25@25@ 25@
*/
})
从上面代码中可以得出,每次group1
的内容就差不多使我们想要的东西了,只要去掉@
即可。
接下来,看完整版代码:
let arr = [12, 23, 12, 15, 25, 23, 16, 25, 16];
arr.sort((a,b) => a-b);
arrStr = arr.join('@') + '@';
let reg = /(\d+@)\1*/g,
newArr = [];
arrStr.replace(reg, (val, group1) => {
// newArr.push(Number(group1.slice(0, group1.length-1)));
newArr.push(parseFloat(group1));
})
console.log(newArr); // [ 12, 15, 16, 23, 25 ]
本文总结
看完这篇文章的同学,可以在面试的时候分类,分思想给面试官描述,可以先说我用哪几种思想实现过,它们的写法又分别有什么不同。
最后希望这篇文章可以帮助到大家,感谢阅读。
看完三件事❤
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
- 点赞,转发,有你们的
『在看』
,才是我创造的动力。 - 关注公众号
『前端时光屋』
,不定期分享原创知识。 - 同时可以期待后续文章ing🚀