前端面试中的贪心算法:小白也能懂的“最优解”攻略

581 阅读5分钟

前言

最近在作者准备面试时,突然被一道算法题卡住了: "如何用最少的硬币找零?" 。正当我抓耳挠腮时,程序员卡尔的贪心算法教程让我豁然开朗。今天我就用最接地气的方式,带大家攻克这个前端面试中的高频考点!

一、什么是贪心算法?从生活场景说起

1.1 日常生活中的贪心思维

想象你去超市买零食:

  • 目标:用最少的钱买最多的零食
  • 策略:每次都选性价比最高的零食(比如第二件半价)

这就是典型的贪心思维——每一步都选择当前最优解,最终希望得到全局最优 image.png

1.2 程序世界的贪心算法

贪心算法(Greedy Algorithm)的三个核心特征

  1. 局部最优:每一步都选当前最好的
  2. 不可逆:做出选择后不回头
  3. 高效性:通常时间复杂度较低

举个栗子🌰:前端路由匹配时,我们总是优先匹配最具体的路径,这就是一种贪心策略。


二、3道必会贪心算法题

2.1 分发饼干问题(LeetCode 455)

题目描述

假设你是幼儿园老师:

  • 有 g = [1,3,2] 表示3个孩子的胃口值
  • 有 s = [1,1] 表示2块饼干的尺寸
  • 规则:每个孩子最多分到一块饼干,饼干尺寸 >= 胃口值才能满足
  • 求最多能满足多少个孩子?
解题思路
  1. 排序:把小孩和饼干都从小到大排序
  2. 双指针扫描:小饼干优先喂给胃口小的小孩
function findContentChildren(g, s) {
  g.sort((a, b) => a - b); // 孩子排序 [1,2,3]
  s.sort((a, b) => a - b); // 饼干排序 [1,1]
  
  let child = 0, cookie = 0;
  while(child < g.length && cookie < s.length) {
    if(g[child] <= s[cookie]) { // 当前饼干能满足当前孩子
      child++;
    }
    cookie++; // 无论是否满足,饼干都会被消耗
  }
  return child; // 返回被满足的孩子数
}
执行示例
孩子: [1, 2, 3]
饼干: [1, 1]

步骤1:用第1块饼干满足第1个孩子 → child=1
步骤2:第2块饼干无法满足第2个孩子 → 跳过
最终结果:1个孩子被满足

2.2 柠檬水找零(LeetCode 860)

题目描述

你开了一家柠檬水摊:

  • 每杯5元
  • 顾客支付可能是5元、10元、20元
  • 初始你没有零钱
  • 判断能否给所有顾客正确找零
贪心策略解析
  • 优先保留5元:因为5元更灵活(可以找10元和20元)

  • 找零优先级

    • 收到10元:必须找1个5元
    • 收到20元:优先找1个10元+1个5元(而不是3个5元)
function lemonadeChange(bills) {
  let five = 0, ten = 0;
  
  for(const bill of bills) {
    if(bill === 5) {
      five++; // 直接收钱
    } else if(bill === 10) {
      if(five === 0) return false; // 没有5元找零
      five--;
      ten++;
    } else { // 处理20元
      if(ten > 0 && five > 0) { // 优先用10+5找零
        ten--;
        five--;
      } else if(five >= 3) { // 次选用5*3
        five -= 3;
      } else {
        return false;
      }
    }
  }
  return true;
}
常见错误案例
输入: [5,5,10,10,20]
错误处理:第三次收到10元时如果用掉所有5元,后续无法找零20元
正确做法:保留至少1个5元应对可能的20元

2.3 重构字符串(LeetCode 621 扩展)

业务场景联想

假设我们需要实现一个任务调度器:

  • 每个任务需要1个单位时间
  • 相同任务之间必须有n个冷却时间
  • 求最短完成时间
解题思路
  1. 统计任务频率
  2. 按频率降序排列
  3. 计算空闲时间槽
  4. 填充任务
function leastInterval(tasks, n) {
  const freq = new Array(26).fill(0);
  for(const t of tasks) {
    freq[t.charCodeAt(0)-65]++;
  }
  freq.sort((a,b) => b - a); // 降序排列
  
  const maxCount = freq[0] - 1; // 最大任务需要间隔次数
  let idleSlots = maxCount * n; // 初始空闲槽
  
  // 用其他任务填充空闲
  for(let i=1; i<freq.length; i++) {
    idleSlots -= Math.min(freq[i], maxCount);
  }
  
  return tasks.length + (idleSlots > 0 ? idleSlots : 0);
}
执行示例
输入:tasks = ["A","A","A","B","B","B"], n = 2
输出:8
解释:A -> B -> 待命 -> A -> B -> 待命 -> A -> B

三、面试技巧大揭秘

3.1 识别贪心问题的三个信号

  1. 题目要求"最大/最小"值
  2. 问题可以分解为独立步骤
  3. 当前决策不影响后续状态

3.2 贪心一般解题步骤

贪心算法一般分为如下四步:

  • 将问题分解为若干个子问题
  • 找出适合的贪心策略
  • 求解每一个子问题的最优解
  • 将局部最优解堆叠成全局最优解

这个四步其实过于理论化了,我们平时在做贪心类的题目时,如果按照这四步去思考,是不太OK的。

在处理贪心算法题目时,关键在于明确每一步的局部最优解——即当前状态下的最佳选择,并深入分析这些局部最优如何逐步累积以形成全局最优解。通过理解局部决策对整体结果的影响,确认它们能够有效地组合起来,达到解决整个问题的目的,这样便能简化过程,高效求解。

END

贪心算法就像前端开发中的性能优化——我们无法一步到位解决所有问题,但通过持续做出局部最优选择,最终能达到整体最优效果。希望大家在面试中遇到这类题目时,都能像处理CSS布局一样游刃有余!