面试:百度一面,吓尿后

如果你也在准备春招,欢迎加我微信shunwuyu。这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。

前言

百度一面后, 可能由于过度紧张,这一两天过的好恍惚。那位又秃又强的面试官又来梦里,睡个屁。起来复习,一面要是通过,二面会考点什么?各位可以看看面试:百度一面,吓尿了 - 掘金 (juejin.cn)我的一面考题,帮我支个招。

南无阿弥陀佛! 前几天去求了佛,保我百度有二面,二面之前再给我一周准备时间。

百度一面,吓尿后

请教学长

找到内推我的学长,他细心地了解我的面试情况,称赞面完就写面经非常有悟性。能不能有二面无所谓,面完能不能把自己批的体无完肤,发现问题,快速查漏补缺最重要。

确实,春招有二个多月,自己想去的大厂有几十家。哪怕挂掉其中的大多数,只要拿一家offer, 自己也是大厂打工人。学长说,这个叫复盘,他在百度做项目经常会复盘,这是成长的重要法宝。另外,从哲学角度,可以借鉴曾国藩的屡败屡战打落牙齿和血吞,又秃又强的面试官虐我再多,需咬牙成长。在每场面试中,学习成长,成功是早晚的事。

我顺便想问学长二面一般问什么,他没有直接告诉我题型或范围,说不想扼杀我自己思考及解决问题的能力。只告诉我,一面一般是用人的leader, 二面会换人面。二面面试官要不是隔壁组的leader,要不就是部门经理。二面面试官一般会根据一面面试官的反馈,做深入考查,然后反馈给一面面试官他的意见。

所以,跟学长的交流获得以下信息:

  • 根据我的反馈还有面经,一面还行(面试撑到了1小时,面试官愿意花这么多时间,说明有戏)。

  • 不停的复盘,内心的坚定是成功的关键。

  • 二面还是场恶战,要靠自己用心准备。如果二面也表现好,一二面两位面试官双双通过的话,可能就爽歪歪了。

押题

二面若有,面试官可否是周芷若?不管如何,得在这一周练练九阳神功。

手写代码、业务场景题展示编程功底。

算法,还得再巩固下。一面考dp时,比相亲还尴尬,得整点活。

VUE底层和源码,一面基础没问。好好准备下响应式、虚拟DOM、diff算法,俺也可以砍出这三板斧,拥有30分钟高质量的面试展示时间,基本可以给二面的成功定个调。

缓存和性能优化整一整,齐活...

手写代码

数组扁平化

扁平化有很多种写法,建议大家刷下JS数组专题1️⃣ ➖ 数组扁平化 - 掘金 (juejin.cn)

扁平化是一道高频手写考题,不难,实现方案多种多样,但容易流俗。面试官的考查点在于JS基础,看我们对递归、数组toString、es6等是否灵活掌握,如果能有个亮点解法就非常nice。

我打算常见方案口述,给面试官手写一个非递归实现方案,用栈。

js 复制代码
const flatter = (arr) => { 
    # 将数组展开, 只在一端操作便是栈
    let stack = [...arr]; 
    # 结果数组
    let res = []; 
    # 栈空就处理完了
    while(stack.length){ 
        # 取出最后的元素
        let num = stack.pop(); 
        # 如果是数组,继续入栈
        if(Array.isArray(num)) {
            stack.push(...num); 
        } else { 
         // 非数组,加入res
         res.push(num);
     } 
   } 
   // 反转数组 尽量还原原数组顺序
   return res.reverse(); 
}

图片懒加载

  • 为什么要做懒加载?

这个页面启动时加载了几十张图片(甚至更多),而这些图片请求几乎是并发的,在 Chrome 浏览器,最多支持的并发请求次数是有限的,其他的请求会推入到队列中等待或者停滞不前,直到上轮请求完成后新的请求才会发出。所以相当一部分图片资源请求是需要排队等待时间的。

在上面可以看出,有部分图片达到几百 kB,甚至1.5M,直接导致了加载时间过长。

优化页面加载速度非常关键, 懒加载必须做

  • 图片懒加载的原理

<img src="./img/default.png" data-src="./img/1.jpg" /> 真实地址放在data-src中,这样就不会触发请求。

  • getBoundingClientRect

