qinghuan
← 回到知识探索

计算机趣闻 / 可下载小项目

找到一个可疑位,不代表只坏了一位:亲手拆开 Hamming(7,4)

七位里改一位能恢复,改两位却会指向第三个无辜位置。用全部16个码字核对纠错边界,再看三位错误怎样悄悄通过。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 三张检查表怎样给七个位置不同编号?
  4. 校验位为什么能各自补好偶数?
  5. 两位损坏,为什么校验会冤枉第三位?
  6. 合法码字至少隔三格,保证到底到哪儿?
  7. 对全部码字、单错、双错都实算一次
  8. 多一位整体校验,是另一个协议约定
  9. 记住这三点
  10. 别把例子用过头
  11. 自己试一小步
  12. 查原始资料
开始前,会这些就够了
遇到不熟悉的词,先看这 2 个解释
Hamming 距离
两段相同长度的比特串,在多少个位置取值不同。
syndrome(校验综合)
把各组校验是否失败组合成一个结果,用来识别错误模式。
打开完整术语小抄 ↗

把一张四位小纸条发给别人,路上某一格可能被改成相反值。我们多加三格检查信息,既想知道出了问题,也想定位哪里出了问题。但这个定位承诺有前提:每个七位块最多坏一位。

本例使用普通 Hamming(7,4),位置1、2、4放偶校验位,位置3、5、6、7放数据。位置从左到右编号1到7,和整数最低位在右边的书写习惯分开;每个输出数组都明确沿位置顺序显示。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Windows / Python 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • hamming.py:完整可运行示例
  • README.md:运行说明与独立练习
  • observed.stdout.txt:本次实际输出
  • verification.json:源码和产物验证记录
  • SHA256SUMS.txt:内容摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B hamming.py

解压到独立练习文件夹,在含 hamming.py 的目录打开终端运行。普通Hamming(7,4),偶校验,位置1/2/4校验、3/5/6/7数据;不含第八位SECDED,不测真实信道与错误概率。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

校验归零,原来的数据回来了吗?

把四个数据位放到位置 3、5、6、7;位置 1、2、4 放偶校验位。点击下面的位置翻转接收到的位,再看纠错器会怎么做。

检查 1:位置 1、3、5、7 的异或为 1,不通过。

检查 2:位置 2、3、6、7 的异或为 1,不通过。

检查 4:位置 4、5、6、7 的异或为 0,通过。

校验结果:1 × 1 + 1 × 2 + 0 × 4 = 3。纠错器据此翻转位置 3。

发送:0110011 → 接收:0100011 → 处理后:0110011。

取回数据为 1011;和实验中已知的原数据 1011 相同。处理后的校验结果为 0。

本次至多一位出错,符合单错纠正的保证。

演示普通 Hamming(7,4) 的单错纠正。双错会产生非零校验结果,但把它一律当作单错修正会误纠;要同时纠正单错并区分双错,需要额外机制,例如扩展码的整体奇偶校验。

三张检查表怎样给七个位置不同编号?

第一张表检查1、3、5、7;第二张检查2、3、6、7;第三张检查4、5、6、7。每张表中的1的数量都应是偶数。位置5属于第一与第三张,不属于第二张;位置6属于第二与第三张。七个位置的参与组合各不相同。

把第一、第二、第三张失败分别记为1、2、4,再加起来。若只有位置5翻转,失败组合就得到1+4=5;若只有位置6翻转,得到2+4=6。这个三位检查结果叫 syndrome,可先理解成“哪些检查表亮红灯”的编号。

校验位为什么能各自补好偶数?

先把四个数据位放在3、5、6、7,暂把1、2、4设为0。对于位置1的检查组,若当前1的数量为奇数,就把位置1设为1;否则保留0。位置2和位置4也照此处理。

三个校验位置各只属于自己的那张表,所以依次补齐不会破坏前一张。对数据1011,本例得到沿位置顺序的0110011;四个原始数据仍能直接从3、5、6、7读回。源码会拒绝长度不对、非0/1整数和布尔值。

两位损坏,为什么校验会冤枉第三位?

从合法码字出发,同时翻转位置1和2。第一张表失败,第二张表失败,第三张通过,于是syndrome=3。若接收者坚信最多坏一位,就会翻转位置3。现在1、2、3都与原文不同,却又成了一份通过全部检查的合法码字。

两个不同位置的二进制编号异或不会是0,因此恰好两位翻转时,普通(7,4)的syndrome一定非零:只做检测可以发现异常。但它无法仅凭这个值区分“一位坏在3”和“两位坏在1、2”。一旦自动按单错纠正,双错就可能被误报为已修好;本例枚举里336种双错全部误纠成另一个码字。

合法码字至少隔三格,保证到底到哪儿?

任意两个合法(7,4)码字至少有三位不同,称最小 Hamming 距离为3。一个合法码字周围只翻一位的结果,不会同时距另一合法码字也只差一位,因此可以唯一恢复。

但距原文两位的接收串,可能距另一合法码字只差一位。最近邻解码会更靠近那个错误目标,这不是算法遗漏某个特判,而是现有检查信息不足以同时区分这两种来源。若翻转位置1、2、3,三组检查还会全部通过,syndrome=0也不能无条件宣布没有错误。

对全部码字、单错、双错都实算一次

运行 python -B hamming.py。single_bit_cases_restored为112,即16个合法消息各翻7个位置;double_bit_cases_detectable_but_miscorrected为336,即16×21。输出还给出同一原文翻1、2位时的误纠结果,以及翻1、2、3位时syndrome归零的例子。

参考码字表通过枚举全部128个七位串,并按显式三组位置检查偶数得到,不调用主编码器或syndrome函数。参考解码器计算与每个合法码字逐位不同的数量,选择唯一最近者;这与按syndrome直接定位走不同路径。程序还对全部128种接收串比较两种解码结果,并独立核对码字最小距离是3。

