1. 程式人生 > 實用技巧 >程式設計師的數學基礎課 原來取餘操作本身就是個雜湊函式 4

程式設計師的數學基礎課 原來取餘操作本身就是個雜湊函式 4


你好,我是黃申。今天我們來聊聊“餘數”。提起來餘數,我想你肯定不陌生,因為我們生活中就有很多很多與餘數相關的例子。比如說,今天是星期三,你想知道 50 天之後是星期幾,那你可以這樣算,拿 50 除以 7(因為一個星期有 7 天),然後餘 1,最後在今天的基礎上加一天,這樣你就能知道 50 天之後是星期四了。再比如,我們做 Web 程式設計的時候,經常要用到分頁的概念。如果你要展示 1123 條資料,每頁 10 條,那該怎麼計算總共的頁數呢?我想你肯定是拿 1123 除以 10,最後得到商是 112,餘數是 3,所以你的總頁數就是 112+1=113,而最後的餘數就是多出來,湊不夠一頁的資料。
看完這幾個例子,不知道你有沒有發現,餘數總是在一個固定的範圍內。比如你拿任何一個整數除以 7,那得到的餘數肯定是在 0~6 之間的某一個數。所以當我們知道 1900 年的 1 月 1 日是星期一,那便可以知道這一天之後的第 1 萬天、10 萬天是星期幾,是不是很神奇?你知道,整數是沒有邊界的,它可能是正無窮,也可能是負無窮。但是餘數卻可以通過某一種關係,讓整數處於一個確定的邊界內。我想這也是人類發明星期或者禮拜的初衷吧,任你時光變遷,我都是以 7 天為一個週期,“周”而復始地過著確定的生活。因為從星期的角度看,不管你是哪一天,都會落到星期一到星期日的某一天裡。
我們再拿上面星期的例子來看。假如今天是星期一,從今天開始的 100 天裡,都有多少個星期呢?你拿 100 除以 7,得到商 14 餘 2,也就是說這 100 天裡有 14 周多 2 天。換個角度看,我們可以說,這 100 天裡,你的第 1 天、第 8 天、第 15 天等等,在餘數的世界裡都被認為是同一天,因為它們的餘數都是 1,都是星期一,你要上班的日子。同理,第 2 天、第 9 天、第 16 天餘數都是 2,它們都是星期二。這些數的餘數都是一樣的,所以被歸類到了一起,有意思吧?是的,我們的前人早已注意到了這一規律或者特點,所以他們把這一結論稱為同餘定理。簡單來說,就是兩個整數 a 和 b,如果它們除以正整數 m 得到的餘數相等,我們就可以說 a 和 b 對於模 m 同餘。


也就是說,上面我們說的 100 天裡,所有星期一的這些天都是同餘的,所有星期二的這些天就是同餘的,同理,星期三、星期四等等這些天也都是同餘的。
還有,我們經常提到的奇數和偶數,其實也是同餘定理的一個應用。當然,這個應用裡,它的模就是 2 了,2 除以 2 餘 0,所以它是偶數;3 除以 2 餘 1,所以它是奇數。2 和 4 除以 2 的餘數都是 0,所以它們都是一類,都是偶數。3 和 5 除以 2 的餘數都是 1,所以它們都是一類,都是奇數。
你肯定會說,同餘定理就這麼簡單嗎,這個定理到底有什麼實際的用途啊?其實,我上面已經告訴你答案了,你不妨先自己思考下,同餘定理的意義到底是什麼。
簡單來說,同餘定理其實就是用來分類的
(如分庫分表)。你知道,我們有無窮多個整數,那怎麼能夠全面、多維度地管理這些整數?同餘定理就提供了一個思路。
因為不管你的模是幾,最終得到的餘數肯定都在一個範圍內。比如我們上面除以 7,就得到了星期幾;我們除以 2,就得到了奇偶數。所以按照這種方式, 我們就可以把無窮多個整數分成有限多個類。
這一點,在我們的計算機中,可是有大用途。
雜湊(Hash)你應該不陌生,在每個程式語言中,都會有對應的雜湊函式。雜湊有的時候也會被翻譯為雜湊,簡單來說,它就是將任意長度的輸入,通過雜湊演算法,壓縮為某一固定長度的輸出。這話聽著是不是有點耳熟?我們上面的求餘過程不就是在做這事兒嗎?
舉個例子,假如你想要快速讀寫 100 萬條資料記錄,要達到高速地存取,最理想的情況當然是開闢一個連續的空間存放這些資料,這樣就可以減少定址的時間。但是由於條件的限制,我們並沒有能夠容納 100 萬條記錄的連續地址空間,這個時候該怎麼辦呢?
我們可以考察一下,看看系統是否可以提供若干個較小的連續空間,而每個空間又能存放一定數量的記錄。比如我們找到了 100 個較小的連續空間,也就是說,這些空間彼此之間是被分隔開來的,但是內部是連續的,並足以容納 1 萬條記錄連續存放,那麼我們就可以使用餘數和同餘定理來設計一個雜湊函式,並實現雜湊表的結構

