真实面试场景下如何做算法题

0 阅读6分钟

image.png

前言

我在大厂, 经历过大大小小的面试, 这里说一下中大厂面试场景:

现在不管是前端还是后端, 大厂的一场面试基本1小时, 前面40分钟拷打八股和项目, 然后基本都会在面试末尾给一道题目, 一般会给25分钟左右的时间(包含做题和探讨), 本文主要介绍如何以通过面试为目的来做题

有一个小误区: 好多同学会以为只要最快的速度做完题目就行, 其实不对, 试想一下前面讲项目比较磕绊, 或者八股答得不好, 到了做题直接3分钟秒了, 面试官会觉得你是不是背过, 反而不合适 (说的就是你:接雨水)

正文

选择了一道lc算法题, 我以面试场景和结合代码解释来说明

题目描述

1653. 使字符串平衡的最少删除次数

给你一个字符串 s ,它仅包含字符 'a' 和 'b',你可以删除 s 中任意数目的字符,使得 s 平衡 。

当不存在下标对 (i,j) 满足 i < j ,且 s[i] = 'b' 的同时 s[j]= 'a' ,此时认为 s 是 平衡 的。 返回使 s 平衡 的 最少 删除次数。

选择道题有两个理由:

  1. 写文章的时候是2月7号, 是今天的每日一题, 比较随机, 贴合面试场景
  2. 解法可以有递进

题意翻译

一句话解释, 让字符串变成所有的a在前b在后, 比如aaabb, aa合法, ababb不合法, 需要删掉第一个b变成aabb

面试中怎么处理

说实话我自己做只是从暴力想到了前后缀处理, 动态规划是看了答案才会的, 面试的场景下, 一般面试官会给出一些长度比较小的case, 可以花5分钟快速暴力, 确保简单的case都能过, 这至少能说明有基本的理解能力和代码能力

1. 暴力方法

暴力都没思路的基本寄了

一般面试官给了两个case 例如 s = "aababbab", s = "bbaaaaabb"

分析完题目可知要让a在前b在后, 且删除掉的字符数最少, 针对第一个case, 只要把第三位b和第七位的a删了, 变成aaabbb即可, 第二个case删掉最前面两个b变成aaaaabb即可

思路: 就是遍历到索引i的时候, 统计i前面所有的b和i后面所有的a数量, 注意把a或者b全删掉也是一种合法情况

面试中一般console.log跑出来两个case答案是2就行

const fun = (s) => {
  const l = s.length;
  let res = Infinity;
  for (let i = 0; i < s.length; i++) {
    let cnt = 0;
    for (let j = 0; j <= i; j++) {
      const h = s[j];
      if (h === "b") {
        cnt++;
      }
    }

    for (let j = i + 1; j < l; j++) {
      const h = s[j];
      if (h === "a") {
        cnt++;
      }
    }

    res = Math.min(res, cnt);
  }

  // 把其中一个字符全删了也是合法的
  const cntA = s.split("").filter((v) => v === "a").length;
  return Math.min(res, cntA, l - cntA);
};

s = "aababbab";
// s = "bbaaaaabb";
console.log(fun(s)); // 2

2. 优化暴力

注意到暴力法代码的第6行和第13行的两个内部for循环, 主要就是为了统计到索引i处, 前面有多少b和后面有多少a, 每次都计算, 造成了O(n^2)的时间复杂度, 可以针对这里优化, 可以提前用数组存储一下i前后的ab数量

以上这些话一定要和面试官说出来, 面试是需要交流的, 比闷头做题要更好

分别使用两个数组, 来计算到i位置时, 前面b的数量, 和后面a的数量, 注释写在代码中了

// 使用前后预处理来优化暴力解法
const fun = (s) => {
  const count = [];
  let c = 0;
  const l = s.length;
  for (let i = 0; i < l; i++) {
    const g = s[i];
    if (g === "b") {
      c++;
    }
    count.push(c);
  }

  const rightCount = [];
  c = 0;
  for (let i = l - 1; i >= 0; i--) {
    const g = s[i];
    if (g === "a") {
      c++;
    }
    rightCount.push(c);
  }
  rightCount.reverse();

  let res = Infinity;
  for (let i = 0; i < l; i++) {
    let cnt = 0;
    // count[i] = [0, i]中b的数量
    cnt += count[i];
    // rightCount[i + 1] = [i + 1, l - 1]中a的数量
    cnt += rightCount[i + 1] ?? 0;
    res = Math.min(res, cnt);
  }

  // 把其中一个字符全删了也是合法的
  const cntA = s.split("").filter((v) => v === "a").length;
  return Math.min(res, cntA, l - cntA);
};

3. 动态规划

一般前后缀预处理法写出来, 面试差不多可以了, 如果还能更进一步, 那这道算法题的面试价值就凸显出来了

对于s来说, 最后一个字符

  • 如果是b, 不需要处理, s的答案就是s.slice(0, -1)这个字符串的答案
  • 如果是a, 分两种情况, 两者取最小值
    • 删掉, 答案是s.slice(0, -1)的答案 + 1, 删掉这个a需要加一次
    • 不删掉, 答案是s.slice(0, -1)中b的数量, 为了保留这个a, 前面的b都得删

动态规划的几大要素

定义, 初始值, 递归公式

dp[i]表示[0, i]长度字符串的答案

第一个字符不管是a还是b都不需要删, dp[0] = 0, 就是说s如果只有一个字符的时候, 答案是0

如果s有两个及以上的字符, 要按照上面分析的情况开始递推

递推公式

如果s[i]是b, dp[i] = dp[i - 1]
如果s[i]是a, dp[i] = Math.min(dp[i - 1] + 1, 前面b的数量)

完整代码

const fun = (s) => {
  const len = s.length;
  // dp[i]表示[0, i]长度字符串的答案
  // 对于第一个字符不管是a是b 都不需要删除 dp[0] = 0
  const dp = new Array(len).fill(0)
  let cntB = 0
  for (let i = 0; i < len; i++) {
    if (s[i] === 'b') {
      cntB++
    }

    if (i > 0) {
      if (s[i] === 'b') {
        dp[i] = dp[i - 1]
      } else {
        dp[i] = Math.min(dp[i - 1] + 1, cntB)
      }
    }
  }
  return dp[len - 1]
}

4. 动态规划的空间优化

注意到dp[i]只与dp[i - 1]有关, 就可以进行空间的优化, 用一个变量简化

这个是dp空间优化的精髓, 即等号右边是过去的值, 左边是现在的值

一般如果能写出动态规划的常规版本, 面试中简单判断一下能不能空间优化, 如果能且保证不出问题, 可以快速用3分钟改进一下, 面试更加分

如果没把握改空间优化, 比如一些二维dp的题, dp[i][j]改成dp[j], 这种题不难, 但是面试那种场合容易改错, 可以直接和面试官说一下有空间优化的思路和方式

别一上来直接写空间最优版本, 既没有表现出思维有递进, 也不够稳, 然后简单的case跑不过, 那就寄了

切记: 面试的目的是通过面试本身, 原则就是一个

最终答案, 多么简洁优美

const fun = (s) => {
  const len = s.length;
  let dp = 0
  for (let i = 0, cntB = 0; i < len; i++) {
    if (s[i] === 'b') {
      cntB++
    } else {
      dp = Math.min(dp + 1, cntB)
    }
  }
  return dp
};