实现懒加载的核心API,就是DOM元素(img)上的getBoundingClientRect方法。

getBoundingClientRect方法返回元素相对视口的位置,用于判断图片是否进入可视区域。当图片距离视口的距离小于视口高度时,加载该图片。

javascript 复制代码
function lazyload() {
  let viewHeight = document.body.clientHeight //获取可视区高度
  let imgs = document.querySelectorAll('img[data-src]')
  imgs.forEach((item, index) => {
    if (item.dataset.src === '') return

    // 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
    let rect = item.getBoundingClientRect()
    if (rect.bottom >= 0 && rect.top < viewHeight) {
      item.src = item.dataset.src
      item.removeAttribute('data-src')
    }
  })
}
  • 事件绑定
javascript 复制代码
window.addEventListener('scroll', lazyload)
  • 节流

scroll 事件会在很短的时间内触发很多次,严重影响页面性能,为了提高网页性能,我们需要一个节流函数来控制函数的多次触发,在一段时间内(如 200ms)只执行一次回调。

throttle 这里就不写了。

javascript 复制代码
window.addEventListener('scroll', throttle(lazyload, 200))
  • IntersectionObserver

IntersectionObserver 是一个新的 API,可以自动"观察"元素是否可见。不需要计算这些属性的方式,也不需要搞节流。如果大家了解观察模式就应该好理解。

lua 复制代码
var io = new IntersectionObserver(callback, option) // 开始观察 io.observe(document.getElementById('example')) // 停止观察 io.unobserve(element) // 关闭观察器 
io.disconnect()

现在我们用IntersectionObserver实现图片懒加载

entry.isIntersecting 可以直接判断是否进入可视区

js 复制代码
const imgs = document.querySelectorAll('img[data-src]')
const config = {
  rootMargin: '0px',
  threshold: 0,
}
let observer = new IntersectionObserver((entries, self) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      let img = entry.target
      let src = img.dataset.src
      if (src) {
        img.src = src
        img.removeAttribute('data-src')
      }
      // 解除观察
      self.unobserve(entry.target)
    }
  })
}, config)

imgs.forEach((image) => {
  observer.observe(image)
})
  • vue 可以通过编写懒加载指令实现懒加载
ini 复制代码
<img v-lazy="xxx.jpg" />

下次新开一篇写下v-lazy指令,这里就不展开了。

到这里,懒加载回答的比较完美了。

算法

面试:百度一面,吓尿了 - 掘金 (juejin.cn)一文发表后,蛮多友友表示有同感。其中上图大佬,三战百度,两次都被问了斐波那契数列。看来面试前狂刷算法高频考题的确有必要,只要有颗想去百度的心,被虐8次也无所谓。 三战百度, 我吓尿了....

背包问题 (Knapsack Problem)

背包问题是很多困难动态规划题的母题,虽然之前刷过,我还是准备再复习一遍。

背包问题分为01背包、完全背包、多重背包、分组背包、混合背包等,到不是我想一次性把这些背包葫芦娃兄弟搞定,说实话短时间内也是不可能的。因为01背包问题比较简单,我就想怎么在这个问题上表现的更深入一些,跟其它竞争者有些区别。所以我在准备这道题的时候,刻意的准备了点相关背包的问题。

  • 01 背包是基础,牢牢把握。

有 n 件物品,物品重量用一个名为 weights 的数组存起来,物品的价值用一个名为 values 的数组存起来;每件物品的重量用 weights[i] 来表示,每件物品的价值用 values[i] 来表示。现在有一个容纳重量上限为 bagWeight 的背包,问你如何选取物品放入背包,才能使得背包内的物品总价值最大?

物品 重量 价值
物品0 1 15
物品1 3 20
物品2 4 30

这道题并不难,首先我们要讲清楚01背包的概念(面试不要慌,表达清楚)。01的意思是拿或不拿,背包有容量上限,如何取物品,得最值,适合用动态规划来解决。

不急于使出马保国老师的"接、化、发",一是撑撑有效面试时长、二是与别的面试者区分开来(其它人一上去肯定就dp公式一写,当了把一分钟解决问题的快男)。我打算先聊聊这题的暴力破解法,把背包问题聊清楚。

暴力解法