那這個函式應該怎麼設計呢?你可以先停下來思考思考,提醒你下,你可以再想想星期幾的那個例子,因為這裡面用的就是餘數的思想。


在這個公式中,x 表示等待被轉換的數值,而 size 表示有限儲存空間的大小,mod 表示取餘操作。通過餘數,你就能將任何數值,轉換為有限範圍內的一個數值,然後根據這個新的數值,來確定將資料存放在何處。
具體來說,我們可以通過記錄標號模 100 的餘數,指定某條記錄存放在哪個空間。這個時候,我們的公式就變成了這樣:

假設有兩條記錄,它們的記錄標號分別是 1 和 101。我們把這些模 100 之後餘數都是 1 的,存放到第 1 個可用空間裡。以此類推,將餘數為 2 的 2、102、202 等,存放到第 2 個可用空間,將 100、200、300 等存放到第 100 個可用空間裡。
這樣,我們就可以根據求餘的快速數字變化,對資料進行分組,並把它們存放到不同的地址空間裡。而求餘操作本身非常簡單,因此幾乎不會增加定址時間。

除此之外,為了增加資料雜湊的隨機程度,我們還可以在公式中加入一個較大的隨機數 MAX,於是,上面的公式就可以寫成這樣:

我們假設隨機數 MAX 是 590199,那麼我們針對標號為 1 的記錄進行重新計算,最後的計算結果就是 0,而針對標號 101 的記錄,如果隨機數 MAX 取 627901,對應的結果應該是 2。這樣先前被分配到空間 1 的兩條記錄,在新的計算公式作用下,就會被分配到不同的可用空間中。
你可以嘗試記錄 2 和 102,或者記錄 100 和 200,最後應該也是同樣的情況。你會發現,使用了 MAX 這個隨機數之後,被分配到同一個空間中的記錄就更加“隨機”,更適合需要將資料重新洗牌的應用場景,比如加密演算法、MapReduce 中的資料分發、記錄的高速查詢和定位等等。
讓我以加密演算法為例,在這裡面引入 MAX 隨機數就可以增強加密演算法的保密程度,是不是很厲害?舉個例子,比如說我們要加密一組三位數,那我們設定一個這樣的加密規則:

先對每個三位數的個、十和百位數,都加上一個較大的隨機數。
然後將每位上的數都除以 7,用所得的餘數代替原有的個、十、百位數;
最後將第一位和第三位交換。

這就是一個基本的加密變換過程。
假如說,我們要加密數字 625,根據剛才的規則,我們來試試。假設隨機數我選擇 590127。那百、十和個位分別加上這個隨機數,就變成了 590133,590129,590132。然後,三位分別除以 7 求餘後得到 5,1,4。最終,我們可以得到加密後的數字就是 415。因為加密的人知道加密的規則、求餘所用的除數 7、除法的商、以及所引入的隨機數 590127,所以當拿到 415 的時候,加密者就可以算出原始的資料是 625。是不是很有意思?

小結
到這裡,餘數的所有知識點我們都講完了。我想在此之前,你肯定是知道餘數,也明白怎麼求餘。但對於餘數的應用不知道你之前是否有思考過呢?我們經常說,數學是計算機的基礎,在餘數這個小知識點裡,我們就能找到很多的應用場景,比如我前面介紹的雜湊函式加密演算法,當然,也還有我們沒有介紹到的,比如迴圈冗餘校驗等等。餘數只是數學知識中的滄海一粟。你在中學或者大學的時候,肯定接觸過很多的數學知識和定理,程式設計的時候也會經常和數字、公式以及資料打交道,但是真正學懂數學的人卻沒幾個。希望我們可以從餘數這個小概念開始,讓你認識到數學思想其實非常實用,用好這些知識,對你的程式設計,甚至生活都有意想不到的作用。