1. 贪心算法基础与解题框架
贪心算法(Greedy Algorithm)是解决最优化问题的一种常用方法,它在每个决策点都采取当前看来最优的选择,希望通过局部最优解的组合达到全局最优。与动态规划不同,贪心算法不需要保存子问题的解,因此通常具有更高的时间效率。
1.1 贪心算法的适用场景
贪心算法适用于具有"贪心选择性质"和"最优子结构"的问题。具体表现为:
- 问题可以分解为一系列子问题
- 每个子问题的局部最优解能导致全局最优解
- 无后效性:当前选择不会影响后续子问题的结构
典型应用场景包括:
- 分配问题(如分发饼干)
- 区间调度问题
- 最短路径问题(如Dijkstra算法)
- 压缩编码(如Huffman编码)
1.2 贪心算法的解题模板
虽然贪心问题各不相同,但通常遵循以下解决框架:
- 问题转化:将原问题转化为可以通过一系列选择来解决的形式
- 确定贪心策略:明确在每个步骤如何做出"局部最优"选择
- 证明正确性:验证该策略确实能得到全局最优解(可通过反证法或数学归纳法)
- 实现算法:编写代码实现贪心策略
注意:贪心算法的难点往往在于贪心策略的确定和正确性证明,而非代码实现本身。许多看似适合贪心的问题实际上需要动态规划解决。
2. 分发饼干问题(455题)解析
2.1 问题重述与初步分析
题目描述: 假设你是一位很棒的家长,想要给你的孩子们一些饼干。每个孩子 i 都有一个胃口值 g[i],这是能让该孩子满足的饼干的最小尺寸;每块饼干 j 都有一个尺寸 s[j]。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i。你的目标是尽可能满足更多的孩子,并输出这个最大数值。
示例: 输入:g = [1,2,3], s = [1,1] 输出:1 解释:你有三个孩子和两块饼干,尺寸分别为1和1。三个孩子的胃口值分别为1,2,3。虽然你有两块饼干,但由于它们的尺寸都是1,你只能让胃口值是1的孩子满足。
2.2 贪心策略设计与证明
这个问题可以采用"最小的饼干满足最小的胃口"策略:
- 将孩子数组g和饼干数组s分别排序
- 使用双指针法,从小到大尝试匹配
- 如果当前饼干满足当前孩子,则计数+1,两个指针都后移
- 如果不满足,则只移动饼干指针尝试更大的饼干
正确性证明:
- 排序后可以确保我们总是尝试用最小的可用饼干满足最小的未被满足的胃口
- 如果放弃当前较小的饼干去满足更大的胃口,不会得到更好的解
- 因此局部最优选择能导致全局最优解
2.3 代码实现与复杂度分析
def findContentChildren(g, s): g.sort() s.sort() child = cookie = 0 while child < len(g) and cookie < len(s): if s[cookie] >= g[child]: child += 1 cookie += 1 return child时间复杂度:O(nlogn + mlogm),主要由排序过程决定 空间复杂度:O(1),只使用了常数级别的额外空间
实际应用中的优化点:如果输入数据范围有限,可以考虑使用计数排序来优化时间复杂度到O(n+m)
3. 摆动序列问题(376题)深度解析
3.1 问题描述与理解
题目描述: 如果连续数字之间的差严格地在正负之间交替,则这样的数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也被视为摆动序列。
给定一个整数序列,返回作为摆动序列的最长子序列的长度。通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
示例: 输入:[1,7,4,9,2,5] 输出:6 解释:整个序列就是一个摆动序列,因为差值(6,-3,5,-7,3)交替变化。
3.2 贪心解法思路
这个问题可以采用状态机思想来解决:
- 定义三种状态:起始、上升、下降
- 遍历序列,根据当前数字与前一个数字的比较结果转换状态
- 每当状态发生有效转换时,摆动序列长度+1
贪心性质体现在:我们只需要记录状态变化点,而不需要考虑中间的非摆动元素。
3.3 状态机实现与边界处理
def wiggleMaxLength(nums): if len(nums) < 2: return len(nums) prev_diff = nums[1] - nums[0] count = 2 if prev_diff != 0 else 1 for i in range(2, len(nums)): diff = nums[i] - nums[i-1] if (diff > 0 and prev_diff <= 0) or (diff < 0 and prev_diff >= 0): count += 1 prev_diff = diff return count关键点:
- 处理初始两个元素相等的情况
- 只有当差值符号真正变化时才增加计数
- 跳过连续的上升或下降序列中的中间元素
时间复杂度:O(n),只需一次遍历 空间复杂度:O(1),使用常数空间
4. 最大子序和问题(53题)的贪心视角
4.1 问题定义与常规解法
题目描述: 给定一个整数数组nums,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例: 输入:[-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组[4,-1,2,1]的和最大,为6。
传统解法是Kadane算法,属于动态规划思想。但从贪心角度看,我们也可以找到类似的解决方案。
4.2 贪心策略解析
贪心思路:
- 维护当前连续子数组的和
- 当当前和变为负数时,立即放弃当前子数组(因为负数会降低后续和)
- 始终保持跟踪遇到的最大和
这种策略的贪心性体现在:一旦当前子数组的和不再有利于后续求和(变为负),就立即做出放弃的局部最优选择。
4.3 代码实现与变种问题
def maxSubArray(nums): max_sum = current_sum = nums[0] for num in nums[1:]: current_sum = max(num, current_sum + num) max_sum = max(max_sum, current_sum) return max_sum时间复杂度:O(n),一次遍历 空间复杂度:O(1),常数空间
变种问题:
- 返回最大子数组的起始和结束索引
- 处理环形数组的情况
- 二维矩阵中的最大子矩阵和
5. 贪心算法实战技巧与常见误区
5.1 贪心算法的证明方法
正确实现贪心算法的关键在于证明其正确性。常用证明方法包括:
- 交换论证:证明任何非贪心的选择都可以通过交换调整为贪心选择而不降低解的质量
- 数学归纳法:证明贪心选择在每一步都保持最优解的存在
- 决策包容性:证明贪心选择所包含的解空间一定包含最优解
5.2 贪心与动态规划的抉择
许多问题既可以用贪心也可以用动态规划解决,选择依据:
- 如果问题具有贪心选择性质,优先考虑贪心(通常更高效)
- 当需要保存大量子问题解时,考虑动态规划
- 当贪心策略的正确性难以证明时,可能需要动态规划
典型对比:
- 背包问题:0-1背包用DP,分数背包用贪心
- 最短路径:Dijkstra(贪心) vs Bellman-Ford(DP)
5.3 面试中的常见陷阱
- 过度使用贪心:不是所有看似可以贪心的问题都适合贪心解法
- 忽略边界条件:如空输入、全部负数等情况
- 错误的状态定义:在状态转换类问题中错误定义状态
- 忽略排序成本:贪心算法常需要先排序,但可能成为时间瓶颈
实际工程中的应用:贪心算法常用于调度系统、资源分配、网络路由等实时性要求高的场景,因为其高效性。但在金融等需要绝对精确的领域要谨慎使用。