一個問題的產生
與筆者同一年代的人應該都有這樣的共同記憶:一個炎日的夏日,坐在沙發上,吃着冰爽的西瓜,看DVD中的迪迦奧特曼動畫片,這樣悠閑的時光即使是短暫的回憶起也令人神往。But nothing can always be perfect,最令人痛心的莫過於碟片光潔的背面產生了划痕,一道巨大的划痕常常意味着我們變不成光。但好在這樣的事情不會經常發生,甚至不可思議的一些較淺的划痕並不會影響動畫片的正常播放。
這件看似平常的事情現在回想起似乎是不合邏輯的。DVD光盤上無數的小坑儲存着0和1,划痕導致了光盤上數據的損失,且這些損失數據所在的位置是隨機的。那么如果直接讀取這些信息,光盤上儲存的視頻信息必然無法正常播放。
由此我們不難推斷出,一定有某種糾錯的機制修復了(至少在某種程度上)這些缺損的信息。那這樣的修復機制是如何實現的呢?
一個很直接的思路就是進行備份。方法也很簡單,我們如果能將這一份數據備份三份,那么我們只需要檢測某一位置上數字與其他兩份是否相同,就能在很大程度上修復錯誤的信息。然而如此大的冗余量明顯是不切合實際的。那么有沒有一種方法既能檢驗並修復出傳輸過程的錯誤(噪音),又只產生盡可能小的冗余呢?
奇偶效驗(Parity Check)
相信聰明的你不難發現,檢驗錯誤和讀取信息之間有着本質上的不同。前者只需檢驗信息流的某一特征來回答是或否的問題,而后者則需要讀取信息流全部的信息。那么對於一個僅由0、1組成的信息串,最顯著的特征無疑是0或1的總數。那么將0或1的個數作為冗余來檢驗錯誤是否可行呢?可行,但難以操作。因為對於一個未知的信息流,我們難以確定所需要的冗余位數。
好在數字的奇偶特性為我們提供一種更為實際的檢驗方法。對於任意長度的數據,僅需一位額外的空間開銷便可確定其奇偶性特征,因此滿足我們對於冗余盡可能小的要求。並且由於單個位上僅存在0、1兩種狀態,那么如果我們知道了發生錯誤的位,若將該位取反,還可以恢復數據(消除噪音)。這種方法便是大名鼎鼎的奇偶效驗。
奇偶校驗是一種添加一個奇偶位用來指示之前的數據中包含有奇數還是偶數個1的檢驗方式。如果在傳輸的過程中,有奇數個位發生了改變,那么這個錯誤將被檢測出來(注意奇偶位本身也可能改變)。一般來說,如果數據中包含有奇數個1的話,則將奇偶位設定為1;反之,如果數據中有偶數個1的話,則將奇偶位設定為0。換句話說,原始數據和奇偶位組成的新數據中,將總共包含偶數個1.
讀到這里,你一定會提出疑問:如果同時有兩個位上的1發生了改變,這樣的情況如何處理呢?是的,奇偶校驗並非總是有效,如果數據中有偶數個位發生變化,則奇偶位仍將是正確的,因此不能檢測出錯誤。而且,即使奇偶校驗檢測出了錯誤,它也不能指出哪一位出現了錯誤,從而難以進行更正。數據必須整體丟棄並且重新傳輸。在一個“容易出錯”(噪音較大)的媒介中,成功傳輸數據可能需要很長時間甚至不可能完成。
做到這一步的我們不想放棄,那能否在奇偶檢驗的方法上做出改進,使其能發現出現錯誤的位置以加以修復呢?1940年代晚期,在貝爾實驗室(Bell Labs)工作的理查德·衛斯里·漢明(Richard Wesley Hamming)因讀卡機不可靠而頻繁發生錯誤而十分沮喪。他遇到了和我們現在相似的困擾,因此開始逐步開發功能日益強大的偵錯算法。在1950年,他發表了一種線性糾錯碼,巧妙地解決了這一問題,這便是我們現在所熟知的漢明碼(Hamming code)。
漢明碼(Hamming code)
記得在一次部門聚會上,一個學長提出玩這樣一個小游戲。游戲的規則很簡單,主持人先會講述一個離奇的事實,而游玩者通過不斷向主持人提出是或否的問題,最后推斷出故事的真相。漢明碼正是采用了這樣的思路以確定了錯誤產生的位置。
算法如下
從1開始給數字的數據位(從左向右)標上序號, 1,2,3,4,5...
將這些數據位的位置序號轉換為二進制,1, 10, 11, 100, 101,等。
數據位的位置序號中所有為二的冪次方的位(編號1,2,4,8,等,即數據位位置序號的二進制表示中只有一個1)是校驗位
所有其它位置的數據位(數據位位置序號的二進制表示中至少2個是1)是數據位
每一位的數據包含在特定的兩個或兩個以上的校驗位中,這些校驗位取決於這些數據位的位置數值的二進制表示
①校驗位1覆蓋了所有數據位位置序號的二進制表示倒數第一位是1的數據:1(校驗位自身,這里都是二進制,下同),11,101,111,1001,等
②校驗位2覆蓋了所有數據位位置序號的二進制表示倒數第二位是1的數據:10(校驗位自身),11,110,111,1010,1011,等
③校驗位4覆蓋了所有數據位位置序號的二進制表示倒數第三位是1的數據:100(校驗位自身),101,110,111,1100,1101,1110,1111,等
④校驗位8覆蓋了所有數據位位置序號的二進制表示倒數第四位是1的數據:1000(校驗位自身),1001,1010,1011,1100,1101,1110,1111,等
⑤簡而言之,所有校驗位覆蓋了數據位置和該校驗位位置的二進制與的值不為0的數。
通過對校驗位進行檢測,我們便可逐步鎖定錯誤發生的位置。值得一提的是無論采用奇校驗還是偶校驗都是可行的。偶校驗從數學的角度看更簡單一些,但在實踐中並沒有區別。
理解漢明碼算法
上述的算法描述有些抽象,為了便於理解漢明碼算法,我們不妨做一個簡單的游戲。
在這個游戲中包含發送者(sender)和接受者(receiver)兩個玩家。發送者會發送一個16bit的數據塊,其中包含了11bit的有效信息。然而在傳輸過程中,某一位可能發生變化。接受者需要根據4位冗余的信息,確定發生錯誤的位置。
為了直觀地表現數據的形式,我們不妨將16bit的數據塊表示為4*4的矩陣形式,並將其根據從左到右、從上到下的順序編號為0~15。其中2的n次方位為檢驗為,n為自然數。
因此,編號為1的空格為第一個檢驗位,負責檢驗2,4列的奇偶檢驗,即保證2,4列中1的個數一定為偶數。
編號為2的空格為第二個檢驗位,負責檢驗3,4列的奇偶檢驗。
編號為4的空格為第三個檢驗位,負責2,4行的奇偶檢驗。
編號為8的空格為最后一個檢驗位,負責3,4行的奇偶檢驗。
由此我們僅需依次對四個校驗位進行效驗,即可最終確定錯誤發生的位置。
到這里你一定會提出這樣的疑問,0位是否多余呢?事實上0位確實可以忽略,但我們可以將0位作為整體的奇偶檢驗位,這樣便能檢驗出是否發生了兩個位的變化(盡管我們並不能知道這兩個位的具體位置)。
比如發送者發出的數據為00110001110(11位),如圖
聰明的你不妨花一點時間,根據剛才的方法填寫下檢驗位的數據。
![]()
現在公布答案,分別為0、0、1、1、1。
假設經過傳輸后,接受者收到的數據塊如下圖所示,這里其實發生了數據錯誤,根據剛才的檢驗方法,你找出發生錯誤的位置了嗎?
![]()
可以看到校驗位2與校驗位8對應的校驗范圍內1的個數為奇數,不符合我們的編碼規則,所以是第8+2=10位(1010)的數字反了,即10位上的1應該是0。
有興趣的話,你還可以選擇其它的數據,進行該游戲。
對於更長的數據塊我們同樣能依據上述的規律進行的檢驗。隨着數據量的增大,你會發現漢明碼算法的簡潔與優雅是如此的驚人。
