资讯详情

资讯详情

01序列间隔检查:一次遍历解决力扣1437边界问题

前几天有读者在后台问我一道看起来很简短的题给你一个01序列以及一个整数k要判断是不是所有1都至少间隔k个元素。这不光是力扣1437的原题英文名 Check If All 1s are at Least Length K Places Away也是很多公司面试时用来考察代码基本功的常客。我第一次见这题时觉得太简单了结果一写就错。问题就出在“至少间隔k个元素”这句话里藏着两个容易混淆的细节索引差和中间元素个数总差1k0时又会颠覆不少人的直觉。题本身不复杂数组里只有0和1你要保证任意两个1之间至少隔着k个0。换句话说只要出现距离过近的1就返回False。但越是这种简单的题越能看出一个人对边界条件的敏感度。这篇文章我会把完整思路、代码实现、常见坑位、延伸变形一次讲透。适合准备面试的选手也适合刚接触数组遍历和状态记录的新手尤其是那种“看题解秒懂自己写就报错”的同学重点看第4节。另外说明一下我整理这篇时参考了题解圈里一些常用讲法比如负雪明烛在力扣讨论区里强调过的观点先把题目条件翻译成严格的不等式再落到代码上。这个习惯确实能避免一半的笔误。下面我们直接开始。1. 先看题这个01序列到底在要求什么1.1 原题还原附三组手算示例先把原题描述写明白。输入是一个只包含0和1的数组nums以及一个整数k。你需要判断是否满足数组中任意两个1之间都至少相隔k个元素。满足返回true不满足返回false。注意是“任意两个1之间”不是“两个相邻的1之间”。虽然你检查的时候只需要看相邻的1但本质上和任意是等价的。如果相邻的两个1都满足间隔那更远的一对自然也满足。这一点是后续设计一次遍历算法的前提。举例1nums [1,0,0,0,1,0,0,1]k 2。三个1分别在下标0、4、7。第一对1中间隔着3个0第二对1中间隔着2个0都大于等于2所以返回true。举例2nums [1,0,0,1,0,1]k 2。三个1在下标0、3、5。第一对中间有2个0满足第二对中间只有1个0不满足返回false。举例3nums [1,1,1,0]k 0。k0表示1之间可以不隔任何元素也就是允许1紧挨着。三个1都满足条件返回true。很多人第一次看到k0会懵其实代入“0个间隔元素”就通了。你可以发现这个题的本质就是对每一对相邻的1检查中间那段0的数量是否够k。中间元素不能是1因为如果是1就说明这两者不是相邻的1。所以“01序列”这个约束其实大大简化了判断。1.2 题目背后想考察的三个基本功这种题在面试里属于“Easy”档难点不在算法设计而在能否一遍写对。面试官想看到的不是你背题而是你有没有稳定的编码习惯。第一个基本功是数组遍历。这里不需要双指针、不需要二分就是一个简单的for循环。但能不能想到“边遍历边记录上一个1的位置”取决于你是否理解遍历过程中的状态维护。第二个基本功是状态定义。你要用一个变量last保存上一个1的下标每次遇到新的1都拿当前下标和last做比较。这种“上一个合法位置”的思路在很多题里都会复用比如判断字符串中相同字符之间的距离或者检查相邻事件是否满足最小时间间隔。第三个基本功是边界处理。空数组、纯0数组、只有一个1的数组、k0这四种情况看起来不起眼但往往是区分AC和WA的分水岭。很多人的代码在普通用例上没问题一跑到边界就崩就是缺少这些自测用例。从业务角度看这个题也能映射到真实场景。比如机房里有01序列表示机柜是否被占用要求两台被占用的机柜之间至少隔k个空机柜目的是散热或隔离故障域。再比如排班系统里两个值班日之间至少要间隔k天休息。所以别小看这道题它背后是一类“最小间距约束”问题。2. 两版解法的取舍暴力扫 vs 只记上一次的12.1 暴力法能过但面试官不会放过你最直觉的做法是遇到一个1就往后看k个位置检查这k个位置里还有没有1。如果看到了1说明间隔不够直接返回false。直到遍历完整个数组。这个思路是对的而且非常好理解。面试时如果你先把暴力解讲出来再主动优化反而加分。它的时间复杂度是O(n*k)空间复杂度O(1)。leetcode上如果k比较小甚至也能跑过去。但面试官大概率会追问一句如果k非常大怎么办最坏情况下k接近n每一轮检查都会扫到数组末尾整体变成O(n^2)显然不够优雅。另外暴力法在写的时候也容易踩坑到底检查i1到ik还是i1到ik1这又回到了“间隔k个元素到底是多少个0”的问题。如果你用暴力法扫的位置没数对照样错。所以即使写暴力版我也建议你用统一的下标差思路去判断而不是机械地数k个格子。2.2 一次遍历的核心把“间隔k个元素”翻译成“索引差大于k”我们要找的最终解法其实是单次遍历加一个标记变量。先定义last为上一个1的下标初始时还没有任何1我们用一个特殊值表示不存在比如-1。当遍历到下标i且nums[i]等于1时计算diff i - last。注意diff是下标差不是中间元素个数。中间元素个数应该是diff - 1因为下标i和last之间夹着的元素是last1到i-1一共有i-last-1个。题目的要求是“中间至少间隔k个元素”。由于数组里只有0和1且在检查相邻1时中间不可能再有1所以这些中间元素只能全是0。于是条件可以翻译成diff - 1 k等价于diff k 1或者说 diff k。所以不满足条件等价于diff k。这一小段数学翻译是整个题的核心。当你把“间隔k个元素”转换成“索引差大于k”之后后边的代码就变成一行判断了。负雪明烛在题解里反复强调的也是这个习惯先搞清楚条件严格的长什么样再动手写if不然很容易写出差一个符号的bug。2.3 双指针方案为什么容易写翻车还有一种思路是双指针。用left和right分别指向相邻的两个1初始时left指向第一个1right继续往右找下一个1。如果right - left k说明间隔不够返回false否则把left移动到right继续找下一对。这个逻辑本身没问题但写起来比单指针版本麻烦。你需要先写一个循环跳过前导0找到第一个1还要处理right找不到下一个1的情况更麻烦的是如果整个数组只有一个1你需要保证流程结束时不误判。很多人写着写着就多了一堆flag和break代码长度变大出错率也上去了。双指针并不是错的但它在这里属于“杀鸡用牛刀”。单指针版本只需要一个last变量不需要理会“第一个1到底在哪”只需要在循环里用last ! -1判断即可。简洁性直接决定了代码的可读性面试时越短越不容易暴露bug。2.4 三种方案横向对比方案时间复杂度空间复杂度代码量犯错风险推荐度暴力法O(n*k)O(1)短中容易数错间隔范围可作为过渡思路单指针记录lastO(n)O(1)最短低只需注意k0和首个1最优解双指针O(n)O(1)较长较高需要处理多个边界不推荐从表格能看出单指针方案全面胜出。其实这类“检查相邻合法元素间距”的题绝大部分都能用“记录上一个位置”的模板解决。你只要练熟这一种很多变形题都能直接套。3. 手把手实现一次AC的正确姿势3.1 动手前先回答三个问题我在本地刷题时有个习惯任何题写代码之前先拿几句话把题目条件重新组织一遍。这道题我会依次回答下面三个问题。第一“间隔k个元素”到底是什么意思我的答案是相邻两个1之间至少要k个0注意这个k是“中间元素个数”不是“下标差”。所以判断时必须使用diff - 1 k这个式子或者它的等价形式diff k。第二第一个1要不要比较不要。因为它前面没有其他1没有“相邻的一对”。实现上就靠last -1来识别。第三数组里只有一个1或者一个1都没有返回什么返回true。因为“任意两个1之间”这种约束对不存在的配对是天然成立的数学上叫虚真。面试时如果问到这个别犹豫。这三个问题想清楚后代码结构其实已经固定了一个遍历循环一个last变量一个if判断。3.2 Python逐行实现与解释以下是力扣风格的Python解法from typing import List class Solution: def kLengthApart(self, nums: List[int], k: int) - bool: last -1 for i, num in enumerate(nums): if num 1: if last ! -1 and i - last k: return False last i return True逐行拆解一下。last -1表示“还没有遇到过1”。注意这里不能用last 0代替原因后面第4节会专门讲。循环内部只有当num等于1时才需要处理。如果是0直接跳过因为0不影响1和1之间的间隔。遇到1后先判断last是否已经有值。如果last不是-1说明当前1不是第一个1它可以和上一个1组成一对此时计算i - last。如果i - last k说明两个1的下标差不超过k也就是中间元素个数小于k直接返回False。例如nums [1,0,1]k 2i 2last 0i - last 22 2成立返回False。这个用例等价于中间只有1个0小于要求的2确实是False。如果判断通过说明当前1和上一个1的间隔是合法的。此时把last更新为i继续往后遍历。如果整个数组遍历完都没有触发return False说明所有相邻1的间隔都满足要求返回True。这段代码还有一个细节检测到违规后立即返回False不需要把数组遍历完。这是经典的“早退出”写法既省时间又省脑力。3.3 面试考Java或C时怎么快速迁移用Python写习惯了如果面试官要求用Java或C千万不要慌逻辑完全一样。Java版本如下class Solution { public boolean kLengthApart(int[] nums, int k) { int last -1; for (int i 0; i nums.length; i) { if (nums[i] 1) { if (last ! -1 i - last k) { return false; } last i; } } return true; } }需要注意Java里数组没有enumerate这种语法糖直接用下标访问即可。判断条件里的i - last是int运算不存在溢出问题。这里唯一要注意的是别把i - last k写成i - last k那个坑在第4节讲。C版本也顺手给一下class Solution { public: bool kLengthApart(vectorint nums, int k) { int last -1; for (int i 0; i nums.size(); i) { if (nums[i] 1) { if (last ! -1 i - last k) { return false; } last i; } } return true; } };C的vector可以直接用size()但要注意nums.size()返回的是size_t无符号类型和int的i比较时一般没问题但如果你写i nums.size() - 1这种表达式就要小心下溢。这个题里我们不需要所以还好。3.4 手算验证把示例完整跑一遍写完代码别急着提交先在大脑里跑两个用例尤其是第二个返回false的用例。先跑nums [1,0,0,0,1,0,0,1]k 2。i0时num1last-1所以跳过判断last更新为0。i1、2、3都是0继续。i4时num1last0i-last4。判断4 2不成立说明间隔足够last更新为4。i5、6是0跳过。i7时num1last4i-last3。判断3 2不成立返回true。结果是正确的。再跑nums [1,0,0,1,0,1]k 2。i0时last-1更新last0。i3时num1last0i-last33 2不成立last更新为3。i5时num1last3i-last22 2成立直接return False。和预期一致。手算的意义在于确认对条件的翻译没有偏差。很多时候你代码写对了但if里的符号写反了手算一遍就能发现。3.5 复杂度与边界结论时间上只需要遍历数组一次每个元素最多被访问一次所以时间复杂度是O(n)。空间上只用了last这一个变量空间复杂度是O(1)。这个复杂度已经是最优的了因为理论上至少要把每个元素读一遍才能确认所有1的位置。还有一个容易被忽略的结论这个题不需要考虑“当前1和后面所有1的距离”只考虑和上一个1的距离就够。因为数组按顺序遍历后面的1还没有出现等它出现时自然会和当前1比较。这种把全局约束拆成“局部相邻约束”的思维方式是很多贪心算法的雏形。4. 踩坑实录90%的人会错在这些地方4.1 最经典的符号错 还是 这个题最大的坑就是判断符号。很多人写成了这样if last ! -1 and i - last k: return False表面上看i - last k表示下标差小于k好像也是“距离太近”的意思。但代入例子就露馅了。nums [1,0,0,1]k 2。下标差是3中间隔了2个0满足条件应该返回true。用错误代码判断i-last 33 2不成立所以不会返回false最终返回true。看起来结果碰巧对。再换一个用例nums [1,0,0,1]k 3。下标差还是3但中间只有2个0明显不满足“至少间隔3个0”应该返回false。错误代码判断3 3不成立返回true。这就错了。问题在于i - last是下标差而我们关心的是中间元素个数也就是i - last - 1。正确的条件“中间元素个数小于k”应该写成i - last - 1 k这个式子整理一下就是i - last k 1因为i、last、k都是整数所以等价于i - last k。所以正确写法是不是。我自己的记法是只要下标差没有超过k就说明中间元素最多k-1个不够数。想明白这一点就再也不会写反了。4.2 k0不是反例是照妖镜k0是一个很特殊的边界。题目要求所有1至少间隔0个元素翻译成人话就是允许1紧挨着。因此[1,1,1]配合k0应该返回true。用标准代码判断i1last0i-last11 0不成立通过i2last1i-last1也不成立最终返回true。正确。但如果你在代码里额外写了“看到相邻两个1就返回false”的特殊逻辑比如if (nums[i] 1 nums[i-1] 1) return false那k0时就会误杀。不少人习惯于给连续1加特判觉得这样“更稳妥”实际上恰恰相反。正确思路是用统一的不等式让k0自然地被处理掉。k0这类用例就是照妖镜谁的逻辑是拼凑的一测便知。4.3 数组中只有一个1时别误杀假设nums [0,0,1,0,0]k 5。数组中只有一个1按照题意应该返回true因为没有“两个1之间”需要检查。如果你把last初始化为0而不是-1会发生什么遍历到i2时第一个1出现这时last0i-last2然后判断2 5成立直接返回false。这就把唯一一个1误判成违规了。所以last的初始值必须能表示“尚不存在1”。用-1是常见做法因为下标从0开始-1永远不会和真实下标冲突。同时判断条件里必须加上last ! -1确保第一个1只设置状态、不参与比较。4.4 初始值last到底该设成多少承接上面last可以用-1也可以用Integer.MIN_VALUE或float(-inf)这种“不可能的极小值”。但-1最简单因为数组下标最小是0-1天然代表“不存在”。还有一种做法是初始化为-n然后不检查last是否为-1直接用i - last k来判断。对第一个1来说i - last会非常大一定大于k所以不会误判。但这种方法隐藏着隐患如果k也很大比如k 10^9而n比较小第一个1的i - last可能不够大导致误判。而且把“不存在”编码成一个很大的负数可读性不如显式的-1加判断。我推荐老老实实写last -1和last ! -1。虽然多一个判断条件但语义清晰不容易出错。4.5 拿来即用的自测用例清单在提交代码之前建议把下面这张表整组跑一遍。这些用例几乎覆盖了所有边界。测试用例k期望结果检查意图[1,0,0,1]2true基础满足用例[1,0,0,1]3false中间0不够[1,0,1]1true正好间隔1个0[1,0,1]2false下标差等于k的特例[1,1]0truek0允许紧挨[1,1]1false最小违规[1]100true单1不误杀[]5true空数组[0,0,0]5true全0数组[0,0,1,0,0,0,1]3true中间有3个0[0,0,1,0,0,0,1]4false中间0少一个特别注意[1,0,1]和k2这一组。下标差是2中间元素个数是1小于2所以返回false。用代码判断就是i-last22 2成立返回false。很多人栽在这里因为他们把“下标差”和“中间个数”混为一谈了。5. 举一反三这套标记法的通用套路5.1 变形题1检查所有0的间隔把题目稍微改一下给你一个01序列判断所有0是否至少间隔k个元素。其实只需要把代码里判断num 1改成num 0其他逻辑完全不变。last -1 for i, num in enumerate(nums): if num 0: if last ! -1 and i - last k: return False last i return True这说明这个解法真正关心的不是“1”而是“你指定的目标元素”。实际业务里你可能是要检查两台故障机器之间的距离也可能是检查两次发版之间的间隔天数。只要把它抽象成“某种元素的间距不能太近”这个模板就能用。5.2 变形题2环形场景与首尾相接的1如果序列首尾相接比如这些1分布在一个环形队列上那么第一对要检查的就不是“遍历出现的前两个1”而是“第一个1和最后一个1隔着环绕一圈的距离”。处理思路是先按线性方式检查中间所有相邻1最后再单独检查首尾1绕一圈的距离。这个距离可以算成n - last first其中first是第一个1的下标last是最后一个1的下标。也就是说从最后一个1往后绕到数组末尾再从数组开头绕到第一个1这两段加在一起是它们绕环距离。如果这个距离也满足diff k整个环形数组才算通过。这种变形在面试中不太会直接出但用来训练“把边界纳入状态记录”的意识很有用。你平时练题如果只满足于线性版本遇到环形版本时会觉得无从下手。5.3 通用模板只要你想检查“相邻合法元素”就用lastPosition把上面的解法提炼成一个通用模板last None for i, x in enumerate(data): if x target: if last is not None and i - last k: return False last i return True这个模板的适用范围很广。比如一个字符串问题给定字符串s和整数k判断是否存在两个相同字符它们之间的距离不超过k。如果存在就返回false。这种题和我们的01序列题本质一模一样只是target从1变成了某个具体字符。模板的价值在于它能帮你快速搭建代码骨架接下来你只需要做两件事确定target确定“违规”的不等式。一旦养成这个习惯刷类似题的速度会明显提升。5.4 手撕代码前的最终自查清单结合所有踩坑经验我总结了一份手撕代码前的最终自查清单。每次遇到这类题按顺序过一遍空数组、纯0数组、只有一个1数组是否返回true是否用统一的i - last k判断而不是特判“连续1”是否把“间隔k个元素”等价成“下标差大于k”而不是小于k第一个1是否被正确跳过last是否为-1且判断了last ! -1k0的用例是否天然通过检测到违规后是否及时返回false而不是继续无意义遍历这份清单花不了多少时间但能拦住90%的低级错误。对我来说它比任何花哨解法都管用。最后再分享一个我的个人习惯。现在遇到这种“间距约束”的题我会先在草稿纸上写出diff - 1 k这个不等式再考虑代码怎么写。因为人脑直接翻译自然语言容易漏掉那个“差1”但数学表达式不会骗人。你只要把这个式子往纸上一摆所有符号问题、边界问题都会变得异常清晰。这道题练完我希望你带走的不是一段答案而是这个“先翻译条件再写代码”的流程它值回你读这篇文章的时间了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →