背景
前几天在b站了看到3b1b的汉明码介绍视频,觉得很有意思,遂写这篇文章记录下学习该概念的笔记。
发明动机
计算机科学或是电信领域,数据传输过程中总是不可避免产生错误,导致终端收到的数据和原来发的数据不一致,因此需要一种纠错机制来判断收到的数据是否正确。
一般人会想到的最简便的方法,便是多复制几段数据:比如一组数据是ABCD,那么就多复制两份一样的数据进行传递,终端读取数据时比较三份数据,按照顺序一个个比特位读过去,相同数最多的比特位可以认为是正确的数据。如下收到三组数据
0101
0101
1101
第0位上,第一、二组数据都是0
,第三组为1
,可以认为第0位正确的数据是0
,其他位的数据三组都一样,所以正确的数据可以认为是0101
但显然这种机制有个缺点:数据冗余,需要原来数据的3倍容量,而且复出这么多容量的代价下,碰到有某两组数组在某位上出现翻转(即1变成0,0变成1),该方法也无法识别出来。
那有什么方法能够尽量使用少量的空间来纠错呢?
汉明码
奇偶校验
考虑有15个比特的数据要传输,将他们按顺序放到如下1-15的格子里面
|1|0|1|0|
|1|0|1|1|
|1|0|0|0|
|1|1|1|0|
这15个比特值中,如果有奇数个1
时,第0位为1
,反之为0
,即保证要传输的数据中有偶数个1
。由于上述数据中有7个1
,则第0位填上1
。这第0
位可以称为奇偶校验位
这样做有什么用呢?
接受数据的一方接受到这16个数据,首先判断下共有几个1,如果有奇数个,即表示传输过程中至少有1
位翻转了(当然也有可能是3、5..个),如果是偶数个,则表示得到的数据是正确的(也可能是有2、4个位翻转了)。
那么这里我们仅仅通过一个比特位,就能“判断”出得到的数据有没有错了。
当然这种方法似乎很“脆弱”,在碰到2个以上的错误时就无法分辨出来了,但它是个很重要的一个基础。
找出错误并纠正
通过上诉描述,我们已经知道了奇偶校验是怎么做了。接下来我们继续使用它,但不对所有数字进行检验,而是将传输的数据分成多组分别进行奇偶校验,如下
1 2 3 4
1 |1|0|1|0|
2 |1|0|1|1|
3 |1|0|0|0|
4 |1|1|1|0|
左侧和上测的数字表示几行,几列。
添加四个检验位,分别在第1、2、4、8位上,即如下打x
的位置(2^n
的位置),(这样放是有讲究的,也是该检验方法的巧妙之处,后面的内容会解释)
1 2 3 4
1 |1|x|x|0|
2 |x|0|1|1|
3 |x|0|0|0|
4 |1|1|1|0|
然后我们分别对2、4列,3、4列,2、4行,3、4行做如上的奇偶校验(共四组对应上面打叉的位置)
分析过程
首先对2、4列进行奇偶校验,结果正确表示错误位是在1、3列,反之在2、4列,同理对3、4列检验,如果正确代表错误在1、2列,反之在3、4列,...经过对四组数据进行校验,我们就可以定位到错误的数据了,如下:
graph TD
开始 --> id1{校验2,4列}
id1 --正确,错在1,3列-->id2{检验3,4列}
id1 --错误,错在2,4列-->id3{检验3,4列}
id2 --正确,错在1列-->id4{检验2,4行}
id2 --错误,错在3列-->id5{检验2,4行}
id3 --正确,错在2列-->id6{检验2,4行}
id3 --错误,错在4列-->id7{检验2,4行}
id4 --正确,错在1,3行-->id8{检验3,4行}
id4 --错误,错在2,4行-->id9{检验3,4行}
id5 --正确,错在1,3行-->id10{检验3,4行}
id5 --错误,错在2,4行-->id11{检验3,4行}
id6 --正确,错在1,3行-->id12{检验3,4行}
id6 --错误,错在2,4行-->id13{检验3,4行}
id7 --正确,错在1,3行-->id14{检验3,4行}
id7 --错误,错在2,4行-->id15{检验3,4行}
id8 --正确-->id16[第1行1列错误]
id8 --正确-->id17[第3行1列错误]
id9 --正确-->id18[第2行1列错误]
id9 --正确-->id19[第4行1列错误]
id10 --正确-->id20[第1行3列错误]
id10 --正确-->id21[第3行3列错误]
id11 --正确-->id22[第2行3列错误]
id11 --正确-->id23[第4行3列错误]
id12 --正确-->id24[第1行2列错误]
id12 --正确-->id25[第3行2列错误]
id13 --正确-->id26[第2行2列错误]
id13 --正确-->id27[第4行2列错误]
id14 --正确-->id28[第1行4列错误]
id14 --正确-->id29[第3行4列错误]
id15 --正确-->id30[第2行4列错误]
id15 --正确-->id31[第4行4列错误]
过程就类似二分法的方式,这里检测出错误的位置,只需翻转,就可以认为是正确的数据了
这里有一个问题,四组分析后会有16种错误,但实际上还有一种情况是正确,因此这里做一个处理,第0位(即第一行第一列)不携带数据(四次判断正确即表示数据正确),这样就刚好15+1(正确)种情况。
那么第0位就让他空着吗?那太浪费资源了,这里可以把第0位也作为奇偶校验位,校验所有数据,像上文一样。那么这里设置有什么用呢?
这里分两种情况考虑
- 如果四组检验出来有错误,而第0位的检验有错误,则可以认为就是有一位数据错误了(翻转一下,第0位就正确了)
- 如果四组检验出来有错误,而第0位的检验有正确,则可以认为有两个位置错了,因为两个位置的数据翻转不影响整体的奇偶性(虽然不能纠正两个错误)
代码实现
function checkError(data) {
return data.map((val, idx) => val === 1 ? idx : 0)
.reduce((a, b) => a ^ b)
}
为什么上诉代码就能找出对应错误的位置呢?不是应该进行四次判断一次次筛选吗?这就是为什么要这样分组以及检验码为什么放在2^n
位置的原因了
首先我们将每个位对应的序号用二进制表示
|0000|0001|0010|0011|
|0100|0101|0110|0111|
|1000|1001|1010|1011|
|1100|1101|1110|1111|
首先我们观察第2、4列,可以看到其对应二进制数字的第0位都是1
,那么将这2、4列中所有位1
的数进行异或处理,如果有偶数个,则得出的数的第0位为0
即表示错误在第1、3列,有奇数个则得出第0位为1
,即表示错误在2、4列(太巧妙了!),其他分组同理(3、4列第一位都是1
...),因此我们就可以吧所有为1的数对应的位置进行异或,这样得到的数据极为错误位置。
注意到校验位只有一个位上是0,即只会影响其中一次的检验。
这就是为什么校验位要放在这个位置,以及为什么分组要这样分的缘故
当然汉明码也只能对出现一个错误的情况有用,两个及以上就没办法了。但是校验位对空间的占用很小,只需用几个空间就能纠正出一个错误,也是个不错的选择,特别当,如果用一个16*16的格子,那也仅需要8个检验位。
同样为了避免两次出错,我们会选择合适大小长度的作为块,例如一个16个数字的块出现2个错误的概率肯定是比256个数字的块低。