news 2026/10/20 0:25:53

从种花问题看贪心算法:三种解法、边界处理与面试技巧

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从种花问题看贪心算法:三种解法、边界处理与面试技巧

准备技术面试刷算法题的时候,我遇到过一道看起来特别像生活场景的题:给定一个一维花坛数组,0表示空位,1表示已经种了花,要求新种下的花不能和已有花相邻,问还能不能种下指定数量的花。题目名字很直观,叫“种花问题”。第一次做它的时候,我以为要老老实实模拟种花过程,做完了才发现这题真正考的是贪心思维:如何在长度上万的花坛数组里,快速判断还能塞下几朵花。

这道题在各类在线评测平台上都有收录,属于“面试高频且代码量不大”的典型题。你如果刚开始刷贪心,拿它当入门非常合适;如果处在面试冲刺期,它也经常被当作二面里“先讲思路、再做优化”的热身题。适合谁看呢?我认为是两类人:一类是刚学会数组遍历、想找一道能“练透边界处理”的初级题的人;另一类是已经能AC,但想把“为什么能贪心”讲清楚、应付面试官追问的人。这篇内容我会从题目本质拆起,给出三种解法、完整边界条件分析和面试表达技巧,最后附一组我踩过的坑和自查清单。

1. 题目本质:一个被包装成种花的数组遍历问题

1.1 题目到底在说什么

先还原完整题面。有一个长度为 m 的整数数组 flowerbed,每个元素只有两种取值:

  • 0 表示这个位置是空的,可以种花;
  • 1 表示这个位置已经种了花。

种新花时有硬性约束:任意两朵花不能种在相邻位置上。也就是说,最终数组里不能出现连续的两个 1。题目保证输入数据本身合法,也就是原始数组里已经存在的 1 不会相邻。现在给你一个整数 n,让你判断还能不能在这个花坛里再种下 n 朵花。

举一个最典型的样例:flowerbed = [1,0,0,0,1],n = 1,答案是 true,因为可以把新花种在中间那个 0 的位置,得到 [1,0,1,0,1];但如果 n = 2,答案就是 false,因为即使把两个 0 都利用起来,中间只有连续三个 0,受两边已有花的限制,最多只能再种 1 朵。

从抽象的角度看,这道题本质是:在一个存在若干“禁用点”的一维数组里,选择尽可能多的互不相邻的空位置。数组结构、种花规则都是包装层,内核是“不相邻选点”的组合问题。理解了这一层,后面很多变体题你都能一眼看穿。

1.2 为什么这道题适合练贪心

原因有三点。

第一,它不需要复杂的数据结构。你只需要一个数组、一个计数器、一次遍历,连哈希表都用不上。这降低了理解成本,很适合拿来建立“贪心策略”的直觉。

第二,它的“最优子结构”非常明显。从左到右处理花坛时,每处理完一段,剩余部分就是一个更短的花坛子问题。你不需要回头看已经处理完的区域,只需要关心当前位置和下一两个位置,这种局部决策驱动的结构就是贪心算法的典型土壤。

第三,它有一个天然的开销极低的判定标准:能种就种。很多贪心题需要复杂的排序或者堆来辅助,而这题根本不需要,因为数组顺序已经给定,你只要决定“这朵花要不要种在这里”。

我自己刷题时的一个判断标准是:如果一道题能在 20 分钟内讲清楚“贪心为什么正确”,它就是一道好的入门贪心题。种花问题完全满足这个标准。

1.3 输入规模与约束的潜台词

很多人做题不先看数据范围,这习惯要改。花坛长度 m 通常在 1 到 10^4 这个量级,元素只有 0 和 1;n 的范围一般是 0 到 10^4。这意味着:

  • 你的最优解至少得是 O(n) 级别的遍历,O(n²) 的暴力大概率会超时;
  • 空间上,O(1) 是你应该追求的目标;
  • n 可能为 0,这是一个经常被忽略但白送分的边界条件;
  • m 可能为 1,也就是只有一个格子的花坛,这时候判断逻辑要格外小心。