每个物品只有两个状态:取与不取。n 个物品组成最大价值,那么时间复杂度是2^n。可见,指数级别的时间复杂度是相当恐怖的,上动态规划。

动态规划

上动规五部曲!

  • 明确dp数组的定义

    背包问题可以用二维数组(重量和价值两个维度),但其实可以优化为一维, 这也会是面试官感兴趣的地方,安排!但这里我们先搞二维的。

    dp[i][j] i 表示什么意思?表示[0-i]的物品,即物品的编号。

    j表示容量,背包的容量是一边取物品,一边增加的,最后不超过c就可以了。

    dp[i][j]表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

    我们围绕数组的意义去执行初始化、递推公式等...

  • 递推公式

    dp[i][j] 取决于第i个物品放不放进背包。

    不放物品i, 背包容量为j(细节,dp数组的意义)它所能装的最大价值是dp[i-1][j]。在这种情况下,i-1表示不放物品i, j的意思是价值还是不放前的价值。

    放物品i,背包的最大价值应该是背包容量-物品重量的最大价值+物品i的价值 dp[i-1][j-weights[i]] + values[i]。这里有点复杂哈。dp[i-1][j-w[i]]表示不放物品i时,重量当然的j-weights[i]的最大价值 加上第i件物品的价值。

    那么公式就出来了,取他们的最大值:

    js 复制代码
    dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weights[i]]+values[i])
  • 初始化dp数组

初始化,一定要和dp数组的定义吻合。所以二维表格中一列为背包重量,一列为物品价值, 格子中的值为最大价值。

状态转移方程dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weights[i]]+values[i])中可以看出,dp[i][j]由它的上方(dp[i-1][j])和左上方( dp[i-1][j-weights[i]])推导而来,所以初始化时,我们要考虑第一行和第一列。

首先从dp[i][j]的定义出发,如果背包重量j为0的话,无论怎么选物品,背包价值总和一定为O。

js 复制代码
for (let i = 0; i < n;i++) {
    dp[i][0] = 0;
}

再从 dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])出发,可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。即dp[0][j]。

js 复制代码
for (let j = weight[0]; j <= bagWeight; j++) {
    dp[0][j] = value[0];
}

其他下标怎么初始化呢?因为每个格子放的是最大价值, 等待被指导。而且物品的价值为正,所以只需要初始化为0就可以了。

所以, 最终初始化代码如下:

js 复制代码
function knapsack(bagWeight, weights, values) {
    let n = weights.length;
    // 整个表格都为0
    let dp = new Array(n + 1).fill(0).map(() => new Array(bagWeight + 1).fill(0));
    
    for (let j = weights[0]; j <= bagWeight; j++) {
        dp[0][j] = values[0];
    }
    console.log(dp)
}

knapsack(4, [1, 3, 4], [15, 10, 30]);

打印结果

  • 遍历

这里有两重遍历的维度,即物品背包重量

那么到底是先遍历物品还是先遍历重量呢?

我们先来遍历物品

ini 复制代码
for(let i = 1; i < n; i++) { 
// 遍历物品 从1 开始, 是因为初始化时搞了
    for(let j = 0; j <= bagWeight; j++) { 
    // 遍历背包容量
        if (j < weights[i]) dp[i][j] = dp[i - 1][j]; // 这个是为了展现dp数组里元素的变化
        else dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weighst[i]] + values[i]);
    }
}
  • 打印DP数组
ini 复制代码
return dp[n-1][bagWeight];

完整代码如下:

ini 复制代码
function knapsack(bagWeight, weights, values) {
    let n = weights.length;
    let dp = new Array(n).fill(0).map(() => new Array(bagWeight + 1).fill(0));

    for (let j = weights[0]; j <= bagWeight; j++) {
        dp[0][j] = values[0];
    }
    // console.log(dp)

    for(let i = 1; i < n; i++) { 
        // 遍历物品 从1 开始, 是因为初始化时搞了
            for(let j = 0; j <= bagWeight; j++) { 
            // 遍历背包容量
                if (j < weights[i]) dp[i][j] = dp[i - 1][j]; // 这个是为了展现dp数组里元素的变化
                else dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i]] + values[i]);
            }
        }
    console.log(dp)
    return dp[n-1][bagWeight]
}

console.log(knapsack(4, [1, 3, 4], [15, 10, 30]));

