ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

检查二进制字符串字段:如何判断最多只有一个连续‘1‘段

检查二进制字符串字段:如何判断最多只有一个连续‘1‘段 1. 题目到底在问什么1.1 先读题检查二进制字符串字段LeetCode 第 1784 题题名是“检查二进制字符串字段”。我第一次看到这个题名的时候脑子里闪过的是数据库里“字段”的概念心想这跟二进制字符串有什么关系。点进去看才明白这里的“字段”指的是字符串里连续的一段。原题描述很简洁给你一个二进制字符串s字符串里只包含字符0和1。请你判断这个字符串中是否最多只有一个连续由1组成的段。这里的“段”可以理解成连续的一段1中间不能有0隔开。如果满足条件返回true否则返回false。举个例子s 1001这里有两个1段第一个1是单独一段末尾的1是另一段所以不满足返回false。而s 110只有一个连续的1段11返回true。s 0呢里面没有1我们当然认为它是满足条件的因为“最多一个”包含零个的情况所以返回true。这道题在 LeetCode 上难度标记是“简单”但如果你只是把它当一道签到题刷过去其实有点亏。它背后涉及到字符串扫描、状态记录、边界条件判断甚至还能延伸到正则表达式、状态机这些实战里常用的东西。我身边不少同事刷完这题后都感叹简单题里藏着不少该养成的习惯。1.2 为什么这道题值得专门写一篇说实话LeetCode 每日一题里简单题很多很多就是直接模拟。但这道题的“坑”比表面看起来多比如字符串可能全是0可能只有一个字符可能开头就是0还可能1段之间只隔了一个0。这些边界我一开始没想全白送了一次 Wrong Answer。另外这道题在思路上有好几种解法直接扫描、用标准库split、用正则、甚至用find技巧。每种解法背后都对应一种编程习惯。我已经不止一次在 code review 里看到同事写的类似判断逻辑要么多写了标记位要么忽略了“没有1也要返回 true”的隐含条件。所以这篇我就不只讲题也把实际开发中检查“字段”时容易踩的坑一起聊了。无论你是刚接触算法的学生还是在工作中想补一补基础功底的开发这道题都适合花十五分钟认真过一遍。下面我会从读题开始一步步拆解最后给出多种实现和测试用例。2. 核心思路拆解2.1 “最多一个连续 1 段”到底怎么判断先明确一个概念什么是“字段”在这道题里一个“段”就是连续的一段相同字符。题目问的是1的段是否最多只有一个。换句话说在整个字符串中不允许出现两段1被0隔开的情况。那怎么判断最朴素的想法是从左往右扫一遍数一数一共出现了几个“由1组成的连续区块”。如果区块数大于 1就返回false。比如110011区块是11、11共 2 个返回false。111区块 1 个返回true。000区块 0 个返回true。所以核心在于如何识别一个“区块”的开始。一个区块开始的标志是当前字符是1并且前一个字符不是1或者当前就是第一个字符。一旦识别到区块开始计数器加一。如果计数器超过 1直接返回false不用再看后面的字符了。这里有个小细节有人说可以只看是否存在01这个子串。因为如果出现了01说明前面有一段1已经结束了后面又出现了新的1那必然存在至少两段。比如101里有10和011001里有01。反过来如果没有01字符串里就不可能出现新的1段。为什么因为1段要被0隔开新一段开始的位置一定是前一个字符是0当前字符是1也就是子串01。所以判断条件可以简化成字符串中是否存在01子串。这个思路很有意思也是很多题解里提到的“一行解法”的基础。2.2 扫描法和“找 01”法谁更通用直接扫描的办法好理解适合作为第一反应。写起来大概是def check(s): cnt 0 for i, ch in enumerate(s): if ch 1: if i 0 or s[i - 1] 0: cnt 1 if cnt 1: return False return True这个写法逻辑清楚面试时能一步步解释清楚不容易出错。而“找 01”的写法更取巧def check(s): return 01 not in s第一次看到这个解法的时候我愣了一下真的就这么简单仔细一琢磨确实是对的。因为一旦出现01就意味着“一段 1 已经结束下一个字符开始了一段新的 1”这不就是两段了吗不允许出现多段那就要求永远不能出现01。这两种解法本质上是同一个判断的两个角度。扫描法是在统计“段的个数”找01是在检查“是否发生了段的切换”。我个人的习惯是如果是在 LeetCode 上刷题用找01的方法最省事如果是生产环境里要维护的代码我会选扫描法因为后面如果要扩展成“最多允许两个 1 段”之类的需求扫描法改起来更直接。3. 代码实现与细节3.1 Python 实现与测试先给一个完整的 Python 实现我把扫描法和01判断都写出来方便对比class Solution: def checkOnesSegment(self, s: str) - bool: # 方法一统计连续 1 的段数 count 0 for i, c in enumerate(s): if c 1 and (i 0 or s[i - 1] 0): count 1 if count 1: return False return True用01 not in s更简洁class Solution: def checkOnesSegment(self, s: str) - bool: return 01 not in s写完后我习惯直接用手头的例子过一遍输入预期输出扫描法过程1001false索引 0 遇到1count1索引 3 遇到1且前一个是0count2返回 false110true索引 01count1索引 11但前一个也是1不计数索引 20结束返回 true1true索引 01count1返回 true0true没有遇到1count0返回 true101false索引 01count1索引 21且前一个是0count2返回 false1010false同上虽然末尾是0但第二个1段已经出现这些用例里最容易漏的是0和1这种单字符情况还有101这种段之间只隔一个0的情况。多写几个测试用例比直接提交更能培养边界意识。3.2 其他语言的写法C 版本的扫描法class Solution { public: bool checkOnesSegment(string s) { int cnt 0; for (int i 0; i s.size(); i) { if (s[i] 1 (i 0 || s[i - 1] 0)) { cnt; if (cnt 1) return false; } } return true; } };Java 版本class Solution { public boolean checkOnesSegment(String s) { int cnt 0; for (int i 0; i s.length(); i) { if (s.charAt(i) 1 (i 0 || s.charAt(i - 1) 0)) { cnt; if (cnt 1) return false; } } return true; } }Go 版本func checkOnesSegment(s string) bool { cnt : 0 for i : 0; i len(s); i { if s[i] 1 (i 0 || s[i-1] 0) { cnt if cnt 1 { return false } } } return true }这些写法都大同小异。注意 C 和 Go 里访问字符时用的是s[i]类型是byteJava 用charAt。核心判断条件完全一样。如果你用01子串判断Java 可以用!s.contains(01)C 可以用s.find(01) string::nposGo 可以用!strings.Contains(s, 01)。标准库能省不少事但要知道它背后的语义和复杂度。3.3 边界条件与易错点这类题最怕想当然。我总结几个容易踩的坑没有1也算满足条件。原题说的是“最多有一个连续 1 段”0 个当然也算“最多一个”。所以000要返回true。很多人写计数器初始化为 1然后看到第二个段才返回 false结果000返回了 false这就错了。单个1也必须返回 true。字符串1只有一段满足条件。这个在“找 01”解法里天然正确但在扫描法里要注意循环条件别写错。i 0的判断不能少。在扫描法里如果前一个字符是s[i-1]i 从 0 开始就会越界。要么像我的写法一样加i 0 ||要么从i 1开始循环单独处理第一个字符。“01”判断法虽然简洁但要注意01是连续子串不是“包含 0 和 1”。有人把它理解成“同时有 0 和 1”那就错了。10这类字符串同时有 0 和 1但它满足条件因为唯一的 1 段在最前面。这些问题我一次性踩过好几个。尤其是第一个LeetCode 的测试用例里确实有0这种输入我一开始写的逻辑就栽在上面了。4. 复杂度分析与优化空间4.1 时间与空间复杂度先看扫描法。它遍历了字符串一次每个字符只访问一次所以时间复杂度是O(n)其中n是字符串长度。不论是用计数器还是找01子串本质上都需要检查字符串内容所以下限是O(n)。空间上只用了一个整数变量所以是O(1)。再看01 not in s这种写法。Python 的in运算符对字符串做子串搜索底层是类似 Boyer-Moore 或双向匹配的算法平均情况下很快但最坏时间复杂度也是O(n*m)这里模式串长度为 2所以是O(2n)也就是O(n)。空间也是O(1)。所以两种方法在复杂度上没区别。有人可能会问能不能用正则表达式一行搞定比如re.search(r101, s)。可以但是没必要。正则在匹配时要回溯性能通常不如手写扫描而且可读性对不熟悉正则的人来说反而更差。这道题的输入规模虽然没给上限但字符串匹配类的题目一般会到10^5甚至更长手写扫描是最稳妥的。4.2 有没有更“聪明”的解法这个问题我见题解区有人讨论过能不能用split来做比如return len([x for x in s.split(0) if x]) 1思路是把字符串按0切分剩下的非空子串就是每一段连续的1。如果这样的段不超过一个就返回 true。这个写法也能过不过 C 和 Java 里没有这么方便的split返回列表的操作而且split会额外创建数组空间复杂度变成O(n)。所以不推荐在正式代码里用。类似的还有filter、groupby这类函数式写法它们能工作但要么可读性差要么性能一般。我个人觉得这种简单题没有什么“更聪明”的必要真正重要是理解“段”这个抽象。你能从01这个子串联想到“段的切换”说明你对字符串结构已经有感觉了。这种“从特殊子串反推条件”的思路放到后续很多字符串题目里都很有用比如判断一个字符串是否由某个单词重复组成比如检查括号字符串是否合法都是在寻找“破坏规则的关键位置”。5. 常见问题与踩坑实录5.1 题解里常见的错误写法我逛题解区的时候经常能看到以下几种错误这里列出来帮大家避雷。错误一用count(1)判断有人写成return s.count(1) 1这当然不对因为s 101里有 2 个1但有两段返回 false而s 11011里有 4 个1也有两段false。count(1)只能统计字符个数无法反映连续性。这个错误本质上是混淆了“字符数量”和“段数量”。错误二用正则1找所有匹配import re return len(re.findall(r1, s)) 1这个写法是对的但容易误写成len(re.findall(r1, s)) 1那就变成了统计单个字符。用正则找连续1段时号不能丢。不过即使写对了也依赖正则引擎性能不如扫描。错误三扫描时只判断“是否有 01”但写成了“是否有 10”有朋友把条件记反了觉得出现10就说明有第二段。仔细想想10表示一段1结束了后面接0这是正常的结束方式并不代表开始新段。比如1100只有一个段却含有10。所以判断标准必须是01不是10。错误四把0也当成需要计数的对象有人会统计“0 段”的个数然后想通过 0 段个数判断 1 段个数绕来绕去把自己绕晕。其实这道题只关心1段不用管 0 段。请记住题目字面上是“二进制字符串字段”实际是“连续 1 段”不是所有字段。5.2 实际开发中的“二进制字符串字段”检查这道题虽然来自算法题但类似判断在业务代码里真的会遇到。比如某个系统用一个二进制字符串来表示用户一周的签到状态1010101表示周一、周三等签到1代表已签到。这时候业务规则可能是“一周内最多连续签到一次”意思就是1不能出现两段否则判定异常。再比如配置项开关一个权限字符串1100前面的位表示某功能后面的位表示另一功能。如果产品说“这个权限组里不能有多个独立 1 段”那其实就是这道题的判断逻辑。这类场景里我一般不建议写01 not in s因为业务含义不直观。我宁愿写一个函数名字叫hasMoreThanOneSegment或者checkOnesSegment内部用扫描法统计段数然后在注释里写清楚“如果段数大于 1 说明存在多个独立开启区间”。这样的代码三个月后别人接手时不需要去猜01是什么意思。这是刷题和写业务代码最大的区别LeetCode 可以追求一行流生产环境要追求可读性和可维护性。5.3 调试与测试技巧我自己的做题习惯是不管题目多简单都先在本地把测试用例写成表格跑一遍。你可以用 assert 快速验证assert Solution().checkOnesSegment(1001) False assert Solution().checkOnesSegment(110) True assert Solution().checkOnesSegment(1) True assert Solution().checkOnesSegment(0) True assert Solution().checkOnesSegment(101) False assert Solution().checkOnesSegment(11) True assert Solution().checkOnesSegment(1010) False assert Solution().checkOnesSegment(010) True全部通过后再额外生成一些随机字符串做压力测试。比如写个脚本随机生成 0/1 字符串用暴力法统计段数和01判断法对拍看看结果是否一致。这是刷题时非常有效的验证手段特别是对于这种逻辑简单的题对拍能帮你发现隐藏的边界。6. 从这道题延伸出去的思考6.1 类似题型和进阶方向和这道题相关的题还有不少。比如 LeetCode 的“最大连续 1 的个数”485 题是让你求最长连续1段的长度还有“将每个元素替换为右侧最大元素”1299 题思路上有相似之处。如果你把这道题的判断逻辑改成“找出所有 1 段的起止位置”那就变成了区间合并问题。比较有意思的是这道题还可以和一个经典问题联系起来判断一个字符串是否由0和1组成且 0 和 1 各自只能连续出现一次。那就是判断字符串是否形如0*1*或者1*0*。这种思路在正则表达式里就是^(0*1*|1*0*)$。遇到这类需求时直接用01 not in s和10 not in s同时判断就行。6.2 每日一题到底怎么刷我刷每日一题这几年最大的体会是简单题别急着交试着用多种方法做一遍再想想边界条件。比如这道题你可以先用扫描法 AC再想想为什么01能直接判定再想想如果题目改成“最多两个 1 段”怎么写。每次这样多问一步一道简单题就能抵三道题的效果。另外建议把每道题的思路用自己的话写下来。我写过不少题解发现写题解本身会逼着你把模糊的感觉变成清晰的逻辑。很多人在评论里只写“秒了”或者“简单”这其实浪费了练习的机会。哪怕只是像这篇一样把扫描法的每一步拆开把01的判断逻辑讲清楚你对字符串处理的理解就会不一样。6.3 我个人的一个小技巧最后分享一个我在处理这类字符串状态判断时的通用技巧把“状态变化”的触发条件写出来。比如这道题状态变化就是“从 0 到 1”。一旦状态变化就说明开启了一个新段。你在代码里只要盯住“状态变化点”即可不需要维护完整状态。这个方法对更复杂的状态机同样适用。比如解析一段命令行的参数判断一个字符串是否符合某个协议格式都可以用“关注状态跃迁”的思路。只要把跃迁条件列清楚代码写出来就会又短又可靠。所以我建议你在刷完这道题后顺手做个小练习写一个函数输入任意二进制字符串返回所有连续1段的起始和结束下标。这个练习能帮你把这道题里“段”的概念彻底内化以后遇到“检查字段”相关的需求时你就能自然而然地想到应该用什么数据结构、什么扫描策略。这比死记01 not in s要有价值得多。
返回列表