资讯动态

唯一可译码判定:Kraft不等式与Sardinas-Patterson算法

发布时间:2026/10/4 4:30:40 来源:尧图企业网站定制
做通信系统或者编码方案的时候绕不开一个词唯一可译码。判定一个码是不是唯一可译码听起来是信息论教科书里的理论问题但真正动手设计变长码、比对霍夫曼编码或者考卷上写证明题的时候你会发现自己还是需要一套能落地的判定方法而不是只背一个定义。这篇就是来聊清楚这件事的唯一可译码到底是什么意思怎么快速判定判定过程中有哪些坑是教材里不会明说的以及怎么把判定过程写成一目了然的代码。信息论教材通常会把“唯一可译码”“即时码”“Kraft不等式”排成一章但很多初学者看完定义还是不会判断一个具体码是不是唯一可译。原因在于唯一可译码的定义是针对“任意有限长码符号串”的这个“任意”听起来太抽象了。没有可行的判定算法定义就只是定义没法用。好在这个问题其实有非常成熟的解法——Sardinas-Patterson算法通常简称为SP算法。这篇文章会用通俗的语言把这个算法讲透配上完整的手算例子和Python实现顺便聊几个我实际踩过的坑。无论你是信息论方向的学生、通信工程从业者还是单纯对编码理论感兴趣的读者看完都应该能自己动手判定一个码是否唯一可译。1. 先搞清楚什么才算“唯一可译”1.1 唯一可译码的严格定义所谓码code本质是一张映射表把源符号集合中的每个符号映射到一个由码符号组成的码字。比如二元码中源符号a、b、c可以分别映射为“0”“10”“11”这张表就是一个码。唯一可译码的定义是对于任意有限长的码符号串如果它能够被分割成一个个码字的序列那么这个分割方式必须是唯一的。换句话说任意一个码符号串不可能存在两种不同的码字序列切分方式使得两者拼起来得到完全相同的符号串。举个例子码C {0, 01, 10}。看码符号串“010”它可以被切分为“0 | 10”对应源符号序列假设映射关系是a-0, b-01, c-10就是a c但它也可以被切分为“01 | 0”对应源符号序列就是b a。同一个码符号串两套完全不同的切分都能对应到合法的码字序列所以这个码就不是唯一可译码。而码C {0, 10, 11}则没有这个问题。任何由0、1组成的串比如“011010”切分的时候看到0就是一个码字看到10或11也都是明确的码字不存在歧义所以它是唯一可译码。1.2 为什么“唯一可译”这么重要编码的最终目的是传输和存储。如果接收端拿到一串二进制却无法确定它到底对应哪一串源符号解码就失去了可靠性。实际系统中如果码不是唯一可译的轻则解码出错重则整个数据流都崩掉。在数据压缩领域变长码比如霍夫曼编码天然就要求唯一可译否则解压出来的内容和原始数据对不上。在通信系统的信道编码设计中虽然还会加入纠错能力但码本身的唯一可译性依然是前提条件。也正因为唯一可译码这么重要才需要一套系统性的判定方法而不是靠肉眼观察几个例子就下结论。1.3 先区分三个容易混淆的概念唯一可译码、即时码、前缀码这三个概念经常被混着说但严格来说并不等价。前缀码也叫即时码指的是任何一个码字都不是另一个码字的前缀。比如{0, 10, 110}就是前缀码因为没有一个码字是另一个的开头部分。即时码的意思是接收端收到最后一个码字符号的瞬间就能判断出一个码字已经完整到达不需要“等一等再看后面”。前缀码一定是即时码即时码也一定是前缀码这两个词在离散无记忆信源编码里基本可以通用。唯一可译码的范围比前缀码更大。一个码可以是唯一可译的但并前缀码都不是例如后面会详细聊的{0, 01, 011}。所以检查前缀关系只是判定唯一可译码的充分条件和快速途径而不是必要条件。2. 为什么Kraft不等式只能当“入场券”而不是“判决书”2.1 Kraft不等式究竟说了什么Kraft不等式是编码理论中的经典结论对于任意一个唯一可译码其码字长度l1, l2, ..., lq一定满足二进制情况下 sum(2^(-li)) ≤ 1更一般地对于D元码 sum(D^(-li)) ≤ 1这个不等式常被用来做“必要性检查”如果一个码的码长分布不满足Kraft不等式那它一定不是唯一可译码。但需要注意满足Kraft不等式只是必要条件不是充分条件。2.2 一个满足Kraft但非唯一可译的经典反例码C {0, 01, 10}码长分别是1、2、2。计算2^(-1) 2^(-2) 2^(-2) 0.5 0.25 0.25 1恰好等于1满足Kraft不等式的等号条件。但这个码我们前面已经分析过不是唯一可译码。这就是最经典的“满足Kraft却不是唯一可译”的反例。很多人在这里会栽跟头以为Kraft不等式满足了码就一定是唯一可译的。事实远非如此。Kraft不等式只给出了“码长分布是否可能存在一个唯一可译码”的必要条件至于具体码字分配成什么样还得另外判定。2.3 再看前缀条件的作用如果Kraft不等式满足并且这个码还是一个前缀码那它一定唯一可译。这是因为前缀码的编码树是一棵完整的树任意一个叶子节点对应的编码都不会与另一个叶子编码产生前缀冲突解码时自然可以沿着树从根走到叶子路径唯一切割唯一。所以一个比较实用的判定思路是分步走先查Kraft再查前缀关系最后才动用完整的SP算法。这样做的好处是很多实际场景中码字都是按前缀码设计的比如霍夫曼编码前两步就能快速给出结论不需要每次都跑完整的复杂判定。3. Sardinas-Patterson判定法尾缀集合的追逃游戏3.1 核心思想用“尾缀”追踪解码歧义SP算法的核心逻辑非常直观可以用一句话概括如果一个码不是唯一可译码那么必然存在一个码符号串可以被切成两套码字序列。这两套序列在某个位置开始分叉而分叉的来源必然是一个码字是另一个码字的前缀。举个最简单的例子。码字“01”和“0”较长码字“01”是以较短码字“0”开头的前缀是“0”剩余部分是“1”。这个“1”就是尾缀。如果这个尾缀本身也是一个码字或者通过其他码字继续传递下去最终碰上一个码字那么歧义就已经形成了。SP算法本质上就是把这些“前缀剥掉后剩下的尾缀”全部收集起来迭代地检查尾缀集合是否与码字集合发生碰撞。只要发生碰撞就说明存在一个符号串可以通过至少两种方式切分成码字序列。3.2 尾缀集合的构造规则用S表示尾缀集合初始集合记为S1。构造规则是这样的第一步对任意两个码字ci和cjci不等于cj如果ci是cj的前缀那么把cj去掉前缀ci之后剩下的部分加入S1。例如ci0cj01那么剩余“1”加入S1。第二步对当前尾缀集合S_k中的每一个元素u以及码字集合C中的每一个码字c做同样的前缀关系判断如果u是c的前缀那么把c去掉u之后剩下的部分加入S_{k1}如果c是u的前缀那么把u去掉c之后剩下的部分加入S_{k1}。判定规则是如果在某一步尾缀集合S_k与码字集合C有交集也就是某个尾缀恰好是一个码字那么该码不是唯一可译码。如果某一步尾缀集合为空或者尾缀集合进入循环且始终没有与码字集合相交那么该码是唯一可译码。3.3 为什么“尾缀命中码字”就意味着不可唯一译码这里值得多解释一步。假设在迭代过程中尾缀u出现在某一层而u恰好又是码字集合C中的某个码字。那么我们可以回溯构造出一个码符号串X它对应两套不同的码字切分。简单来说初始时一个码字ci是另一个码字cj的前缀留下了尾缀u。如果u本身就是一个码字ck那么码符号串ci u就可以被切分为ci | u即两个码字ci和u而ci u恰好等于cj可以切分为一个码字cj。于是“ci | u”和“cj”就是同一个符号串的两种切分方式歧义产生了。更复杂的情况则需要多轮迭代但本质逻辑是一样的每一轮迭代都是在传递“前缀差异”直到最后碰到一个码字把这个差异“闭合”成两套完整的切分方案。3.4 算法一定会终止吗判断一个码是否唯一可译最怕“无限循环”。但SP算法是可以保证在有限步内终止的。理由很简单每个尾缀都是某个码字去掉前缀后剩下的部分因此尾缀的长度不会超过码字集合中的最大码长。最大码长是有限的所以所有可能的尾缀种类也是有限的。尾缀集合的数量有限迭代过程要么在某一步变空要么重复出现之前出现过的集合状态。重复出现意味着进入循环循环状态下不会产生新的尾缀也不会摇身一变蹦出新的码字。因此只要设置一个集合状态记录遇到重复集合就停止判定结果不受影响。4. 手把手实操两个典型案例跑一遍完整判定流程4.1 案例一非唯一可译码的判定全过程取码C {0, 01, 10}最大码长为2共有3个码字。下面完整跑一遍SP算法。初始尾缀集合S1的构造检查“0”和“01”0是01的前缀剩余尾缀为“1”加入S1检查“0”和“10”0不是10的前缀10也不是0的前缀无尾缀检查“01”和“10”01不是10的前缀10也不是01的前缀无尾缀其他组合同理没有新增。得到S1 {“1”}。接下来判断S1是否与C有交集C {0, 01, 10}S1 {“1”}1不是码字所以继续迭代。构造S2取S1中的元素“1”与每个码字比较“1”与码字“0”1不以0开头0不以1开头无前缀关系“1”与码字“01”1不以0开头01不以1开头无前缀关系“1”与码字“10”1是10的前缀10去掉前缀1后剩下“0”把“0”加入S2。得到S2 {“0”}。此时检查S2与C的交集C中包含码字“0”S2中也包含“0”发生了碰撞。判定结果码C {0, 01, 10}不是唯一可译码。验证一下码符号串“010”有两种合法切分“0|10”和“01|0”完全符合定义。4.2 案例二唯一可译但非前缀码的神奇案例取码C {0, 01, 011}。这个码明显不是前缀码“0”是“01”的前缀“01”又是“011”的前缀。很多人第一眼会觉得它不是唯一可译码实际上它是。跑一遍SP算法 S1构造“0”是“01”的前缀尾缀“1”加入“0”是“011”的前缀尾缀“11”加入“01”是“011”的前缀尾缀“1”已存在。得到S1 {“1”, “11”}。S1与C无交集。构造S2分别取“1”和“11”与每个码字比较取“1”与“0”无前缀关系与“01”无前缀关系与“011”无前缀关系取“11”与“0”无前缀关系与“01”无前缀关系与“011”比较11不是011的前缀011也不是11的前缀无尾缀。得到S2 ∅。S2为空判定该码是唯一可译码。整个过程到此终止。这个例子非常值得反复体会。它说明前缀码是唯一可译码的充分条件但远非必要条件。唯一可译码这个集合比我们直觉中“看起来不会混淆”的码要大得多。4.3 实操过程中的注意事项手算SP算法时有几个小建议。第一建议把码字按照长度排序从短到长逐个检查不容易漏掉前缀关系。第二尾缀集合建议用集合而不是列表来记录因为重复的尾缀没有必要保留。第三每生成一层尾缀集合立刻与码字集合做交集判断一旦碰撞马上停止不用继续算下去。第四如果尾缀集合进入“似乎要循环”的状态可以画一个状态表格记录每层集合看到重复集合就停止判定为唯一可译码。5. 常见误判与踩坑实录我在判定时反复栽过的跟头5.1 误把Kraft不等式当充分条件这个坑前面已经重点说过。考场上很多人算出一个码满足Kraft不等式就直接写“是唯一可译码”这是非常典型的错误。反例{0, 01, 10}就是一个教训。Kraft不等式的作用是排查那些码长分布本身就不可能构成唯一可译码的情况而不是直接认可一个码。做了这么多年编码相关工作我的习惯是先算Kraft再跑SP两步结合起来才敢下结论。5.2 看到“非前缀码”就直接宣判码{0, 01, 011}就是最好的反例它存在层层前缀关系但依然是唯一可译码。所以非前缀码不等于非唯一可译码。前缀关系只是产生歧义的一个来源而不是全部来源。严谨的判定必须依赖SP算法而不是凭直觉看“像不像有歧义”。5.3 漏掉重复码字的检查SP算法的前提是码字集合中不能出现完全相同的码字。如果两个不同的源符号映射到同一个码字那显然连可译都谈不上更不用说唯一可译。所以实际判定前第一步永远先检查是否有重复码字。重复码字这种低级问题在手工设计的编码表里偶尔真的会出现尤其是码表较长、手工填写的时候。5.4 以为只要出现尾缀就一定不是唯一可译码这是另一个常见的错误理解。尾缀集合出现非空元素只是说明码字之间存在前缀关系。前缀关系存在并不代表一定产生歧义除非尾缀最终与码字集合发生碰撞。比如{0, 01, 011}的S1集合有“1”和“11”看起来“很危险”但迭代一步就空掉了并没有碰撞。所以正确的理解是尾缀是歧义的“种子”但种子不发芽就不能判死刑。5.5 程序实现时忽略空串处理如果码字集合中包含空串事情会变得非常奇怪空串可以和任何码字拼接任何编码串都可以通过在任意位置插入空串“码字”而得到新的切分方式所以码一定不是唯一可译码。实际应用中码字一般默认非空但如果你写程序处理外部输入最好显式检查一下空串不然SP算法在构造尾缀时会出现无限循环或者把空串当成正常码字的隐患。5.6 定长码的快速判断技巧有一种特殊情况可以不走SP算法直接得出结论如果所有码字长度相同且没有重复码字那么这个码一定是唯一可译码。因为定长码的分割是固定的每L个符号切成一个码字只要码字互不相同切分方式就是唯一的。这个结论看似简单但在处理实际编码表时能省不少事。6. 把判定写成程序从手算到自动化6.1 判定流程的整体设计把整个判定过程转化为程序流程可以分成四步第一步输入码字列表检查是否有重复码字或空串如果有直接返回“不是唯一可译码”。第二步计算Kraft不等式如果连必要条件都不满足可以直接返回“不是唯一可译码”。这一步是优化不是必须的因为SP算法本身能在有限步内给出准确结论但Kraft检查通常能把问题提前暴露。第三步检查前缀码条件如果没有任何码字是另一个码字的前缀直接返回“是唯一可译码”。这一步也是优化很多合法编码都是前缀码检查前缀条件只需要O(n^2)次字符串比较比跑完整的SP迭代更快。第四步进入SP算法主体迭代构造尾缀集合直到出现碰撞或集合为空或集合状态重复。6.2 一个可以直接跑的Python实现下面给出一个完整的Python实现。代码以清晰易读为主用于教学场景不追求极端性能优化。def is_uniquely_decodable(codes): # 基础检查重复码字 if len(codes) ! len(set(codes)): return False # 基础检查空串 if in codes: return False # 基础检查Kraft不等式二元码 kraft_sum sum(2.0 ** (-len(c)) for c in codes) if kraft_sum 1 1e-9: return False # 快速检查前缀码 n len(codes) prefix_free True for i in range(n): for j in range(n): if i ! j and codes[j].startswith(codes[i]): prefix_free False break if not prefix_free: break if prefix_free: return True # SP算法主体 # 构造初始尾缀集合 S1 S set() for c1 in codes: for c2 in codes: if c1 ! c2 and c2.startswith(c1): S.add(c2[len(c1):]) # 注意没有尾缀时表示没有前缀冲突已经可以判定 if not S: return True seen_states set() while S: # 如果尾缀集合中出现码字则存在歧义 if S set(codes): return False # 状态去重防止无限循环 state_key tuple(sorted(S)) if state_key in seen_states: return True seen_states.add(state_key) # 构造下一层尾缀集合 new_S set() for u in S: for c in codes: if c.startswith(u): new_S.add(c[len(u):]) elif u.startswith(c): new_S.add(u[len(c):]) S new_S # 尾缀集合为空表示没有产生歧义 return True # 测试 test_codes [0, 01, 10] print(is_uniquely_decodable(test_codes)) # False test_codes2 [0, 01, 011] print(is_uniquely_decodable(test_codes2)) # True test_codes3 [0, 10, 11] print(is_uniquely_decodable(test_codes3)) # True运行这段代码输出分别是False、True、True与前面手算结论完全一致。6.3 实现时需要注意的细节第一个细节是前缀检查里的双重循环需要排除i等于j的情况否则每个码字都是自身的前缀会把所有码都误判为“非前缀码”。第二个细节是状态去重时尾缀集合要先转为有序元组因为集合本身是无序且不可哈希的直接用集合作为字典键会报错。第三个细节是浮点数比较Kraft不等式时要留一点容差比如1e-9避免二进制浮点计算误差造成的误判。6.4 程序化判定的适用范围这个实现适合码字数量在几百以内的场景复杂度大约为O(n^2 * L)其中n是码字数L是最大码长。如果码字规模特别大可以考虑用Trie树前缀树来加速前缀判断但编码理论常见的码表规模都不大普通双重循环已经完全够用。7. 由热词“仙人掌图判定”延伸结构判定问题之间的暗号7.1 仙人掌图判定是什么最近关注到一个挺有意思的热词叫“仙人掌图判定”。仙人掌图是图论里的一类特殊连通图它的每条边最多只属于一个简单环。也就是说图中的环与环之间可以共用一个顶点但不能共用一条边。整个图看起来就像仙人掌的茎上长出许多独立的刺环因而得名。判定一个图是否为仙人掌图常用的做法是DFS遍历同时记录每条边被多少个环覆盖。遍历过程中只要发现一条边同时属于两个不同的环就说明这个图不是仙人掌图。这类算法和信息论似乎八竿子打不着但细想一下它和唯一可译码判定在思维模式上其实有暗号相通。7.2 两者的共性局部性质与全局性质的博弈唯一可译码判定的难点在于歧义可能出现在任意长度的码符号串中是一个全局性质。但SP算法通过尾缀集合的局部迭代把无限长度的可能性压缩到有限状态里从而在有限步内给出结论。仙人掌图判定也有类似的逻辑一个图是否是仙人掌图看起来需要检查所有可能的环组合是全局性质。但DFS遍历时通过记录每条边被环覆盖的次数就能在遍历过程中逐步积累局部信息最终给出全局判断。这两类问题的共同点是它们都要求通过有限的局部检查来回答一个全局性质问题。理解了这一层再看SP算法和DFS判环算法你会发现它们的骨架惊人地相似——都需要维护一个“状态集合”都需要处理“终止条件”都需要防止死循环。这种跨领域的结构相似性正是算法思维最迷人的地方。7.3 从热词得到的启发说实话刚看到“仙人掌图判定”这个热搜词时我还愣了一下以为是什么编码相关的冷门术语。查了一下发现是图论问题之后反而觉得这个联动很有意思。它提醒我很多看似不相关的判定问题底层方法是可以互相借鉴的。比如SP算法中的“状态集合去重”技巧在仙人掌图判定里就对应“边覆盖计数”反过来图遍历中的“访问标记”技巧也可以帮助理解SP算法为什么要记录已经出现过的尾缀集合状态。所以如果你正在学唯一可译码判定不妨顺手了解一下仙人掌图判定两者对照着看对“如何把无限可能性压缩成有限检查”这件事的理解会更深一层。一些实际使用的体会写到这儿突然想起自己当年第一次手算SP算法的经历。当时对着教材上的定义看了半天觉得每一步都懂但合上书自己写例子就卡住了。卡住的原因后来想明白了光知道“构造尾缀集合”这句话是不够的必须真正动手把一个码从S1算到S2算到看见尾缀“1”撞上码字“0”的那一瞬间才算真正理解了什么叫“碰撞判定”。我自己现在做编码方案验证的时候通常不会只跑一种判定方法。线段式的流程已经刻在脑子里先查重复码字和空串再算Kraft不等式然后看前缀情况最后用程序跑SP算法。手算和程序交叉验证能最大程度避免低级错误。最后再分享一个小技巧。如果你在考试或面试中遇到唯一可译码判定时间又紧可以先尝试构造一个“可疑序列”。所谓可疑序列就是找一个较长的码字看看它能否通过拆分成“较短码字一系列尾缀”的方式与另一种切分产生冲突。构造可疑序列的成功率虽然不高但一旦构造出来比跑完整SP算法要快得多、直观得多。构造不出来的时候再用SP算法稳扎稳打地判定。这算是我在实践中学到的一个经验补充希望对你也有用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