题目描述

✅ 271. 字符串的编码与解码

题意分析

将字符串列表编码成一个字符串,并能从编码无损还原原列表。原来的顺序、重复项、空字符串以及每项的具体内容都必须保留。

字符串正文可能含数字或分隔符,因此不能假设某个普通字符永远不会出现在数据里。空列表与只包含一个空字符串的列表也不同。这里的解码入口接收本实现编码函数生成的合法数据。

解法:长度前缀界定每个字符串

核心思路

[!blue]

不靠分隔符划分正文,而为每项添加明确的长度头,格式为“十进制长度、#、原文”。# 只用于确定长度字段在哪里结束;知道长度后,正文就按指定数量读取,不再扫描正文内部的分隔符。

解码维护当前位置 index。先逐位累积长度,读到 # 后跳过它,再截取接下来的 length 个单位作为一个完整字符串。将指针移动到正文末尾后,那里必定是下一段长度头或整个编码的末尾。

每一轮先解析唯一的十进制长度,再按确定边界取正文,因此正文含多少个 # 或数字都不会产生歧义。零长度正文也要加入一次空字符串;虽然正文指针不再额外移动,长度头和分隔符已经被消费,循环仍然前进。

空列表没有任何片段,编码为空;非空列表即使所有项都是空字符串,也会输出各自的零长度头,因此两者能够区分。Java 的长度与截取都使用 UTF-16 单元,Go 都使用字节,各自的一对编码解码保持单位一致即可往返;这两份实现没有约定一个统一的跨语言字节长度协议。

解题步骤

  1. 编码时按列表顺序,对每项追加长度、分隔符和原文。
  2. 解码从字符串开头开始,将分隔符前的数字累积为当前段长度。
  3. 跳过分隔符,截取恰好该长度的正文并加入结果,包含长度为零的情况。
  4. 移到正文结束位置,继续解析下一长度头,直到编码全部消费。
  5. 返回恢复后的列表,保持原顺序与重复项。

代码实现

class Codec {
    public String encode(List<String> strs) {
        StringBuilder out = new StringBuilder();

        for (String s : strs) {
            out.append(s.length()).append('#').append(s);
        }

        return out.toString();
    }

    public List<String> decode(String s) {
        List<String> out = new ArrayList<>();
        int index = 0;

        while (index < s.length()) {
            int length = 0;

            while (s.charAt(index) != '#') {
                length = length * 10 + s.charAt(index++) - '0';
            }

            index++;
            out.add(s.substring(index, index + length));
            index += length;
        }

        return out;
    }
}
import (
    "strconv"
    "strings"
)

type Codec struct{}

func Constructor() Codec { return Codec{} }

func (c *Codec) Encode(strs []string) string {
    var out strings.Builder
    for _, s := range strs {
        out.WriteString(strconv.Itoa(len(s)))
        out.WriteByte('#')
        out.WriteString(s)
    }
    return out.String()
}

func (c *Codec) Decode(s string) []string {
    out := []string{}
    for index := 0; index < len(s); {
        length := 0
        for s[index] != '#' {
            length = length*10 + int(s[index]-'0')
            index++
        }
        index++
        out = append(out, s[index:index+length])
        index += length
    }
    return out
}

复杂度分析

  • 时间复杂度:以完整编码长度 L 计,编码和解码都为 $O(L)$。长度字段、正文和分隔符都只线性处理,空字符串的段头开销也计入 L。
  • 空间复杂度:$O(L)$,保存编码缓冲或解码结果;解码除返回结果外只需长度与位置等常数状态。

关键点总结

[!green]

  • 分隔符只结束长度头,正文边界由长度决定,正文不需要转义。
  • 每一项都有自己的头信息,空项、重复项和空列表都能准确还原。
  • 编码长度单位必须与解码截取单位一致,不能混用字符数与字节数。
  • 当前解码依赖合法编码契约,消费位置始终落在下一段头部。

易错点总结

[!yellow]

  • 对整个编码直接按 # 切分,正文中的同名字符也会被错误分段。
  • 长度字段只读一位,无法处理多位长度。
  • 长度为零时跳过加入结果,丢失原列表中的空字符串项。
  • 没有跳过段头分隔符,或取完正文后没有推进到正确位置,后续片段全部错位。
  • 编码记录一种长度单位,解码按另一种单位截取,多字节或多单元字符会破坏边界。
  • 直接把合法数据解码器当作任意外部输入的协议校验器,当前代码并不包含格式与越界检查。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/14915840
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!