展开收起代码 · 小实验python · 117 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Hamming(7,4): single-bit correction does not distinguish two-bit errors."""
import itertools
import json


def validate(bits, length):
    bits = tuple(bits)
    if len(bits) != length or any(type(b) is not int or b not in (0, 1) for b in bits):
        raise ValueError("exact number of integer zero/one bits required")
    return bits


def encode(data):
    data = validate(data, 4)
    word = [0] * 8  # Position zero is unused.
    for position, bit in zip((3, 5, 6, 7), data):
        word[position] = bit
    for p in (1, 2, 4):
        word[p] = sum(word[i] for i in range(1, 8) if i & p) % 2
    return tuple(word[1:])


def syndrome(word):
    word = validate(word, 7)
    return sum(p for p in (1, 2, 4)
               if sum(word[i-1] for i in range(1, 8) if i & p) % 2)


def correct_assuming_at_most_one(word):
    word = list(validate(word, 7))
    suspect = syndrome(word)
    if suspect:
        word[suspect - 1] ^= 1
    return tuple(word)


def flip(word, positions):
    result = list(word)
    for p in positions:
        result[p-1] ^= 1
    return tuple(result)


def reference_codebook():
    # Independent oracle: enumerate all 128 words and use explicit parity sets.
    # This does not call encode(), syndrome(), or the bit-index construction.
    checks = ((0, 2, 4, 6), (1, 2, 5, 6), (3, 4, 5, 6))
    return [word for word in itertools.product((0, 1), repeat=7)
            if all(sum(word[i] for i in row) % 2 == 0 for row in checks)]


def distance(a, b):
    return sum(x != y for x, y in zip(a, b))


def nearest(word, codebook):
    ranked = sorted((distance(word, code), code) for code in codebook)
    if ranked[0][0] == ranked[1][0]:
        raise AssertionError("ambiguous nearest word")
    return ranked[0][1]


def main():
    checks, singles, doubles = 0, 0, 0
    def check(ok):
        nonlocal checks
        if not ok:
            raise AssertionError("Hamming check failed")
        checks += 1
    codebook = reference_codebook()
    check(len(codebook) == 16)
    check(min(distance(a, b) for a, b in itertools.combinations(codebook, 2)) == 3)
    for data in itertools.product((0, 1), repeat=4):
        word = encode(data)
        check(word in codebook)
        check(tuple(word[i-1] for i in (3, 5, 6, 7)) == data)
        check(correct_assuming_at_most_one(word) == word)
        for p in range(1, 8):
            damaged = flip(word, (p,))
            corrected = correct_assuming_at_most_one(damaged)
            check(syndrome(damaged) == p)
            check(corrected == word)
            check(corrected == nearest(damaged, codebook))
            singles += 1
        for p, q in itertools.combinations(range(1, 8), 2):
            damaged = flip(word, (p, q))
            corrected = correct_assuming_at_most_one(damaged)
            check(syndrome(damaged) != 0)
            check(corrected != word and corrected in codebook)
            check(corrected == nearest(damaged, codebook))
            doubles += 1
    # Every 7-bit input is at distance <= 1 from exactly one legal codeword.
    for word in itertools.product((0, 1), repeat=7):
        check(correct_assuming_at_most_one(word) == nearest(word, codebook))
    original = encode((1, 0, 1, 1))
    double = flip(original, (1, 2))
    triple = flip(original, (1, 2, 3))
    check(syndrome(double) == 3)
    check(syndrome(triple) == 0 and triple != original)
    for bad in [(True, 0, 0, 0), (1, 0, 1), (1, 0, 2, 0)]:
        try:
            encode(bad)
        except ValueError:
            check(True)
        else:
            raise AssertionError("invalid data accepted")
    print(json.dumps({"checks_passed": checks, "single_bit_cases_restored": singles,
                      "double_bit_cases_detectable_but_miscorrected": doubles,
                      "original_positions_1_to_7": original,
                      "flip_1_2": {"received": double, "syndrome": syndrome(double),
                                   "assumed_single_correction": correct_assuming_at_most_one(double)},
                      "flip_1_2_3": {"received": triple, "syndrome": syndrome(triple)},
                      "scope": "plain (7,4), even parity; no eighth parity bit and no SECDED decoder"}, indent=2))


if __name__ == "__main__":
    main()

怎样运行:在独立练习文件夹中执行 python -B hamming.py。2026-10-18 已在 Windows、Python 3.14.0 运行此份源码并通过自带检查。普通Hamming(7,4),偶校验,位置1/2/4校验、3/5/6/7数据;不含第八位SECDED,不测真实信道与错误概率。

运行后对照结果
{
  "checks_passed": 1527,
  "single_bit_cases_restored": 112,
  "double_bit_cases_detectable_but_miscorrected": 336,
  "original_positions_1_to_7": [
    0,
    1,
    1,
    0,
    0,
    1,
    1
  ],
  "flip_1_2": {
    "received": [
      1,
      0,
      1,
      0,
      0,
      1,
      1
    ],
    "syndrome": 3,
    "assumed_single_correction": [
      1,
      0,
      0,
      0,
      0,
      1,
      1
    ]
  },
  "flip_1_2_3": {
    "received": [
      1,
      0,
      0,
      0,
      0,
      1,
      1
    ],
    "syndrome": 0
  },
  "scope": "plain (7,4), even parity; no eighth parity bit and no SECDED decoder"
}

多一位整体校验,是另一个协议约定

给整个七位码字再加一个整体偶校验位,可构成距离4的扩展 Hamming(8,4)码。配套解码规则利用整体奇偶性区分单错纠正与双错报错,通常称SECDED。仅多写一个数字8而不修改解码规则并不会自动获得能力。

本文没有实现第八位或SECDED解码器,也没有模拟真实内存条、无线传输或错误概率。每次flip作用于互不相同的位置;同一位翻两次会抵消,不属于这里恰好两位不同的双错。七位固定长度使运算量固定,但“固定”不等于具备密码学恒定时间保证。

记住这三点

  • syndrome只描述检查失败组合,单错定位依赖最多一位错误的前提。
  • 普通(7,4)可作双错检测,却不能同时把双错与单错自动区分。
  • syndrome=0只说明满足校验方程,多位错误仍可能通过。

别把例子用过头

  • 把非零syndrome当成确凿的单错位置。
  • 把普通(7,4)与加整体奇偶位的SECDED混为一谈。
  • 忽略编号方向,拿数组第0项当作位置0参与校验。

自己试一小步

换一个四位消息后,哪些错误性质仍与消息内容无关?

  1. 自己选择不同于正文的四位消息,并手算七位码字。
  2. 选择一个单错位置和两个不同的双错位置,先推三张检查表。
  3. 保留原文、接收串、syndrome与假设单错的输出,比较是否真正还原。

做到这些,就算完成

  • 校验位位置1、2、4与数据位3、5、6、7保持不变。
  • 逐位比较原文,不能只看修复后syndrome是否为0。
  • 把实际翻转的位数与纠错器假设分开,解释不一致时出现了什么。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。