另外,题目保证原始数组里没有相邻的 1,这一点很重要。如果面试官突然改口说“输入可能非法”,那就是另外一个防御性编程的问题了,面试时可以主动提一句。

2. 三种解法:从暴力到贪心的完整推导

2.1 暴力模拟:先把规则跑通再说

第一次碰到这题,大多数人会这么想:既然要判断能不能种 n 朵,那就真的去种一遍,数一数最多能种多少朵。

朴素写法很直接:从左到右遍历每个位置,如果当前位置是 0,并且左边不是 1、右边也不是 1,那就可以种,于是把这个位置改成 1,计数器加一。

def can_place_flowers_naive(flowerbed, n): count = 0 length = len(flowerbed) for i in range(length): if flowerbed[i] == 0: left_empty = (i == 0) or (flowerbed[i - 1] == 0) right_empty = (i == length - 1) or (flowerbed[i + 1] == 0) if left_empty and right_empty: flowerbed[i] = 1 count += 1 return count >= n

这个写法本身没有错误,时间复杂度也是 O(n)。但它有一个不容易察觉的问题:种完花之后,代码不会主动跳过下一格,下次循环仍然会检查已经种下花的右侧相邻格。虽然检查结果依然正确,因为种了花之后这个格子会变成 1,右侧格子在下一轮判断时左边不空,不会重复种,但这种做法实际上多做了一些无用判断。

面试时写出这个版本可以作为“正确性优先”的起点,但不要停在这里。下一步要想的是:能不能利用规则本身减少扫描次数?

2.2 区间统计法:从“数连续0”理解种花上限

如果换一个角度,把花坛看成由若干个“连续 0 段”拼接起来的结构,问题会变得更数学化。每一段连续 0 的两边是边界或者 1,这一段的长度决定了它的种花上限。

具体公式分三种情况。

