一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情。
前端面试常考的 10 道数组算法题,看看你会几道?
持续更新!
- 数组去重
- 随机打乱
- 统计数字
- 合并数组
- 数组拍平
- 多维数组深拷贝
- 两数之和
- 全排列
- 唯一元素
- 求众数
1. 数组去重
去除数组中重复元素。
(1)Set
:不允许添加重复元素。
function unique(arr) {
return [...new Set(arr)]
}
unique([1, 1, 2, 2, 2, 3, 4, 4, 5]) // [ 1, 2, 3, 4, 5 ]
(2)使用 Map
记录以及出现过的元素,再次遇到,不加入结果集中。
function unique(arr) {
let map = new Map()
let ans = []
for (let e of arr) {
if (!map.has(e)) {
ans.push(e)
map.set(e, 1)
}
}
return ans
}
unique([1, 1, 2, 2, 2, 3, 4, 4, 5]) // [ 1, 2, 3, 4, 5 ]
2. 随机打乱
面试官:怎么尽量随机打乱一个数组?代码实现。
很多人第一时间想到的应该是这个:
arr.sort(() => Math.random() - 0.5)
但实际上这种打乱算法很不合理,在打乱次数很大的情况下更明显。处于原位置的元素的情况会大幅增加。这是 Array.protoype.sort
内部实现的原因,不同长度数组采用的底层算法不同。
更加合理的是一种 洗牌算法。不断循环从前 n-1
个数字中随机选出一个 数字,和当前位置 n
交换,然后让 n
递减,直到打乱所有数字。
// 洗牌算法,打乱数组
function shuffle(arr) {
let len = arr.length
while (len--) {
let random = (Math.random() * len) >>> 0 // 移位取整
;[arr[random], arr[len]] = [arr[len], arr[random]] // 交换元素
}
}
3. 统计数字
给你一个数组,统计数组中各元素的个数。
这个很常见了,有多种方法实现。
(1)Map
function counter(arr) {
const map = new Map()
for (let e of arr) {
map.set(e, (map?.get(e) ?? 0) + 1)
}
return map
}
counter([1, 2, 2, 3, 4, 4, 5, 5, 5])
// Map {1 => 1, 2 => 2, 3 => 1, 4 => 2, 5 => 3}
(2)reduce
function counter(arr) {
return arr.reduce((pre, cur) => {
if (pre?.[cur]) pre[cur]++
else pre[cur] = 1
return pre
}, {})
}
counter([1, 2, 2, 3, 4, 4, 5, 5, 5])
// {1: 1, 2: 2, 3: 1, 4: 2, 5: 3}
4. 合并数组
合并多个数组,最终只返回一个数组,且去除数组中无效值。
function merge(...arrs) {
let ans = []
for (let arr of arrs) {
// 过滤规则,去除 null,undefined,NaN,""等,注意保留 0
const rules = (x) => Boolean(x) || x === 0
arr = arr.filter(rules)
// 数组展开运算符也可以用循环代替
ans = [...ans, ...arr]
}
return ans
}
merge([0, 1, 2, 3, null], [4, 5, 6, undefined], [7, 8, NaN])
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
5. 数组拍平
给你一个多维数组,将其转换为一个一维数组。
又是一道经典题目,多维数组转一维。很多方法可实现,我们分别介绍 递归、迭代 这两种方法。
递归实现
1、普通递归
function flat(arr) {
let ans = []
arr.forEach((e) => {
if (Array.isArray(e)) ans.push(...flat(e))
else ans.push(e)
})
return ans
}
let arr = [[1, 2, [3, 4]], 5, [6, 7, [8]], 9]
flat(arr) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
2、使用 reduce
进行 递归 展开
function flat(arr) {
return arr.reduce((pre, cur) => {
// 若 cur 为数组,则进入递归:flat(cur);否则直接加入结果集中
pre = pre.concat(Array.isArray(cur) ? flat(cur) : cur)
return pre
}, [])
}
let arr = [[1, 2, [3, 4]], 5, [6, 7, [8]], 9]
flat(arr) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
迭代实现
1、数组展开运算符 + concat
function flat(arr) {
// 不断展开数组中的项,直到数组中没有数组类型
while (true) {
// 核心
arr = [].concat(...arr)
let flag = true
for (let e of arr) {
if (Array.isArray(e)) {
// 只要还存在一个子数组没展开就继续迭代
flag = false
break
}
}
if (flag) return arr
}
}
2、apply()
+ some()
上述方法的展开运算符可以用 apply
代替,因为 apply
接收数组形式的参数集。而判断数组中是否还有数组类型的元素可以用 some()
方法代替,代码如下:
function flat(arr) {
// 只要 arr 中还存在数组类型元素,则继续展开 arr,直到 arr 为一维数组
while (arr.some((item) => Array.isArray(item))) {
arr = [].concat.apply([], arr)
}
return arr
}
let arr = [[1, 2, [3, 4]], 5, [6, 7, [8]], 9]
flat(arr) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
ES2019 提供了 Array.prototype.flat()
方法,接收一个 depth
表示多维数组的维度,则使用 arr.flat(Infinity)
即可拍平各层深度的多维数组。
6. 多维数组深拷贝
实现一个函数
arrayDeepCopy()
,要求对一个多维数进行深拷贝。这里你只需要考虑数组中的对象只有Array
类型,且原始值均为数字。例如:[ [1, [2]], [3, [4, 5]] ]
。
这里你可以停下来,自己写一写。这里有坑,如果你的代码或者思路是下面这样的,说明你踩坑了。
错误示例
// 错误示例
function arrayDeepCopy(arr) {
let ans = []
for (let e of arr) {
ans.push(e)
}
return ans
}
这种拷贝方式,对于原数组中的数字类型,确实有用,但是 对于数组中的数组类型,依然是浅拷贝。请看下面例子。
let arr = [[1, [2]], [3, [4, 5]]]
let res = arrayDeepCopy(arr)
res[0][1] = 100
console.log(res) // [ [ 1, 100 ], [ 3, [ 4, 5 ] ] ]
console.log(arr) // [ [ 1, 100 ], [ 3, [ 4, 5 ] ] ]
正确方法
对于更深层次的数组类型的元素,我们需要进一步进行深拷贝,这里可以用递归思路解决。(实际上这属于对象深拷贝的一种情况,当对象类型为数组时。)
// * 正确示例
function arrayDeepCopy(arr) {
let ans = []
for (let e of arr) {
if (Array.isArray(e)) {
// 若为数组,则进入深拷贝递归
ans.push(arrayDeepCopy(e))
} else {
// 否则,直接添加到结果集中
ans.push(e)
}
}
return ans
}
结果测试:
let arr = [[1, [2]], [3, [4, 5]]]
let res = arrayDeepCopy(arr)
res[0][1] = 100
console.log(res)// [ [ 1, 100 ], [ 3, [ 4, 5 ] ] ]
console.log(arr)// [ [ 1, [ 2 ] ], [ 3, [ 4, 5 ] ] ]
7. 两数之和
某大厂这段时间春招的一个算法题,就是力扣第一题,但也有点不一样。
题目描述:在数组上定义一个方法
findSum(target)
,要求找出数组中的两个不同位置的数,使得其和为target
。例如:
nums = [1,2,3,5,-2,2,6]
nums.findSum(4) -> [[1,3],[2,2],[-2,6]]
要求定义在 nums
上,也就是在其原型上 prototype
上定义(使用 this
访问当前数组),找出所有满足条件的元素组合,然后返回。
首先大家都能想的方法就是两层循环,直接遍历寻找这样的组合,时间复杂度为 O(n^2)
。
Array.prototype.findSum = function (target) {
let ans = []
for (let i = 0; i < this.length - 1; i++) {
for (let j = i + 1; j < this.length; j++) {
if (this[i] + this[j] === target) ans.push([this[i], this[j]])
}
}
return ans
}
nums.findSum(4) // [ [ 1, 3 ], [ 2, 2 ], [ -2, 6 ] ]
面试中,能写出效率更高的算法对我们是有利的,毕竟这种方法一般人都会。下面介绍第二章方法,使用哈希 Map
。
遍历数组,对于当前元素 e
,去 map
中查询,若存在它需要的那一个数 target - e
,则说明这一对满足和等于 target
。若不存在,则 map.set(target - e, e)
。空间换时间,时间复杂度为 O(n)
。
Array.prototype.findSum = function (target) {
const map = new Map()
const ans = []
for (let e of this) {
if (!map.has(e)) {
map.set(target - e, e)
} else {
ans.push([map.get(e), e])
}
}
return ans
}
nums.findSum(4) // [ [ 1, 3 ], [ 2, 2 ], [ -2, 6 ] ]