题目描述

✅ 393. UTF-8 编码验证

image-20260928235522779

image-20260928235522780

题意分析

按题面给定的字节格式,判断整个数组能否恰好分成若干完整字符,不需要还原字符内容。每项只取低 8 位;一个字符占 1 到 4 个字节,首字节决定长度,其余字节都必须以二进制 10 开头。

解法:记录还需要的续字节数量

核心思路

[!blue]

用 remaining 表示当前字符还缺多少个续字节。remaining == 0 说明前面的字符已经完整,当前字节必须是新字符的首字节;remaining > 0 则说明当前字节只能接在前一个首字节之后,不能另起一个字符。

对首字节,前缀 0xxxxxxx 表示单字节字符,无需续字节;前缀 110xxxxx、1110xxxx、11110xxx 分别表示总长为 2、3、4 字节,因此将 remaining 设为 1、2、3。其他前缀都不合法,包括没有首字节引领的 10xxxxxx,以及超过四字节长度的前缀。

代码通过右移丢弃内容位,只比较前缀:b >> 7 == 0 检查最高位为 0;b >> 5 == 6、b >> 4 == 14、b >> 3 == 30 分别检查 110、1110、11110。这些模式互斥,不会把一个合法首字节识别为两种长度。

等待续字节时,只接受 b >> 6 == 2,即最高两位为 10,随后将 remaining 减一。降到 0 就说明该字符恰好结束,下一项可以开始新字符。若格式不符,立即返回 false。

这样每个字节都按当前字符所需的位置验证,且只有收齐续字节后才能开始下一个字符。数组结束时还要检查 remaining == 0,否则虽然已读字节的前缀都正确,最后一个字符仍然不完整。

解题步骤

  1. 初始化 remaining = 0,逐项计算 b = num & 255,只保留低 8 位。
  2. 若 remaining == 0,识别合法首字节并设置所需续字节数;单字节字符直接处理下一项,无法匹配则返回 false。
  3. 若 remaining > 0,检查当前字节是否以 10 开头,不符合就返回 false,符合则执行 remaining--。
  4. 扫描结束后返回 remaining == 0,确认没有缺失的续字节。

代码实现

class Solution {
    public boolean validUtf8(int[] data) {
        int remaining = 0;

        for (int num : data) {
            int b = num & 255;

            // 开始新字符:前缀常量分别对应二进制 0、110、1110、11110
            if (remaining == 0) {
                if ((b >> 7) == 0) {
                    continue;
                } else if ((b >> 5) == 6) {
                    remaining = 1;
                } else if ((b >> 4) == 14) {
                    remaining = 2;
                } else if ((b >> 3) == 30) {
                    remaining = 3;
                } else {
                    return false;
                }
            } else {
                // 续字节高两位必须为二进制一零
                if ((b >> 6) != 2) {
                    return false;
                }

                remaining--;
            }
        }

        // 末尾不能仍欠续字节
        return remaining == 0;
    }
}
func validUtf8(data []int) bool {
    remaining := 0
    for _, num := range data {
        b := num & 255
        // 开始新字符:前缀常量分别对应二进制 0、110、1110、11110
        if remaining == 0 {
            if b>>7 == 0 {
                continue
            } else if b>>5 == 6 {
                remaining = 1
            } else if b>>4 == 14 {
                remaining = 2
            } else if b>>3 == 30 {
                remaining = 3
            } else {
                return false
            }
        } else {
            // 续字节高两位必须为二进制一零
            if b>>6 != 2 {
                return false
            }
            remaining--
        }
    }
    // 末尾不能仍欠续字节
    return remaining == 0
}

复杂度分析

  • 时间复杂度:$O(n)$,逐字节处理。
  • 空间复杂度:$O(1)$,只保存剩余数量。

关键点总结

[!green]

  • remaining 先决定当前字节扮演首字节还是续字节,再决定应检查哪种前缀。
  • 首字节给出的是字符总长度,所需续字节数要减去已经读取的首字节。
  • 过程中的前缀检查与结束时的数量检查缺一不可。

易错点总结

[!yellow]

  • 只检查每个字节属于某种合法前缀,不检查它出现的位置,会接受孤立续字节。
  • 还欠续字节时不能接受新的首字节,即使这个首字节本身格式正确。
  • 识别多字节首部时,还必须确认连续的 1 后面是 0;仅数出部分前导 1 不足以确定合法长度。
  • 不能在循环正常结束后直接返回 true,末尾仍可能缺少续字节。

相似题目

题目 难度 关联与区别
717. 1 比特与 2 比特字符 简单 同样从首位模式确定接下来应消费多少位或字节,本题需要逐个验证后续字节前缀。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2022/64169609
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!