滚动数组

刚刚我们用二维数组实现了01背包,现在让我们来降维。面试官看我上面的二维解法,大概率会问能不能优化一下,一维数组能不能解决。二维数组卖个破绽,其实一维解决早已准备。勾引面试官往我们准备好的包围圈跳。这里借用郭德纲的一句话,"观众什么时候笑,哪个包袱什么时候响,都是我提前安排好的"。只不过, 这里的观众就是面试官。就伺候他一个人, 那还不准备点包袱

把二维数组降到一维,说明有些状态是可以压缩的。

问题还是这个问题

物品 重量 价值
物品0 1 15
物品1 3 20
物品2 4 30

背包最大重量为4, 最大价值为30

状态转移方程(递推公式) dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weights[i]]+values[i])

i 表示[0-i]个任意物品, j 表示 0-bagWeight 背包重量 dp[i][j] 最大价值。

由递推公式可以看出,dp[i]这一层(不同j)是由上一层(i-1)推导出来的。因为我们最后只需要最大价值,并不需要把每一步都留下来。所以,我们可以直接将上一层的数据拷贝出来,直接计算当前层,将原层值覆盖。这样我们只需要一层,二维变一维。 它可以将一个矩阵压缩成一行, 每一次计算都更新这一行数据。看起来就像是在滚动,这就叫滚动数组

再次使用动规五规五部曲

  • 明确dp数组的定义

dp[j] j还是之前的j , 用了滚动数组,不需要i了。容量为j的背包所背最大价格的物品

  • 递推公式

    不放物品i 还是dp[j] 放物品i dp[j-weights[i]] + values[i]

    dp[j] = Math.max(dp[j], dp[j-weights[i]] + values[i])

  • 初始化

递推公式好理解,我们看下初始化会有什么变化呢?

dp[0] 按含义为0

那么其它项初始化为啥呢? 这里都初始化为0就好,因为dp[j]能放下的最大价值都大于0

  • 遍历顺序
ini 复制代码
for(let i = 0; i < n ; i++) { // 遍历物品  
    for(let j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量  
        dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]); 
    }  
}
  • 代码
ini 复制代码
function knapsack( bagWeight, weights, values) { 
// dp是动态规划的状态保存数组 
    const dp = new Array(bagWeight+1)).fill(0) 
    let n = weights.length
    // res 用来记录所有组合方案中的最大值
    let res = -Infinity
    for(let i=1;i<=n;i++) { 
        for(let v=baseWeight;v>=weights[i];v--) { 
        // 写出状态转移方程 
            dp[v] = Math.max(dp[v], dp[v-weights[i]] + values[i]) 
            // 即时更新最大值 
            if(dp[v] > res) { res = dp[v] } 
        } 
    } 
    return res 
}

其它部分,明天开第二篇吧。没有大厂面试烦,这面完一面后的等待啊,更是心烦...

总结

  • 手写题要刻意练习

  • 动态规划不能凭感觉来,要搞清楚dp定义、推导公式、初始化、遍历顺序、打印DP数组五步每步都搞扎实。

  • 除了状态转移方程,初始化和遍历也可能比较复杂

  • 面试中的亮点和技能包袱,都是可以提前安排好的。 比如01背包,我就从二维引到一维,逐步展示我对动态规划的理解,主导面试节奏。

参考资料

相关推荐
酷爱码11 分钟前
css中的 vertical-align与line-height作用详解
前端·css
沐土Arvin25 分钟前
深入理解 requestIdleCallback:浏览器空闲时段的性能优化利器
开发语言·前端·javascript·设计模式·html
专注VB编程开发20年26 分钟前
VB.NET关于接口实现与简化设计的分析,封装其他类
java·前端·数据库
小妖66636 分钟前
css 中 content: “\e6d0“ 怎么变成图标的?
前端·css
L耀早睡1 小时前
mapreduce打包运行
大数据·前端·spark·mapreduce
HouGISer2 小时前
副业小程序YUERGS,从开发到变现
前端·小程序
outstanding木槿2 小时前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
霸王蟹2 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹2 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
专注VB编程开发20年2 小时前
asp.net IHttpHandler 对分块传输编码的支持,IIs web服务器后端技术
服务器·前端·asp.net