中间段:两边都是 1,连续 0 的长度为 k,那么最多能种 ((k-1)//2) 朵。比如 [1,0,0,0,1],中间 k=3,最多种 1 朵;[1,0,0,0,0,0,1],中间 k=5,最多种 2 朵。

首尾段:只有一边是 1,另一边是数组边界,连续 0 的长度为 k,最多能种 (k//2) 朵。比如开头是 [0,0,0,1],k=3,可以种在位置 0,共 1 朵;开头是 [0,0,0,0,1],k=4,可以种在位置 0 和 2,共 2 朵。

全零段:整个花坛全是 0,长度为 k,最多能种 ((k+1)//2) 朵。比如全零长度 5,可以种在位置 0、2、4,共 3 朵。

这个统计法可以写成代码,但需要小心首尾和全零的区分,容易漏掉特殊情况。我一般都是把它当成推导工具,用来快速心算出某个花坛的“最大容量”,而不太推荐作为面试时的主写版本。因为面试现场写公式,不如写一个逻辑直白、边界统一的遍历版本来得快。

不过这个公式有一个很实用的价值:它可以帮你快速估算上限。比如面试官追问“如果 n 特别大呢”,你可以在遍历前先用公式判断一下整体容量上限,一旦 n 超过上限就直接返回 false,省掉一次完整遍历。

2.3 标准贪心:O(n)时间O(1)空间的跳格遍历

接下来是这道题最推荐的解法,我称它为“跳格遍历法”。核心逻辑就一句话:从左到右看,遇到已种花的位置,它的下一格因为相邻规则一定不能种,直接跳过两格;遇到空格,如果左右都空就种上,种完以后下一格也一定不能种,继续跳过两格;只有左右有一边不空时,才老老实实挪动一格。

def can_place_flowers(flowerbed, n): count = 0 i = 0 m = len(flowerbed) while i < m: if flowerbed[i] == 1: i += 2 continue left_empty = (i == 0) or (flowerbed[i - 1] == 0) right_empty = (i == m - 1) or (flowerbed[i + 1] == 0) if left_empty and right_empty: flowerbed[i] = 1 count += 1 if count >= n: return True i += 2 else: i += 1 return count >= n

我特别想强调这个 i += 2 的设计。它不只是常数优化,而是把贪心策略真正落实到了遍历节奏上。每当你在位置 i 种下一朵花,位置 i+1 无论原来是 0 还是 1,都已经丧失了种花资格,所以没必要再访问它。同理,遇到原始为 1 的位置,它的右侧邻居也必然丧失资格。这个“跳过两格”的动作是合法且安全的。

那为什么左右有一边不空时只能 i += 1?因为当前位置不能种,但下一个位置仍然可能种得下。例如花坛是 [1,0,0],遍历到 i=1 时左边是 1,不能种,但 i=2 是空位且它是末端,右侧自然为空,所以 i=2 可以种。如果这里也莽撞地 i += 2,就会漏掉这朵花。

这个版本是真正的标准答案:时间 O(n),空间 O(1),只做必要的检查。我自己面试时首推这个版本。

2.4 补零法:让边界判断不再头疼

跳格法虽然标准,但对新手有一个门槛:i==0 和 i==m-1 时的左右边界判断容易写错。一个非常讨巧的替代方案是“补零法”,思路是在原数组首尾各补一个 0,这样所有真实位置都有了左右邻居,遍历时不再需要特殊处理边界。

def can_place_flowers_with_padding(flowerbed, n): if n <= 0: return True beds = [0] + flowerbed[:] + [0] count = 0 length = len(beds) for i in range(1, length - 1): if beds[i - 1] == 0 and beds[i] == 0 and beds[i + 1] == 0: beds[i] = 1 count += 1 if count >= n: return True return count >= n

这个写法的好处是判断条件非常对称:只要连续三个位置都是 0,中间那个位置就可以种花。代码一眼就能看懂,边界错误概率几乎降到零。

代价是复制数组会带来 O(n) 的额外空间。如果你面试时先写了补零版,面试官问能不能优化空间,正好可以顺势切到跳格版。这也是一种非常好的面试节奏:先写稳定版本保证正确,再展示你理解空间优化。

我还测试过这个补零法在实际运行中的表现。因为 Python 的列表拼接和遍历本身效率不低,在 10^4 的长度下,两种写法的时间差异完全感知不到。所以如果你在笔试平台只追求 AC,补零法完全够用;但如果面试现场要求手写最优解,跳格法才是正餐。

3. 边界条件与隐藏坑点全梳理

3.1 数组开头和结尾:最容易越界的两处

种花问题的边界坑,十个人里有八个会踩在数组两端。当你检查位置 i 是否能种花时,需要看 i-1 和 i+1 两个邻居。如果 i=0,i-1 越界;如果 i=数组末尾,i+1 越界。很多新手第一次写的时候直接判断 flowerbed[i-1] == 0,然后收到一个 IndexError,瞬间心态崩掉。

处理方式一共有三种,我按推荐程度排序。

第一种是用条件表达式,也就是代码里最常见的写法:left_empty = (i == 0) or (flowerbed[i - 1] == 0)。这里利用了 Python 短路求值,i==0 时后面不会执行,所以不会越界。右侧同理。

第二种是补零法。把数组扩展成 [0] + flowerbed + [0],然后遍历中间部分。真实位置的左右邻居永远是存在的,彻底消灭越界分支。

第三种是在循环开始前手动处理两端,比如分别判断位置 0 和位置末尾,代码会变得冗长,我不推荐。

这里有一个我踩过的坑:(i == 0) or (flowerbed[i - 1] == 0)这句话的顺序很重要。如果把条件写成(flowerbed[i - 1] == 0) or (i == 0),当 i=0 时依然会先执行越界访问,照样报错。短路逻辑必须把越界判断放在前面。

3.2 全零数组与长度为1的特殊场景

全零数组是个容易被公式搞混的场景。连续 5 个 0,很多人直觉以为能种 3 朵,因为位置 0、2、4 都可以种;但连续 4 个 0 就只能种 2 朵,因为位置 0 和 2 可以种,位置 3 没法种。这个规律用公式表示就是 ((k+1)//2),注意它是向上取整的变体。你可以自己手推几个小数组验证,全零长度 1、2、3、4、5 的可种数量分别是 1、1、2、2、3。

长度为 1 的数组更特殊。flowerbed = [0] 时,这唯一一个空格可以种 1 朵;flowerbed = [1] 时,任何新花都不能再种。跳格法处理 [0] 时,i=0 满足左右皆空,种下后 i += 2,循环退出;处理 [1] 时,i += 2,循环退出。逻辑没问题,但如果你用补零法,[0] 会变成 [0,0,0],遍历 i=1 时三连 0,正常种下 1 朵,也正确。怕就怕在脑内模拟反面情况时,把 [0,0] 误判为能种 2 朵。实际上 [0,0] 种了位置 0 之后位置 1 就废了,最多 1 朵。这类小数组建议直接写进测试用例,靠记忆不靠谱。

3.3 提前返回与不可修改数组的进阶需求

两个容易被追问的点。

第一个是 n=0 要不要特殊处理。我的建议是直接返回 True,因为 n=0 意味着不需要种任何花,无论花坛长什么样,答案都是 yes。补零版里如果不提前返回,遍历虽然也能得出 count>=0 恒为 true,但会白白走一遍循环。提前返回既是优化,也体现了你对边界条件的敏感度。

第二个是“如果面试官不允许修改原数组怎么办”。跳格法里我直接写flowerbed[i] = 1,这修改了输入。有些面试场景会明确要求“不得修改输入数据”。此时可以用一个布尔变量记录上一格是否处于“刚种完花”的状态,替代对数组的写操作:

def can_place_flowers_no_modify(flowerbed, n): if n <= 0: return True count = 0 prev_planted = False for i in range(len(flowerbed)): if flowerbed[i] == 1: prev_planted = True continue left_empty = not prev_planted right_empty = (i == len(flowerbed) - 1) or (flowerbed[i + 1] == 0) if left_empty and right_empty: count += 1 prev_planted = True if count >= n: return True else: prev_planted = False return count >= n

这个版本的思路是:虽然不能改数组,但我可以用一个变量记住“上一格到底有没有花”,这个信息足以支撑左侧判断。右边的判断仍然需要看原数组的下一格,因为没有别的信息源。总体上时间 O(n)、空间 O(1),同时保证不修改输入。面试时能写出这个版本,会加分不少。

4. 面试里的算法表达与延伸思考

4.1 为什么贪心是对的:一个三句话证明

很多读者会问:跳格法里“能种就种”,怎么保证这样得到的数量就是最大值?万一某个位置不种,反而能腾出空间种更多朵呢?

这个问题如果答不好,面试官会判定你对贪心算法一知半解。我准备了一个三句话版本,亲测在面试中非常好用。

第一句,从左到右处理时,当前这个可种位置,是剩余花坛中“最靠左”的候选点,任何最优方案如果想在它右侧开始种,都不可能比从它这里开始种更靠前。第二句,如果最优解里它左侧某些位置也要种花,那么这些位置在贪心策略处理时也该已经种下了,和当前点的顺序不冲突。第三句,种下当前这朵花之后,唯一被禁用的是紧挨着的下一格;如果不种当前这朵,下一格或下下一格可能能种,但最多也只能种出一朵,并不能带来“种得更多”的收益,所以局部最优不会损害全局最优。

简单来说:在空位充足时早种不亏,晚种不多;在空位紧张时早种抢占的是本来就冗余的位置,也不会挤压远处的空间。这套直觉,配合反证法,就能构成一个完整的贪心正确性论证。

我不建议你去背诵严格数学证明,面试官更想听到的是你能用自己的话把“早种不亏”这件事讲明白。

4.2 复杂度分析的两种视角

种花问题的复杂度分析看起来很简单,但我见过不少候选人在这里翻车。

时间复杂度的直观结论是 O(n),因为无论跳格版还是补零版,每个位置最多被访问常数次。跳格版里有些位置会跳过两次,但整体循环次数仍然是 m 的线性函数,不会出现 O(n²) 的问题。

空间复杂度则要看版本。跳格版是 O(1),补零版是 O(n),因为复制了一个新数组。面试官如果追问“为什么补零版翻倍了”,你要能答出它为了统一边界判断而牺牲了额外空间。实际工程里,这种取舍很常见:代码清晰度优先,还是内存占用优先,要看场景。算法题里通常推荐先给出 O(1) 空间版本,但如果为了快速写出 bug-free 代码,补零版也完全可接受。

还有一个值得提的优化点:遍历过程中一旦 count 达到 n,可以立刻返回 true,不需要继续扫描。这个提前终止在很多测试用例里能省掉近一半的时间。我在代码里已经加了这个判断,面试时也别忘了提一句。

4.3 常见变体:环形花坛、距离约束延长与反向问题

如果面试官觉得这题太简单,通常会抛出一两个变体来压深度。我整理过几个高频变体,每个都能单独聊五分钟。

环形花坛变体:花坛首尾相接,位置 0 和位置 m-1 也算相邻。这个问题最朴素的处理方式是枚举两个基本情况:在位置 0 种花,或者不在位置 0 种花,然后在剩余的线性结构上跑原来的贪心。因为环形只是在首尾之间额外加了一条约束,所以只要固定住首尾状态,问题就退化成了线性情况。

距离延长变体:要求任意两朵花之间至少空两格,而不是一格。此时判断条件要从“左右相邻为 0”改成“左边隔一个位置为 0,右边隔一个位置为 0”,跳格距离也相应变成每次跳三格。你会发现,解法骨架完全没变,变的只是常数。这正是这类题的价值:它训练的是“规则变化的敏感度”。

反向问题变体:已知花坛长度和要种的花的数量,问至少需要多长的花坛。这个本质上就是把全零公式 ((k+1)//2) 反解成 k 的范围,属于高中数学级别的推导,但放到面试里能考察你对公式的理解深度。

遇到变体题不必慌,先判断它改变了哪个约束条件,再想这个约束条件怎么映射到原题的数组遍历上。几乎所有变体都能还原成“带额外约束的线性扫描”。

5. 输入样例与自查清单

5.1 一组值得手推的测试用例

我整理了一组我自己刷题时常跑的用例,覆盖了大部分边界情况。你写完代码后,建议把下面这张表手动推一遍,再交给在线测评。

花坛数组目标 n预期结果说明
[1,0,0,0,1]1true经典样例,种在中间
[1,0,0,0,1]2false中间最多再塞 1 朵
[0,1,0,1,0]1false每个 0 都被两侧的 1 夹住
[0,0,0,0,0]3true全零数组,种 0、2、4
[0,0,0,0,0]4false全零长度 5 上限是 3 朵
[0,0,1,0,0]2true两头各能种 1 朵
[1]0truen 为 0 恒真
[0]1true单空位可种 1 朵
[1,0]1false唯一空位与已有花相邻
[0,0]1true种位置 0 即可
[0,0]2false两个连续 0 最多 1 朵

手动推这些用例最大的价值,是能让你对“连续 0 段到底能种几朵”形成肌肉记忆。我第一次刷的时候,连续四次栽在 [0,1,0,1,0] 这种看似空位很多、实际一朵都种不下的用例上。后来养成了“先数段、再套公式、最后写代码”的习惯,这类问题基本不再犯错。

5.2 同类题单与刷题建议

种花问题在贪心题单里属于“直觉型题目”,但它和几个常见题型有很强的关联。比如跳跃类问题,核心都是“在某个位置做决策后,后续可到达范围发生变化”;区间类问题,关注的是“局部最优决策是否影响后续区间”。刷题时我建议把种花问题当作一个锚点,用来串联其他贪心基础题:分发饼干、跳跃游戏、无重叠区间、加油站等。它们共享同一个思考框架:每一步做出局部最优选择,并用反证法或替换法证明不会破坏全局最优。

如果你想把这个题刷透,我的建议是写三个版本:补零版、跳格版、不可修改输入版。每个版本写完之后,都追加两个问题:这个版本的空间复杂度是多少?如果去掉提前返回,耗时会不会有感知差异?能把这三个版本和两个问题全部说清楚,这道题才算真正吃透,而不是仅仅 AC 一次。

我自己在面试和带新人时,最喜欢的训练方式是:让对方在白板上先讲思路,再写补零版,最后口头改写成跳格版。整个过程通常控制在八分钟以内。它能快速判断一个人是背了模板,还是真的理解了这个规则。

结尾

我最早做种花问题时,也纠结过要不要用动态规划,后来才发现这道题考察的就是你能不能把一个简单的决策规则讲清楚。种花问题难的地方其实不在写代码,而在你敢不敢在“能种就种”面前停下来,认真证明它是对的。把这个证明想透之后,你会发现自己对贪心算法的理解会有一个明显的台阶式提升。

最后分享一个小技巧:刷完题之后,把这道题的所有边界用例截图存进自己的笔记里,隔一周再写一次。不要看之前的代码,看你能不能一次写出 bug-free 的跳格版。如果能,说明这类“不相邻选择”的题你已经真正拿下了。后续遇到环形变体、间隔延长变体,你也能快速迁移思路,而不是重新从头开始想。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 0:25:51

Agent判断层成本黑洞:专用小模型选型、训练与部署实战

1. 从一张失控的账单说起&#xff1a;为什么“判断”层成了 Agent 的成本黑洞过去一年&#xff0c;我帮三个团队做过智能体&#xff08;Agent&#xff09;系统的成本审计&#xff0c;场景各不相同——一个做电商客服自动回复&#xff0c;一个做代码仓库的自动巡检&#xff0c;还…

作者头像 李华
网站建设 2026/10/20 0:25:51

程序员用串流技术让PC玩主机游戏:延迟画质优化实战

1. 一个被反复追问的老问题&#xff0c;和一群程序员给出的新解法主机独占游戏到底要不要上PC&#xff0c;这个话题在玩家圈子里吵了快十年了。每次有新的独占大作发售&#xff0c;评论区必然分成两派&#xff1a;一派觉得“我买了主机就是为了玩独占&#xff0c;上了PC我买主机…

作者头像 李华
网站建设 2026/10/18 3:23:41

TensorFlow 2.x 完整实战指南:从核心机制到模型部署的全程经验

最近团队里几个刚接触深度学习的同事问我同一个问题&#xff1a;现在框架这么多&#xff0c;TensorFlow 还值得认真学吗&#xff1f;我的回答一直很直接——如果你要做的不只是“在笔记本上跑通一个模型”&#xff0c;而是要考虑数据怎么喂、训练怎么监控、模型怎么上线、上线之…

作者头像 李华
网站建设 2026/10/18 7:14:34

JDK8字符串常量池全解析:从内存布局到intern调优避坑

先说个很经典的面试题&#xff1a;new String("abc")到底创建了几个对象&#xff1f;如果你去网上搜&#xff0c;答案五花八门&#xff0c;有说一个的&#xff0c;有说两个的&#xff0c;还有说三个的。其实这题根本没有固定答案&#xff0c;得看字符串常量池里有没有…

作者头像 李华
网站建设 2026/10/17 20:10:20

SAP CRM中间件入站队列堆积?用SAT做qRFC性能体检,精准定位卡点

做过 SAP CRM 中间件项目的人&#xff0c;恐怕都见过下面这一幕&#xff1a;SMQ2 里入站队列越积越深&#xff0c;业务同事盯着我说“订单同步怎么又断了”&#xff0c;后台翻了一圈&#xff0c;任务既没报错也没完成&#xff0c;时间戳停在半小时前。最难受的还不是要背锅&…

作者头像 李华
网站建设 2026/10/18 13:54:14

上下文模式实战:多轮对话上下文管理策略与性能优化

1. 从“上下文模式”说起&#xff1a;一个被低估的工程概念第一次看到“context-mode”这个词&#xff0c;很多人会以为是某个新出的框架或者库。其实不是。它更像是一种设计思路&#xff0c;一种在系统里管理“上下文”这件事的工程模式。我最早接触这个概念是在做一个多轮对话…

作者头像 李华