计数二进制子串
- https://leetcode.cn/problems/count-binary-substrings/
描述
-
给定一个字符串 s,统计并返回具有相同数量 0 和 1 的非空(连续)子字符串的数量,并且这些子字符串中的所有 0 和所有 1 都是成组连续的。
-
重复出现(不同位置)的子串也要统计它们出现的次数。
示例 1:
输入:s = "00110011"
输出:6
解释:6 个子串满足具有相同数量的连续 1 和 0 :"0011"、"01"、"1100"、"10"、"0011" 和 "01" 。
注意,一些重复出现的子串(不同位置)要统计它们出现的次数。
另外,"00110011" 不是有效的子串,因为所有的 0(还有 1 )没有组合在一起。
示例 2:
输入:s = "10101"
输出:4
解释:有 4 个子串:"10"、"01"、"10"、"01" ,具有相同数量的连续 1 和 0 。
提示
- 1 <= s.length <= 10510^5105
- s[i] 为 ‘0’ 或 ‘1’
Typescript 版算法实现
1 )方案1
// leetcode 大规模数据不通过
function countBinarySubstrings(s: string): number {
// 建立数据结构,队列,用于保存数据
const r = []
// 给定任意子输入都返回第一个符合条件的子串
const match = (s:string) => {
// 找到含有0或1一次或多次的匹配字符串
const j:string = s.match(/^(0+|1+)/)?.[0] ?? ''
// 生成第一个字符异或得到相反的字符,并且长度和匹配字符串保持一致
const o:string = (Number(j[0]) ^ 1).toString().repeat(j.length)
// 构造出新的正则,用于过滤当前切片的规则,注意这里的 ^ 开始很重要
const reg = new RegExp(`^(${j}${o})`)
// 符合条件,拿到 () 中的匹配字符串
if (reg.test(s)) {
return RegExp.$1 // 得到第 1 个子表达式相匹配的文本
}
return ''
}
// 通过for循环控制程序运行的流程
for (let i = 0, len = s.length - 1; i < len; i++) {
// i 递增,进行切片处理,每次 slice 都会向后移动一位
// 注意 slice 左闭右开,参数为:[start, end]
const sub = match(s.slice(i))
if (sub) {
r.push(sub)
}
}
return r.length
};
console.log(countBinarySubstrings('00110011'))
思路:
- 对字符串做切片处理,从第一位到倒数第二位,左闭右开,此处需要一个for循环
- 对每一个切片进行单一处理,每个切片只找一个,避免循环到后面出现重复
- 将找到符合规则的切片存入数组
- 统计最终数组的长度,即答案
注意:
- 这个方案在 leetcode上是通不过的,错误发生在 new RegExp 的时候,错误信息是:SyntaxError: Invalid regular expression,Regular expression too large 这个错误在实际应用场景中是不会出现的,用例有些极端了
- 拼接出的正则表达式 ^(j{j}j{o}) 会超出引擎的处理限制,导致 RangeError 或性能崩溃
2 )方案2
function countBinarySubstrings2(s: string): number {
let count:number = 0 // 用于统计最终符合要求的长度
let last:string = '_'; // 上一个字符初始化
let pre:number = 0; // 上一次统计次数
let cur:number = 0; // 当前字符出现的统计次数
let i:number = 0; // 当前循环次数
while(i <= s.length) {
const c:string = s[i]; // 获取当前切片字符串
// 第一次一定会执行,last 就会被重新赋值,last 保留的是前一个字符
// 只有遇到新的值时,last 才会变成当前新值,这个if里面对当前字符 c 进行统计次数
if(last !== c) {
// 遇到下一个不同字符更新, 用pre保留cur值
last = c; // 保留当前字符串
count += Math.min(pre, cur); // 比较前2个不同字符出现次数的最小值
pre = cur; // pre 保留前一个字符出现的次数
cur = 0; // 如果不相等,cur 统计次数清零
// console.log('count;', count);
}
cur ++; // 统计当前字符出现的次数
i ++; // 本次循环次数+1
}
return count;
}
console.log(countBinarySubstrings2('00110011'));
- 这是官方精选解题方案
- https://leetcode.cn/problems/count-binary-substrings/solution/count-binary-substrings-by-ikaruga/
- 这样前面是连续 0/1 后面是连续 1/0 的数据
- 每次符合的字符取决于前面 0/1 的个数和后面 1/0 的个数,即 min(pre, cur)
- 遍历时,当数字再一次改变时(或到达结尾时), 意味着一段结束,并能得到这一段前面和后面数字的个数
- 每次符合条件,并采用最小值,即可得到当前可用的统计答案
- 再将结果累加到 count 上就得到所有的可能结果
3 )方案3
function countBinarySubstrings3(s: string): number {
// index 是当前循环次数, n 是字符串长度, last是上一次统计数量, ans 是最终统计结果
let index = 0, n = s.length, last = 0, ans = 0;
while (index < n) {
const c = s.charAt(index); // c 是当前字符串
let count = 0; // 每次count都重置一下
// 统计当前相同字符串的结果
while (index < n && s.charAt(index) === c) {
++index;
++count;
}
ans += Math.min(count, last); // 从 count 和 last 找到最小的统计,取最小值
last = count; // 缓存 count统计
}
return ans;
};
console.log(countBinarySubstrings3("00110011"));
- 此方案为官方题解,参考:
- https://leetcode.cn/problems/count-binary-substrings/solutions/367704/ji-shu-er-jin-zhi-zi-chuan-by-leetcode-solution/
- 此方案与上述方案2有异曲同工之妙,两者原理相似
Python3 版算法实现
1 )方案1
class Solution:
def countBinarySubstrings(self, s: str) -> int:
count = 0 # 用于统计最终符合要求的长度
last = '_' # 上一个字符初始化
pre = 0 # 上一次统计次数
cur = 0 # 当前字符出现的统计次数
i = 0 # 当前循环次数
while i <= len(s):
if i == len(s):
c = None
else:
c = s[i] # 获取当前切片字符串
# 第一次一定会执行,last 就会被重新赋值,last 保留的是前一个字符
# 只有遇到新的值时,last 才会变成当前新值,这个if里面对当前字符 c 进行统计次数
if last != c:
# 遇到下一个不同字符更新, 用pre保留cur值
last = c # 保留当前字符串
count += min(pre, cur) # 比较前2个不同字符出现次数的最小值
pre = cur # pre 保留前一个字符出现的次数
cur = 0 # 如果不相等,cur 统计次数清零
cur += 1 # 统计当前字符出现的次数
i += 1 # 本次循环次数+1
return count
2 )方案2
class Solution:
def countBinarySubstrings(self, s: str) -> int:
index = 0 # 当前循环次数
n = len(s) # 字符串长度
last = 0 # 上一次统计数量
ans = 0 # 最终统计结果
while index < n:
c = s[index] # 获取当前字符串
count = 0 # 重置每次统计的计数
# 统计当前相同字符串的结果
while index < n and s[index] == c:
index += 1
count += 1
ans += min(count, last) # 从 count 和 last 找到最小的统计值,取最小值进行累加
last = count # 缓存 count 统计值
return ans
Golang 版算法实现
1 )方案1
func countBinarySubstrings(s string) int {
count := 0 // 用于统计最终符合要求的长度
last := byte('_') // 上一个字符初始化
pre := 0 // 上一次统计次数
cur := 0 // 当前字符出现的统计次数
i := 0 // 当前循环次数
for i <= len(s) {
var c byte
if i == len(s) {
c = 0
} else {
c = s[i] // 获取当前切片字符串
}
// 第一次一定会执行,last 就会被重新赋值,last 保留的是前一个字符
// 只有遇到新的值时,last 才会变成当前新值,这个if里面对当前字符 c 进行统计次数
if last != c {
// 遇到下一个不同字符更新, 用pre保留cur值
last = c // 保留当前字符串
count += min(pre, cur) // 比较前2个不同字符出现次数的最小值
pre = cur // pre 保留前一个字符出现的次数
cur = 0 // 如果不相等,cur 统计次数清零
}
cur++ // 统计当前字符出现的次数
i++ // 本次循环次数+1
}
return count
}
// 辅助函数:返回两个整数中较小的一个
func min(a, b int) int {
if a < b {
return a
}
return b
}
2 )方案2
func countBinarySubstrings(s string) int {
index := 0 // 当前循环次数
n := len(s) // 字符串长度
last := 0 // 上一次统计数量
ans := 0 // 最终统计结果
for index < n {
c := s[index] // 获取当前字符
count := 0 // 重置每次统计的计数
// 统计当前相同字符串的结果
for index < n && s[index] == c {
index++
count++
}
ans += min(count, last) // 从 count 和 last 找到最小的统计值,取最小值进行累加
last = count // 缓存 count 统计值
}
return ans
}
// 辅助函数:返回两个整数中较小的一个
func min(a, b int) int {
if a < b {
return a
}
return b
}
Java 版算法实现
1 ) 暴力枚举与正则匹配法 (Brute Force with Regular Expression)
// 下面的正则算法超出时间限制!
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Main1 {
public static int countBinarySubstrings(String s) {
List<String> r = new ArrayList<>();
for (int i = 0; i < s.length() - 1; i++) {
String sub = match(s.substring(i));
if (!sub.isEmpty()) {
r.add(sub);
}
}
return r.size();
}
private static String match(String s) {
Pattern p = Pattern.compile("^(0+|1+)");
Matcher m = p.matcher(s);
if (!m.find()) {
return "";
}
String j = m.group(1);
char c = j.charAt(0) == '0' ? '1' : '0';
String o = String.valueOf(c).repeat(j.length());
Pattern reg = Pattern.compile("^(" + j + o + ")");
Matcher m2 = reg.matcher(s);
if (m2.find()) {
return m2.group(1);
}
return "";
}
public static void main(String[] args) {
System.out.println(countBinarySubstrings("00110011"));
}
}
算法说明
- 遍历字符串的每个可能起点,通过
substring截取后缀子串。 - 使用正则表达式提取后缀开头的连续相同字符段(如
000)及其长度。 - 动态构造“等长且字符相反”的正则表达式(如
^000111),验证并提取符合条件的子串。 - 将所有匹配到的有效子串存入列表,最终返回列表的大小作为结果。
无法通过的 Case 解释
- 正则表达式超限报错:当输入包含极长的连续相同字符(例如几千个
'1')时,动态拼接出的正则表达式长度会达到上万字符,超出 Java 正则引擎的处理限制,导致PatternSyntaxException或底层栈溢出。 - 超时 (Time Limit Exceeded):
s.substring(i)每次都会创建新的字符串对象,结合正则表达式的编译与匹配,整体时间复杂度接近 O(N2)O(N^2)O(N2)。对于长测试用例,执行时间会远超限制。 - 内存溢出 (Memory Limit Exceeded):代码将所有匹配到的子串字符串都保存到了
ArrayList中。题目仅要求返回数量,这种存储方式会造成极大的额外空间开销。
2 ) 一次遍历与分组统计法 (Linear Scan and Group Counting)
public class Main2 {
public static int countBinarySubstrings(String s) {
int count = 0; // 用于统计最终符合要求的长度
char last = '_'; // 上一个字符初始化
int pre = 0; // 上一次统计次数
int cur = 0; // 当前字符出现的统计次数
int i = 0; // 当前循环次数
while (i <= s.length()) {
char c = i < s.length() ? s.charAt(i) : ' '; // 获取当前字符,越界时赋空格
// 第一次一定会执行,last 就会被重新赋值,last 保留的是前一个字符
// 只有遇到新的值时,last 才会变成当前新值,这个if里面对当前字符 c 进行统计次数
if (last != c) {
// 遇到下一个不同字符更新,用pre保留cur值
last = c; // 保留当前字符串
count += Math.min(pre, cur); // 比较前2个不同字符出现次数的最小值
pre = cur; // pre 保留前一个字符出现的次数
cur = 0; // 如果不相等,cur 统计次数清零
}
cur++; // 统计当前字符出现的次数
i++; // 本次循环次数+1
}
return count;
}
public static void main(String[] args) {
System.out.println(countBinarySubstrings("00110011"));
}
}
算法说明
- 状态初始化:使用
pre记录前一组连续字符的长度,cur记录当前组连续字符的长度,count记录最终结果。 - 线性扫描:遍历字符串,逐个字符判断。若当前字符与前一个字符相同,则当前组的连续长度
cur加 1。 - 分组切换:当遇到不同的字符(或到达字符串末尾触发边界条件)时,说明当前连续字符组结束。此时,符合条件的子串数量取决于相邻两组长度的较小值,即累加
Math.min(pre, cur)到count中。 - 状态更新:将
pre更新为当前的cur,并将cur重置为 0,开始统计新一组连续字符的长度。 - 返回结果:遍历结束后,直接返回累加的
count。该算法时间复杂度为 O(N)O(N)O(N),空间复杂度为 O(1)O(1)O(1),完美避免了正则和字符串切片带来的性能问题。
3 ) 游程统计法 (Run-Length Encoding / Consecutive Segment Counting)
public class Main3 {
public static int countBinarySubstrings3(String s) {
// index 是当前循环次数, n 是字符串长度, last是上一次统计数量, ans 是最终统计结果
int index = 0, n = s.length(), last = 0, ans = 0;
while (index < n) {
char c = s.charAt(index); // c 是当前字符串
int count = 0; // 每次count都重置一下
// 统计当前相同字符串的结果
while (index < n && s.charAt(index) == c) {
++index;
++count;
}
ans += Math.min(count, last); // 从 count 和 last 找到最小的统计,取最小值
last = count; // 缓存 count统计
}
return ans;
}
public static void main(String[] args) {
System.out.println(countBinarySubstrings3("00110011"));
}
}
算法说明
- 外层循环控制分组:使用
index遍历字符串,每次获取当前字符c作为当前连续段的基准字符。 - 内层循环统计长度:通过内层
while循环,只要字符与c相同就继续向后遍历,累加当前连续字符的长度count,并同步推进index。 - 计算有效子串:内层循环结束时,当前连续段统计完毕。将当前段长度
count与上一段长度last取较小值,累加到最终结果ans中。 - 状态传递:将当前段长度
count赋值给last,作为下一段比较的基准,随后进入下一次外层循环处理新的字符段。 - 该算法通过内层循环直接跳过相同字符,减少了外层循环的判断次数,时间复杂度为 O(N)O(N)O(N),空间复杂度为 O(1)O(1)O(1),逻辑非常清晰。
377



被折叠的 条评论
为什么被折叠?



