每个系列一本前端好书,帮你轻松学重点。
本系列来自上海交通大学硕士,华为高级算法工程师 靳宇栋 的 《Hello,算法》
一轮轮的简单选择中,时刻追求自身成长的最大可能,逐步导向最佳答案。
本篇话题展开之前,先看个日常很常见的问题:零钱兑换。
你去超市购物,给收银员100元,而你购买的商品只需要2元,他要怎样给你找钱?
很简单,会100内加减的都能轻易搞定,你会根据还剩余的找零额度,从可选择的币种中选择面值最大的,直至达到数额为止。
其实你会发现,“零钱”只是一种比较具象的表达,把“找零”这件事进一步抽象,可用于解决很多领域的问题。
它,就是“贪心算法”。
贪心算法
贪心算法(greedy algorithm)是一种常见的解决优化问题的算法,其基本思想是在问题的每个决策阶段,都选择当前看起来最优的选择,即贪心地做出局部最优的决策,以期获得全局最优解。
贪心算法简洁且高效,在许多实际问题中有着广泛的应用。
除了找零钱,它还可用于解决“分数背包问题”:
给一个背包,有容量限制,另有N件物品,每件物品都有它的重量和价值,每件物品只能选择一次,但可以选择物品的一部分,价值根据选择的重量比例计算。
求:在限定背包容量下,背包中物品的最大价值。
这个问题的解答策略和“找零”类似:最大化背包内物品总价值,本质上是最大化单位重量下的物品价值。
- 将物品按照单位价值从高到低进行排序。
- 遍历所有物品,每轮贪心地选择单位价值最高的物品。
- 若剩余背包容量不足,则使用当前物品的一部分填满背包。
注意:这里说的是“分数背包”,不是“0-1背包”。
代码实现
以“找零”为例,贪心算法的核心实现:
/* 零钱兑换:贪心 */
function coinChangeGreedy(coins, amt) {
// 假设 coins 数组有序
let i = coins.length - 1;
let count = 0;
// 循环进行贪心选择,直到无剩余金额
while (amt > 0) {
// 找到小于且最接近剩余金额的硬币
while (i > 0 && coins[i] > amt) {
i--;
}
// 选择 coins[i]
amt -= coins[i];
count++;
}
// 若未找到可行方案,则返回 -1
return amt === 0 ? count : -1;
}
示意图如下:
优点与局限
贪心算法的优点是操作直接、实现简单,通常效率也很高。
但不是所有分步解决的问题都适合使用贪心,什么样的问题适合呢?
主要关注两个性质。
- 贪心选择性质:只有当局部最优选择始终可以导致全局最优解时,贪心算法才能保证得到最优解。
- 最优子结构:原问题的最优解包含子问题的最优解。
关键词:最优解。
还是找零问题,贪心能找到最优解的前提是,有足够的币值种类可选,如果没有,像下面这样:
给定币值:[1,20,50],目标值是 60,使用贪心策略,它会找到 50 + 1*10,总数是11,但实际更优的做法是 20 * 3,只需要3就可以。
所以可以理解为,贪心适合的场景是“想要多少(比如10),就有多少”,而不是妥协退而求其次。
其他应用
除了上面介绍的两种,贪心的适用场景还包括但不限于:
- 区间调度问题:假设你有一些任务,每个任务在一段时间内进行,你的目标是完成尽可能多的任务。如果每次都选择结束时间最早的任务,那么贪心算法就可以得到最优解。
- 股票买卖问题:给定一组股票的历史价格,你可以进行多次买卖,但如果你已经持有股票,那么在卖出之前不能再买,目标是获取最大利润。
- 霍夫曼编码:霍夫曼编码是一种用于无损数据压缩的贪心算法。通过构建霍夫曼树,每次选择出现频率最低的两个节点合并,最后得到的霍夫曼树的带权路径长度(编码长度)最小。
- Dijkstra 算法:它是一种解决给定源顶点到其余各顶点的最短路径问题的贪心算法。
小结
贪心是必掌握的经典算法之一,实现也不难,重点是吃透它的使用场景。
下一篇,将是本系列的终篇,让我们一起期待会是什么。
更多好文第一时间接收,可关注公众号:“前端说书匠”