1. 先把题目当成生活中的规则来读
1.1 这道题到底在问什么
LeetCode 605“种花问题”,光看名字你可能觉得是农业题,其实它是一道很典型的贪心算法入门题。我当年在某大厂机考模拟卷里第一次碰到它,第一反应是“这不就是小学奥数的间隔植树问题吗”,但真上手一写才发现,边界条件能把人绕晕。
题目背景非常生活化:你家有一个花坛,花坛是一排连续的坑位,每个坑位要么已经种了花(用 1 表示),要么空着(用 0 表示)。规则是花不能种在相邻的坑位里,因为花长大了会互相遮挡阳光。现在你手里有 n 颗花苗,问能不能在不违反规则的前提下,把这些花苗全部种下去。
用程序的语言翻译一遍:给定一个包含 0 和 1 的数组 flowerbed,再给定一个整数 n,判断是否还能种下 n 朵花。相邻的两个位置不能同时为 1,这是硬性条件。
这道题在很多公司的笔试里属于“热身题”和“筛选题”之间的一道分水岭。热身题是那种大家都会写的字符串反转,筛选题是那种一眼看上去简单、但下手全是坑的动态规划。605 就卡在中间:说难吧,它没有复杂数据结构;说简单吧,你要是没理清边界,分分钟写出一堆 if 嵌套,最后自己都看不懂。
1.2 关键条件其实就三条
新手读题最容易犯的错,是把简单规则想复杂。我带着很多基础薄弱的朋友刷过这道题,总结下来,题目的有效信息只有三条:
- 花坛里的坑位只有两种状态:空(0)和已种花(1)
- 相邻两个坑位不能同时种花,即不能出现连续的两个 1
- 花坛初始状态已经固定,你只能在空的坑位里加花,不能拔掉已有的花重新安排
尤其要注意第三条。很多人第一反应是“我先把所有空位统计出来,然后隔一个种一个”,这个思路在空花坛(全 0)时成立,但一旦数组里有零星分布的花,直接套用“隔一个种一个”就会误判。比如 [0, 1, 0, 0, 1, 0] 这个数组,看起来空位有 4 个,但实际能再种的花只有 0 朵,因为每个空位都被已有的花“物理隔离”了。
搞清楚这三条规则之后,难题就变成了一个纯模拟问题:从左到右扫描花坛,遇到能种的位置就种,统计种下去的数量,最后看够不够 n。
2. 官方题解的“弯”到底弯在哪
2.1 官方分段统计法的问题
这道题在力扣上的官方题解,以及很多大神的题解,普遍采用“分段统计连续 0 的个数”的方法。思路是:把数组按已有的 1 切分成若干段,每段连续 0 的长度为 count,然后套一个公式去计算该段最多能种多少花。
公式大致长这样:
- 如果这段在数组中间,左右都被 1 夹住,能种的数量是
(count - 1) / 2 - 如果这段贴着数组左边界,能种的数量是
count / 2 - 如果这段贴着数组右边界,能种的数量也是
count / 2
我第一次看这个解法时,整个人是懵的。凭什么中间要减 1?除法怎么取整?如果连续 0 有 4 个,为什么中间段只能种 1 朵而不是 2 朵?每一个问号背后都是一个边界场景,光靠背公式很难在考场上快速写对。
更难受的是,这种解法对基础的初学者不友好。它把问题抽象成了“数数 + 套公式”,如果公式记混了,代码就彻底崩了。我在教别人的时候发现,十个新手看官方题解,至少有六个人会问“为什么右边界的 count / 2 和中间段不一样”。不是他们笨,而是这个解法确实跳过了直观逻辑,直接蹦到了数学结论。
2.2 分段统计法为什么容易写错
我们手动推一遍官方思路,你就知道坑在哪了。
假设数组是 [1, 0, 0, 0, 0, 1],中间连续 4 个 0,左右都被 1 挡住。你用手比划一下:第 2 个位置(从左往右数)可以种花,种了之后第 3 个位置就不能种了,第 4 个位置可以种,第 5 个位置又不能种。所以最多能种 2 朵。公式(count - 1) / 2在这里是(4 - 1) / 2 = 3 / 2 = 1,但实际能种 2 朵,公式算少了。
问题出在哪?官方题解里的 count 指的是“被两个 1 夹住的纯连续 0 段”,但在首尾位置时边界条件的处理方式不同,要单独判断。而且不同的题解对“边界”的定义还不完全一样,有的人把数组外也想象成一个 1,有的人想象成一个 0,想象方式不同,公式就不同。
这就导致一个很尴尬的局面:初学者看懂了官方题解的原理,但一写代码,要么忘了对首尾特殊处理,要么把公式的减一只加在中间段却忘了首尾段,最终结果要么少算要么多算。面试时候本来时间就紧,再在这种边界细节上翻车,心态直接崩。
所以我的建议很明确:如果你想在机考里快速拿下这道题,或者说你是基础薄弱型选手,不要一上来就啃官方题解那套分段统计法。换一条更直白、更贴近生活直觉的路子。
3. 新手最容易理解的核心思路:能种就种
3.1 贪心思想在这道题里的正确姿势
“能种就种”这个策略,专业术语叫贪心算法。贪心的核心就一句话:每一步都做当下看起来最优的选择,并且这个选择不会堵死后面的路。
种花问题恰好满足贪心的适用条件。你可以把花坛想象成一排座位,规则是两个人不能坐一起,现在问你最多还能安排几个人入座。从最左边开始,看到一个空位,先瞄一眼左边和右边有没有人,如果两边都没人,立刻坐下去;如果左边或右边有人,就跳过这个位子,继续往右看。
为什么这是对的?因为从左到右扫描时,如果你在当前空位种花,最多只会影响右边紧挨着的那个位置(左边的位置已经扫描过,不会再改变);而如果你不种,这个空位也不会对更右边的位置产生任何好处,反而白白浪费了一个可用坑位。所以“能种就种”不但不会减少总容量,反而是让总容量最大化的做法。
对比一下官方的“分段统计”,你会发现贪心方案几乎不需要记忆任何公式。你只需要记住三个字的动作:能种就种。剩下的事情就是写代码判断“能不能种”。
3.2 边界位置的统一处理逻辑
“能种就种”看起来简单,但真正写代码时,第一个拦路虎就是数组越界。比如你现在站在下标为 0 的位置,你想检查左边有没有花,一访问 i - 1 就变成 -1,直接报错;站在最后一个位置,访问 i + 1 又会越界。
这里有个很好用的技巧:把数组想象成无限延伸的假想花坛,在真正数组的左右两边各补上一个 0(即假想坑位已经有花)。这样判断每个位置时,都统一检查 left 和 right,不用再区分什么“首位置只看右”“尾位置只看左”。
但新手直接写“假想补位”又觉得抽象。我建议用更直接的写法:先写死一个检查函数canPlace,专门判断某个位置能不能种花。在这个函数里,把“越界”当成“有花”来处理。什么意思呢?如果一个位置越界了,我们就认为它已经有花了,即不能种。用一个三元表达式或者带短路逻辑的条件判断就能写出来。
判断一个空位能不能种花,需要同时满足三个条件:
- 当前这个位置的值必须是 0(空位才能种)
- 左边的位置不存在,或者左边的位置是 0
- 右边的位置不存在,或者右边的位置是 0
注意这里的“不存在”就等价于“视为有花”,因为越界位置不可能真的种花,但它会阻断相邻性。这个逻辑用代码写出来非常干净,不管扫到数组的哪个位置,判断逻辑完全相同,完全不需要区分首尾。我觉得这是比官方题解更适合新手的原因之一:没有一堆分支,没有语境切换。
4. 对照代码逐行拆解(多语言版本)
4.1 C++ 版本:短小直观,面试够用
以下是我推荐给初学者的 C++ 写法。它不追求奇技淫巧,就是老老实实扫描 + 判断。
class Solution { public: bool canPlaceFlowers(vector<int>& flowerbed, int n) { int count = 0; // 记录最多还能种下多少朵花 int m = flowerbed.size(); for (int i = 0; i < m; i++) { // 当前必须是空位 if (flowerbed[i] == 0) { // 左边越界视为已有花,所以用 true 跳过 bool leftEmpty = (i == 0) || (flowerbed[i - 1] == 0); bool rightEmpty = (i == m - 1) || (flowerbed[i + 1] == 0); if (leftEmpty && rightEmpty) { // 种花,把当前位置改成 1,避免影响后续判断 flowerbed[i] = 1; count++; // 提前结束:已经够种了,没必要继续扫描 if (count >= n) { return true; } } } } // 循环结束还没达到 n,说明种不下 return count >= n; } };这个代码有几个细节新手需要注意。第一,bool leftEmpty = (i == 0) || (flowerbed[i - 1] == 0);这一行的||有短路作用:当i == 0为 true 时,flowerbed[i - 1]根本不会被执行,所以不会越界。同理,(i == m - 1) || (flowerbed[i + 1] == 0)在最后一个位置时也不会越界。这就是把“越界视为有花”的落地实现。
第二,我种完花后直接修改了原始数组flowerbed[i] = 1。这样做的好处是,后续位置判断时天然知道左边刚才种过花了,不用再额外用一个布尔变量记住“我刚种过”。坏处是修改了传入的参数,如果在面试里和面试官讨论,最好说明一下这个副作用。对于机考笔试来说,这个副作用无所谓,函数结束后数组就没有利用价值了。
第三,提前终止的if (count >= n) return true;是个好习惯。如果 n 是 3,而你扫描到一半已经种满 3 朵,后面的位置根本不用看,直接返回 true,能省不少时间。
4.2 Java 版本:逻辑一致,注意数组长度
class Solution { public boolean canPlaceFlowers(int[] flowerbed, int n) { int count = 0; int m = flowerbed.length; for (int i = 0; i < m; i++) { if (flowerbed[i] == 0) { boolean leftEmpty = (i == 0) || (flowerbed[i - 1] == 0); boolean rightEmpty = (i == m - 1) || (flowerbed[i + 1] == 0); if (leftEmpty && rightEmpty) { flowerbed[i] = 1; count++; if (count >= n) { return true; } } } } return count >= n; } }Java 和 C++ 在逻辑上几乎一一对应。唯一提醒的是 Java 的||运算符同样是短路求值,所以这段代码在边界位置不会抛出ArrayIndexOutOfBoundsException。很多初学者担心“我写 i - 1 会不会越界”,其实只要把边界判断放在前面,后面的访问就不会发生。
4.3 Python 版本:简洁但容易写出“优雅但错误”的代码
class Solution: def canPlaceFlowers(self, flowerbed: List[int], n: int) -> bool: count = 0 m = len(flowerbed) for i in range(m): if flowerbed[i] == 0: 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 return count >= nPython 的or也是短路逻辑,因此可以放心用。另外提醒一点,Python 的列表支持负数索引,如果你不把i == 0放在前面,当 i 等于 0 时访问flowerbed[-1]不会报错,但会取到数组最后一个元素,这在逻辑上是错的,而且很难排查。所以边界判断千万别省,也别自作聪明用负数索引去规避。
三个语言版本核心逻辑完全一致,这也是我推荐初学者的原因:算法思想懂了,任何语言都只是换一层皮。你不需要背某个语言的特定写法,只要把“能种就种”的判断逻辑吃透,考场上哪怕临时换语言也能写出来。
5. 自己动手画图验证的完整过程
5.1 手动模拟一个基础用例
纸上谈兵没有意思,我给你演示一个从头到尾的完整模拟过程,你拿着笔照着画一遍,绝对比看代码十遍都管用。
假设flowerbed = [1, 0, 0, 0, 1],n = 2,问你能否种下 2 朵花?
按照我们“能种就种”的策略,从左往右扫描:
- i = 0:值是 1(已种花),跳过
- i = 1:值是 0,检查左边 flowerbed[0] = 1(有花),所以不能种,跳过
- i = 2:值是 0,左边 flowerbed[1] = 0,右边 flowerbed[3] = 0,两边都空,可以种。把位置 2 改为 1,count 变成 1,此时 count = 1 < 2,继续
- i = 3:值是 0,左边 flowerbed[2] = 1(刚种的),右边 flowerbed[4] = 1,两边都不满足,不能种,跳过
- i = 4:值是 1,跳过
循环结束,count = 1,小于 2,返回 false。
这个用例非常经典。你注意看,数组里一共有 3 个空位,但能种的只有中间那一个。如果你用“隔一个种一个”的直觉,很容易误判成能种 2 朵。这就是为什么必须老老实实做边界检查。
5.2 再模拟一个容易误判的长数组
再来一个更复杂的例子:flowerbed = [0, 0, 1, 0, 0, 0, 0, 1, 0],n = 2。
手动走一遍:
- i = 0:值是 0,左边越界视为有花,右边 flowerbed[1] = 0,右边空。左边不空,所以不能种。等等,越界不是视为有花吗?那左边不空就说明 i = 0 不能种。幸好我们还有这个判断。
- i = 1:值是 0,左边 flowerbed[0] = 0,右边 flowerbed[2] = 1,右边有花,不能种
- i = 2:值是 1,跳过
- i = 3:值是 0,左边 flowerbed[2] = 1,不能种
- i = 4:值是 0,左边 flowerbed[3] = 0,右边 flowerbed[5] = 0,可以种。位置 4 改为 1,count = 1
- i = 5:值是 0,左边 flowerbed[4] = 1(刚种的),不能种
- i = 6:值是 0,左边 flowerbed[5] = 0,右边 flowerbed[7] = 1,右边有花,不能种
- i = 7:值是 1,跳过
- i = 8:值是 0,左边 flowerbed[7] = 1,不能种
循环结束,count = 1,小于 2,返回 false。
这个例子很多人第一次做的时候会数出 2 朵,因为空位特别多,而且右下角那个位置感觉能种。但实际上位置 6 和位置 8 都被位置 7 的现有花隔离了,真正能种只有位置 4。手动走完一遍,你就知道为什么“数空位”这么不可靠。
5.3 提前终止能省多少事
还是上面那个例子,如果 n = 1,那么扫描到 i = 4 种下第一朵花后,count = 1 >= n,直接返回 true,后面 4 个位置根本不用再看了。这种提前终止在实际工程里意义不大,但在机考笔试题里是个人都会紧张,能少写几步逻辑就少几步,Bug 出现的概率也会降低。
6. 复杂度分析与为什么这题能“剪枝”
6.1 时间复杂度和空间复杂度
这段代码的时间复杂度很简单:只需要遍历一次数组,每个位置最多被访问常数次(当前位置、它左右各一次),因此时间复杂度是 O(n),n 是花坛长度。
空间复杂度:你只用了两个布尔变量和一个整数变量,没有借助额外数组、哈希表、栈之类的结构,所以空间复杂度是 O(1)。
这两个复杂度指标在大厂机考里属于“标准解法”级别。很多面试官看到你写 O(1) 空间,会觉得你至少不是一个只知道暴力破解的人。虽然没有动态规划那种让人眼前一亮的感觉,但对笔试来说完全够用。
6.2 提前终止能否改变最坏情况复杂度
有人可能会问,你加了提前终止,时间复杂度是不是就变成 O(k) 了?其实不会。如果 n 特别大,比如 n 等于数组长度的一半甚至更大,你需要遍历完整个数组才能得出结论,所以最坏时间依然是 O(n)。提前终止只在“正好种满 n 朵但数组剩余部分仍然很长”时才有显著收益。比如前 20% 的长度里已经种满了,后面 80% 都不用看。
这种优化在题目里属于“锦上添花”,不算核心考点。但我会把它写在代码里,因为面试官偶尔会问“这个函数可以优化吗”,你直接指着提前终止说“当达到目标数量后就不再扫描”,能展示出性能敏感的意识。
6.3 贪心解法与官方解法复杂度的对比
官方“分段统计”解法的时间复杂度也是 O(n),空间复杂度是 O(1)。也就是说,两种方案在理论效率上没有区别。既然效率一样,选哪一个就完全看你对哪个更熟悉、更不容易写错。对基础薄弱的选手来说,贪心解法理解成本低、边界统一、代码量还少,显然是更合适的选择。
7. 实战中容易踩的五个坑
7.1 坑一:只检查右边,忘了左边
我见过太多初学者写这道题时,代码长这样:
if (flowerbed[i] == 0 && flowerbed[i + 1] == 0) { flowerbed[i] = 1; count++; }这种写法只看了右边没有花,完全不看左边。在空数组[0, 0, 0]这种全零场景下碰巧能过,但只要数组里有零星已有的花,结果就错了。比如[1, 0, 0],你站在 i = 1 时右边是 0,按这个逻辑就能种,可实际上左边已经有一朵花了,根本不能种。
所以必须养成习惯:判断能不能种花,左右两边都要看。每次多写一个条件,权当给自己充了个保险。
7.2 坑二:觉得数组外也能种花
有的同学想着“我能不能在数组边界外面种一朵,这样首位置就不用考虑左边了”,这是没有意义的。题目限定花坛数组就是全部空间,数组外不存在坑位。咱们在代码里把越界当成有花,只是为了让逻辑统一,而不是真的认为边界外有花。这两个概念别混淆。
7.3 坑三:n = 0 时的特殊处理漏掉
当 n = 0 时,你手里没有花苗,那答案必然是 true。有些写法在最前面加一行if (n == 0) return true;,这是正确的,不过即便没有这一行,我们的代码也能自然处理:count 从 0 开始,循环结束count >= n即0 >= 0,返回 true。所以“加不加特判”取决于你个人风格,但新手容易在 n = 0 时手动看代码,觉得逻辑有问题,实际上没毛病。
7.4 坑四:对空数组或长度为 1 的数组处理不当
题目虽然规定了flowerbed至少包含一个元素,但面试官或者你自己测试时,可能会拿[0]或[1]这种极端用例来验证。对于[0],m = 1,i = 0 时左边 i == 0 为 true,右边 i == m - 1 为 true,两边都空,可以种 1 朵。对于[1],i = 0 时当前值不是 0,跳过,count = 0。这两个场景我们的代码都能正确处理,但你心里最好有个数,遇到极端用例不用慌。
7.5 坑五:拿“计数空位”来代替真正的条件判断
这是最隐蔽的坑。有的同学背了官方公式后,写出来的代码是统计所有空位数量再套公式,结果在复杂场景下公式记忆错乱。比如[0, 0, 1, 0, 0]这种,空位数是 4,但实际能种多少?左边两个空位中只能种 1 个,右边两个空位中也只能种 1 个,总共 2 个。公式好算,但一旦数组变成[1, 0, 0, 0, 0, 1, 0, 0]这种有不对称边界的,公式就很容易搞混。
我们的贪心解法不存在任何公式,条件写死了“左右都空才种”。哪怕你是对着代码抄,抄完你也能用自己的话解释每一行在干嘛。
8. 举一反三:同类贪心题的面试扩展玩法
8.1 从“种花”跳到“跳跃游戏”
如果你能完全掌握“能种就种”的贪心思想,那么恭喜你,LeetCode 上另一类贪心题的门也打开了。最典型的就是“跳跃游戏”系列。
跳跃游戏的问题是:给你一个数组,每个元素代表你在该位置最多能往前跳多远,初始站在下标 0,问能不能跳到最后一个位置。这类题的核心也是“每走到一个位置,我都试图更新我能到达的最远距离”,只要最远距离能覆盖到终点,就返回 true。和种花问题一样,它不需要回溯,不需要全局枚举,每一步做局部最优就能推出全局最优。
我建议你把这两道题放在一起刷。先做 605 找感觉,再做跳跃游戏体会“覆盖范围”这个概念的进化版,你的贪心算法水平会有一个质的提升。
8.2 “柠檬水找零”是另一个不错的练兵场
LeetCode 860“柠檬水找零”也是贪心入门题。场景是你卖柠檬水,一杯 5 美元,顾客可能拿 5 美元、10 美元、20 美元来付账,你需要判断能不能给每位顾客正确找零。贪心点在于:收到 20 美元时,优先用 10 美元 + 5 美元找零,而不是三张 5 美元,因为要尽量保留 5 美元这种“万能零钱”。
这道题放在学习路径里,能帮助你建立“局部选择会如何影响后续机会”的敏感度。你会发现,种花问题里“能种就种”和找零问题里“能省就省”,看似相反,实则都是为了让未来更从容。这就是贪心算法的一体两面。
8.3 一道变形题的面试官提问套路
面试官如果把 605 作为暖场题,通常会追问三种变体:“如果花坛不是数组,而是一个环形花坛,怎么处理?”“如果相邻的距离限制不是 1,而是 2,怎么修改代码?” “如果我想返回具体的种花方案,而不只是想得到一个布尔值,怎么改?”
环形变体的思路也好办:把环形花坛拆成“首尾相邻”的场景,只需特判首尾两个位置即可,本质还是同一个扫描框架。距离限制改成 2 的话,检查左右两位都要空。返回具体方案更简单,我们已经在原数组上种花了,方案已经体现在数组里。
如果你能把这些扩展问题的思路都提前想一遍,面试时被问到任何变体,你都不会慌乱。在我看来,刷题的意义不是背下某一道题,而是把一类算法的思维方式内化。
9. 我的面经总结与最后建议
这道 LeetCode 605 大家千万别小看它。我在模拟面试中见过不少候选人,数组题目造个轮子玩得飞起,结果在这道“简单题”上翻船,原因都是边界处理不严谨。基础差不是问题,问题是明明可以直接靠“能种就种”四字真言解决的问题,非要去背官方公式,最后把自己绕进去。
我个人的建议是:凡是遇到简单题,先问自己能不能用一个生活中的常识来理解。如果能把题目翻译成“一排座位能不能再坐人”“一排停车位能不能再塞一辆车”,你就不太可能被公式带偏。代码这种东西,思路活了,写出来是顺手的事。
最后分享一个我自己的小习惯。我刷完每道题后,会手动在纸上跑三个用例:全 0 数组、全 1 数组、以及一个“只有开头有空位”的数组。这三个用例基本能覆盖你能遇到的所有边界陷阱。种花问题也不例外。你把这步养成的习惯带到后面每一题,刷题效率不会差的。
希望这篇嚼碎了讲的方法能帮你稳稳拿下这道题。下一道题,咱们继续一起手撕。