作为一名经历过多次算法面试的老兵,我深知贪心算法在实际面试中的高频出现率。今天要分享的三个题目——分发饼干、摆动序列和最大子序和,正是贪心算法的经典应用场景。这三个题目看似简单,却涵盖了贪心算法的核心思想:局部最优推导全局最优。让我们从实际解题的角度,深入剖析这些题目背后的算法思维。
分发饼干(LeetCode 455)的问题描述很简单:有一群孩子和一堆饼干,每个孩子有一个满足度,每块饼干有一个大小。只有当饼干的大小≥孩子的满足度时,孩子才能得到满足。我们的目标是尽可能满足更多的孩子。
这个问题可以抽象为一个匹配问题:将饼干分配给合适的儿童。贪心算法的思路在这里非常适用——我们总是尝试用最小的饼干满足最容易满足的孩子,这样就能为后续分配保留更大的饼干。
具体实现步骤如下:
python复制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)来自排序,空间复杂度O(1)。
在实际面试中,这个问题可能会有多种变体:
理解基础问题的解法后,这些变种都能通过调整贪心策略来解决。
摆动序列(LeetCode 376)要求找出序列中最长的摆动子序列的长度。摆动序列的定义是相邻元素的差严格在正负之间交替。
例如,[1,7,4,9,2,5]是一个摆动序列,因为差值(6,-3,5,-7,3)正负交替。这个问题的难点在于如何高效地判断摆动点,特别是处理连续递增或递减的情况。
贪心算法的思路是:我们只需要统计序列中实际出现的"峰"和"谷"的数量,忽略中间的过渡元素。具体实现时,我们关注相邻元素差的变化方向。
python复制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
在实际编码中,有几个边界情况需要特别注意:
经验分享:这个问题教会我们,贪心算法有时只需要关注关键转折点,而不必处理所有中间状态。这种思维方式在解决其他序列问题时也非常有用。
最大子序和(LeetCode 53)要求找出数组中连续子数组的最大和。暴力解法是计算所有可能的子数组和,时间复杂度O(n²),显然不适用于大规模数据。
贪心算法的思路是:维护一个当前和,如果当前和变为负数,就重置为0(因为负数会减小后续和)。这种策略有效是因为最大和子数组不可能以一个负数和开头。
python复制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
这个实现是著名的Kadane算法,时间复杂度O(n),空间复杂度O(1)。它展示了贪心算法的精髓:在每一步做出局部最优选择。
为什么这个贪心策略是正确的?可以这样理解:
实战技巧:在面试中,除了写出代码,最好能解释算法的正确性。这展示了你对算法本质的理解。
通过这三个题目,我们可以总结出贪心算法的一些通用解题模式:
贪心算法有效的关键在于问题具有"贪心选择性质"——局部最优解能导致全局最优解。在解决新问题时,我们需要验证这一点。
贪心算法通常适用于:
调试方法:在小样例上手动模拟指针移动过程。
调试方法:打印出每次比较的差值变化,验证摆动条件。
调试方法:跟踪记录每一步的current_sum和max_sum值。
虽然O(nlogn)已经是理论下限(因为排序需要),但在实际应用中可以考虑:
考虑更复杂的摆动条件:
最大子序和问题有许多变种:
在面试中遇到贪心算法问题时:
与面试官交流时:
贪心算法问题往往考察的是问题分解能力和算法思维,而不仅仅是编码能力。通过这三个经典问题的练习,我逐渐掌握了识别贪心问题特征的能力,这在后续的面试中给了我很大帮助。