在这里插入图片描述
在这里插入图片描述

摘要

UTF-8 是目前互联网上使用最广泛的字符编码方式之一,它的灵活性和兼容性使得我们几乎在所有系统中都能正常显示各种语言的字符。
不过,正因为 UTF-8 的变长特性(1~4 个字节表示一个字符),验证一个字节序列是否是合法的 UTF-8 编码并不那么直观。

这道题(LeetCode 393)让我们从底层去“读懂” UTF-8 的规则,通过判断一个整数数组是否能组成合法的 UTF-8 字节序列,间接地理解字符编码的逻辑。

这其实不仅是算法题,更是一道关于底层数据表示和系统字符编码理解的题。

描述

我们需要判断一个整数数组 data 是否是合法的 UTF-8 编码。

每个整数(0 <= data[i] <= 255)代表一个字节,也就是 8 位二进制数。
UTF-8 的编码规则如下:

字节数 格式(二进制) 说明
1 字节 0xxxxxxx 普通 ASCII 字符
2 字节 110xxxxx 10xxxxxx 双字节字符
3 字节 1110xxxx 10xxxxxx 10xxxxxx 三字节字符
4 字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 四字节字符

规律总结:

  • 第一个字节告诉我们当前字符有多少个字节;
  • 后续的字节必须以 10 开头;
  • 所有不满足这两条规则的情况都是非法编码。

题解答案

整体思路可以分成两步:

  1. 统计当前字节开头的 1 的数量
    这个数量决定了当前字符占用多少字节(例如 110xxxxx 就说明需要 2 个字节)。

  2. 验证后续字节
    接下来的若干个字节(数量 = 上一步统计的结果 - 1)必须以 10 开头。
    只要有任何一个不满足条件,就立即返回 false。

整个过程一次遍历即可完成。

题解代码分析

下面是完整的 Swift 代码实现,可以直接运行:

import Foundation

class UTF8Validator {
    func validUtf8(_ data: [Int]) -> Bool {
        var remainingBytes = 0  // 记录当前还需要的字节数
        
        for byte in data {
            // 只取最低 8 位
            let b = byte & 0xFF
            
            if remainingBytes == 0 {
                // 判断当前字节的前缀有多少个 1
                if (b >> 5) == 0b110 {        // 110xxxxx -> 2 字节
                    remainingBytes = 1
                } else if (b >> 4) == 0b1110 { // 1110xxxx -> 3 字节
                    remainingBytes = 2
                } else if (b >> 3) == 0b11110 { // 11110xxx -> 4 字节
                    remainingBytes = 3
                } else if (b >> 7) == 1 {       // 10xxxxxx 开头不合法
                    return false
                }
            } else {
                // 检查后续字节必须以 10 开头
                if (b >> 6) != 0b10 {
                    return false
                }
                remainingBytes -= 1
            }
        }
        
        // 所有字节验证完后,remainingBytes 必须为 0
        return remainingBytes == 0
    }
}

代码讲解

  • b >> n 表示右移 n 位,检查二进制前缀;
  • (b >> 5) == 0b110 判断是否以 110 开头;
  • remainingBytes 表示当前还需要多少个后续字节;
  • 如果遇到非法的 10xxxxxx 开头字节,立刻返回 false
  • 最后检查 remainingBytes == 0,确保整个序列刚好结束,没有“半个字符”残留。

这段逻辑跟 UTF-8 的标准一一对应,非常直观。

示例测试及结果

我们来验证几个典型例子

let validator = UTF8Validator()

print(validator.validUtf8([197, 130, 1]))
// 输出: true
// 解释:11000101 10000010 00000001
// 110 开头说明第一个字符是 2 字节编码,后面的 10xxxxxx 是合法延续字节。
// 最后的 00000001 是单字节字符,全序列合法。

print(validator.validUtf8([235, 140, 4]))
// 输出: false
// 解释:11101011 10001100 00000100
// 第一个是 3 字节字符 (1110 开头),第二个没问题 (10 开头),但第三个不是 10 开头,非法。

print(validator.validUtf8([0b11100010, 0b10000010, 0b10101100]))
// 输出: true
// 这是字符 “€”(欧元符号)的 UTF-8 编码,合法。

输出结果:

true
false
true

时间复杂度

整段代码只需要遍历数组一次,每个字节的检查是 O(1) 操作。

时间复杂度:O(n),其中 n 是字节数。

空间复杂度

整个过程只用了一个变量 remainingBytes 来保存状态,没有使用额外空间。

空间复杂度:O(1)

总结

这道题的关键在于理解 UTF-8 的编码规则,而不是写复杂的算法。
它的难点在于逻辑判断:

  • 如何判断字符头;
  • 如何验证后续字节;
  • 如何保证整体字节数刚好匹配。

从工程角度看,这种验证逻辑常用于:

  • 文件或网络传输时判断数据是否被截断;
  • 数据库或日志系统中检测是否有乱码;
  • 编写底层解析器或协议时(比如 JSON/HTTP)进行编码合法性校验。

这道题不仅能锻炼逻辑思维,还能让你对 “文本是如何变成二进制存储的” 有更直观的理解。

Logo

加入「COC·上海城市开发者社区」,成就更好的自己!

更多推荐