UTF-8 编码验证如何用位掩码判断字节前缀?
简化版
UTF-8 验证要看每个字节的高位前缀。单字节以 0 开头,多字节首字节以 110/1110/11110 开头,后续字节必须以 10 开头。用位掩码判断这些前缀,并维护还需要多少个 continuation byte。
详细版
遍历数组中的每个整数,只取低 8 位。若当前不在多字节字符内部,就根据高位模式判断这是 1、2、3、4 字节字符;若在内部,则当前字节必须满足 (byte & 0b11000000) == 0b10000000。维护 remaining 表示还需要几个后续字节。
时间复杂度 O(n),空间复杂度 O(1)。这题重点是二进制前缀匹配,不是字符集语义。
完整版教学
一、UTF-8 的字节前缀规则
UTF-8 用不同前缀表示一个字符占几个字节:
| 字节类型 | 前缀 |
|---|---|
| 1 字节字符 | 0xxxxxxx |
| 2 字节首字节 | 110xxxxx |
| 3 字节首字节 | 1110xxxx |
| 4 字节首字节 | 11110xxx |
| 后续字节 | 10xxxxxx |
验证时只需要判断这些高位模式是否匹配。
二、为什么只看低 8 位
题目输入通常是整数数组,但每个整数表示一个字节。一个字节只有 8 位,所以要关注低 8 位。
b = data[i] & 0xFF
如果平台保证 0 <= data[i] <= 255,这一步可以省略,但写出来语义更清楚。
三、如何判断后续字节
后续字节必须以 10 开头。用掩码取最高两位:
(b & 0b11000000) == 0b10000000
十六进制写法:
(b & 0xC0) == 0x80
记忆钩子:UTF-8 continuation byte 的身份证是高两位
10。
四、如何判断首字节长度
当不在等待后续字节时,看首字节前缀:
0xxxxxxx => remaining = 0
110xxxxx => remaining = 1
1110xxxx => remaining = 2
11110xxx => remaining = 3
其他 => 非法
可以用从高位开始数连续 1 的数量,也可以直接用掩码匹配固定模式。
五、代码模板
boolean validUtf8(int[] data) {
int remaining = 0;
for (int x : data) {
int b = x & 0xFF;
if (remaining > 0) {
if ((b & 0xC0) != 0x80) return false;
remaining--;
} else {
if ((b & 0x80) == 0) {
remaining = 0;
} else if ((b & 0xE0) == 0xC0) {
remaining = 1;
} else if ((b & 0xF0) == 0xE0) {
remaining = 2;
} else if ((b & 0xF8) == 0xF0) {
remaining = 3;
} else {
return false;
}
}
}
return remaining == 0;
}
最后必须确认 remaining==0,否则说明数据在一个多字节字符中途结束。
六、用例子推演
[197,130,1]:
197 = 11000101 => 2 字节首字节,remaining=1
130 = 10000010 => 合法后续,remaining=0
1 = 00000001 => 单字节
所以合法。
[235,140,4]:
235 = 11101011 => 3 字节首字节,remaining=2
140 = 10001100 => 合法后续,remaining=1
4 = 00000100 => 不是 10 开头,非法
七、常见误区与追问
- 误区:只检查首字节,不检查后续字节。 多字节字符的后续字节必须全部以
10开头。 - 误区:最后不检查 remaining。 数据可能在多字节字符中途结束。
- 误区:把
11111xxx当合法。 UTF-8 最多 4 字节,111110xx这类前缀不合法。 - 追问:为什么用
0xC0判断后续字节?0xC0是11000000,能取出最高两位。 - 追问:输入整数超过 255 怎么办? 应只取低 8 位或按题目约束判断非法。
- 追问:复杂度是多少? 每个字节处理一次,时间
O(n)。
八、加强记忆
UTF-8 验证就是前缀状态机。首字节决定还欠几个后续字节,后续字节必须是 10xxxxxx。掩码 0x80/0xE0/0xF0/0xF8 判断首字节,0xC0==0x80 判断后续字节。最后 remaining 必须归零。