LeetCode 717. 1 比特与 2 比特字符
题目描述
题意分析
要什么:一个 0/1 数组是若干字符编码后拼起来的,编码规则是:
0表示一个 1 比特字符,10和11各表示一个 2 比特字符。题目保证数组最后一位是 0,问最后一个字符是否必然是 1 比特字符。
约束透露的信号:这套编码是前缀无歧义的——看到0就必然是一个单比特字符结束,看到1就必然是一个双比特字符的开头,两种情况互斥且完备。所以从左往右解析时每一步的断句都是唯一确定的,根本不存在多种切分方式,「必然」二字也就有了着落:只要模拟一遍解析就能得到唯一答案,不需要枚举也不需要 DP。这一点是全题的核心,识别不出来就会误以为要做区间划分。
边界:数组末位保证为 0,所以一定能解析完;数组可能只有一个元素[0],此时答案是true;1一定成对出现在解析中,绝不会是最后一位;解析的推进必须以「字符」为单位而不是「比特」为单位。
解法:线性扫描解析
核心思路
有人会把它当成划分问题:设
f[i]表示前i位能否合法解析,再考虑最后一个字符是 1 位还是 2 位。这样做没错,但完全是杀鸡用牛刀——DP 的价值在于「有多种切分方式需要择优或计数」,而本题的切分唯一。
唯一性来自编码规则的互斥性:站在某个字符的起始位置i,如果bits[i] == 0,这个字符只能是 1 比特的0;如果bits[i] == 1,这个字符只能是 2 比特的10或11,无论下一位是什么都要一并吃掉。没有任何一种情形需要「试两条路」。
于是解法就是一遍确定性的解析扫描,要维护的不变量是:指针i始终停在某个字符的起始位置(初始时i = 0显然满足,每次按规则吃掉整个字符后依然满足)。
循环条件是关键:只在i < n - 1时继续解析。为什么不是i < n?因为我们要判断的正是「最后一位那个 0 是不是独立成字」,所以要在还没碰到最后一位时停下来,看指针最终落在哪里。循环结束时只有两种可能:
i == n - 1:前面的字符恰好在最后一位之前解析完,最后一位的 0 只能自成一个 1 比特字符,答案为true。i == n:说明上一步是从i = n - 2出发吃掉了一个 2 比特字符10,把最后一位也吞了进去,最后一个字符是 2 比特的,答案为false。
所以只需返回i == n - 1。
解题步骤
- 令
i = 0,进入while (i < n - 1)循环。为什么上界是n - 1而不是n:我们关心的是最后一位是否被前面的字符吞掉,必须让循环在「还剩最后一位」时自然停下;写成i < n会把最后那个 0 也当作普通字符解析掉,循环结束时i恒等于n,判据失效。- 若
bits[i] == 0,令i++。为什么只前进一位:0就是一个完整的 1 比特字符,吃掉它之后指针自然落在下一个字符的开头,不变量保持成立。- 否则(
bits[i] == 1)令i += 2。为什么可以不看下一位:10和11都是合法的 2 比特字符,下一位是 0 还是 1 都不影响「这个字符占两位」这个事实,所以无需分支。为什么不会越界:题目保证末位是 0,所以1不可能出现在最后一位,i + 1一定是合法下标。- 循环结束后返回
i == n - 1。为什么这个判据充要:不变量保证i停在某个字符的起始位置,而循环退出意味着i >= n - 1;等于n - 1说明最后一位刚好是一个新字符的开头(只能是单比特 0),大于则说明它已被前一个双比特字符吞掉。- 以
bits = [1, 0, 0]走一遍。n = 3,i = 0。循环判断0 < 2成立:bits[0] == 1,吃掉10这个 2 比特字符,i变成 2。再判断2 < 2不成立,循环结束。返回2 == 2即true——解析结果是[10][0],最后一个字符确实是 1 比特的。- 再以
bits = [1, 1, 1, 0]走一遍。n = 4,i = 0。0 < 3成立:bits[0] == 1,i变成 2。2 < 3成立:bits[2] == 1,吃掉10,i变成 4。4 < 3不成立,循环结束。返回4 == 3即false——解析结果是[11][10],最后一位的 0 被第二个双比特字符吞掉了,最后一个字符是 2 比特的。这组用例正是判据的意义所在:末位虽然是 0,却未必独立成字。
代码实现
// 核心实现:线性扫描解析,维护必要状态并避免重复处理。
class Solution {
public boolean isOneBitCharacter(int[] bits) {
int n = bits.length;
int i = 0;
while (i < n - 1) {
if (bits[i] == 0) {
i++;
} else {
i += 2;
}
}
return i == n - 1;
}
}
// 核心实现:线性扫描解析,维护必要状态并避免重复处理。
func isOneBitCharacter(bits []int) bool {
n := len(bits)
i := 0
for i < n-1 {
if bits[i] == 0 {
i++
} else {
i += 2
}
}
return i == n-1
}
复杂度分析
- 时间复杂度:$O(n)$。凭什么:指针
i每轮至少前进 1 位且只增不减,最多走n步就退出,每步只做一次比较和一次加法。- 空间复杂度:$O(1)$。凭什么:全程只有一个指针变量,没有 DP 数组也没有递归栈,输入数组是只读的。
关键点总结
- 先判断切分是否唯一,再决定用模拟还是 DP。前缀无歧义的编码(本题、UTF-8 校验)只需一遍确定性扫描;而像「解码方法」那样
1开头既可能单独成字也可能与下一位组合,切分不唯一,才必须上 DP。这个判断是选对武器的关键。- 循环边界承载了题意。本题把
i < n - 1而不是i < n当作循环条件,等于把「最后一位单独留出来考察」这个问题需求直接编码进了控制流。遇到「最后一个 / 第一个元素有特殊语义」的题,先想能不能用边界把它隔离出来,比事后特判干净得多。- 指针推进的步长要以「逻辑单位」为准,本题是「字符」而不是「比特」。凡是变长编码、变长分组的扫描,都要在心里明确「一次前进吃掉的是一个完整单位」。
- 题目给的保证(末位为 0)不是装饰,它同时保证了「解析一定能完成」和「
i += 2不会越界」。做题时把每条保证对应到代码里的哪一处,是快速建立信心的方法。- 面试视角:这题代码只有五行,考察点其实是能否说清「为什么不需要 DP」和「循环边界为什么是
n-1」。另一种同样正确的写法是从倒数第二位往前数连续 1 的个数,个数为偶则最后一位独立成字;能顺手给出这条 $O(n)$ 但常数更小的解法,说明你真的理解了结构。
易错点总结
- 错误写法:循环条件写成
while (i < n);用例bits = [1, 0, 0]→ 最后一位也被当作普通字符解析,i最终为 3,返回3 == 2即false,正确答案是true。- 错误写法:遇到
bits[i] == 1时还要看下一位,只有bits[i+1] == 0才前进 2 位、否则前进 1 位;用例bits = [1, 1, 0]→i = 0时因为下一位是 1 只前进到 1,i = 1时才吃掉10跳到 3,返回3 == 2即false,正确答案是true(11本身就是一个完整的 2 比特字符,不能拆)。- 错误写法:返回
i == n而不是i == n - 1;用例bits = [0]→i停在 0,返回0 == 1即false,正确答案是true,判据整体反了。- 错误写法:直接返回
bits[n-1] == 0;用例bits = [1, 1, 1, 0]→ 末位确实是 0 故返回true,但它被10吞掉了,正确答案是false。题目保证末位为 0,所以这个判据恒为真,毫无信息量。- 错误写法:改用逆向数 1 的解法,但起点取成最后一位而不是倒数第二位;用例
bits = [1, 0]→ 从末位的 0 开始立刻停止,数出 0 个 1 判为偶数返回true,正确答案是false;起点必须是倒数第二位,本例从那里数出 1 个 1,奇数故为false。- 错误写法:
i += 2前不确认i + 1合法,且在不保证末位为 0 的变形数据上运行;用例bits = [0, 1]→i从 1 跳到 3 越过数组,若后续还要读取则越界。- 错误写法:用
for (int i = 0; i < n; i++)配合if (bits[i] == 1) i++的写法,却忘了循环变量自身还会再自增一次;用例bits = [1, 1, 0]→ 每轮实际前进 2 位看似正确,但循环终点仍是n,无法区分i == n - 1与i == n,判据丢失。- 错误写法:用 DP 求「最后一个字符是否为 1 比特」但状态定义成「前
i位能否合法解析」;用例 任意输入 → 由于切分唯一,所有前缀都能合法解析,DP 表恒为真,得不出任何结论,状态定义与问题不匹配。- 错误写法:把
1后面跟的那一位也当成新字符的起点重新判断;用例bits = [1, 0, 0]→ 从下标 1 的 0 开始又解析出一个字符,i走到 3,返回false,正确答案是true。- 错误写法:对空数组或长度为 0 的输入不做防护;用例
bits = []→n - 1为 -1,循环不执行,返回0 == -1即false;题目保证长度至少为 1,但若把这段代码复用到别处,这个隐患会显形。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 91. 解码方法 | 中等 | 切分不唯一,同一位置既可单独成字也可与下一位组合,必须用 DP 计数 |
| 70. 爬楼梯 | 简单 | 同样是「每步走 1 或 2」,但要统计所有走法数量而非模拟唯一的一条 |
| 55. 跳跃游戏 | 中等 | 步长由数组内容动态决定且可自由选择,靠贪心维护最远可达边界而非逐字符解析 |