前端面试常考的10道数组算法题,看看你会几道?

366 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情


前端面试常考的 10 道数组算法题,看看你会几道?

持续更新!

  1. 数组去重
  2. 随机打乱
  3. 统计数字
  4. 合并数组
  5. 数组拍平
  6. 多维数组深拷贝
  7. 两数之和
  8. 全排列
  9. 唯一元素
  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 ] ]

8. 全排列

9. 唯一元素

10. 求众数