W3Cschool
恭喜您成為首批注冊(cè)用戶(hù)
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
在上兩節(jié)中,我們了解了哈希表的工作原理和哈希沖突的處理方法。然而無(wú)論是開(kāi)放尋址還是鏈地址法,它們只能保證哈希表可以在發(fā)生沖突時(shí)正常工作,但無(wú)法減少哈希沖突的發(fā)生。
如果哈希沖突過(guò)于頻繁,哈希表的性能則會(huì)急劇劣化。如圖 6-7 所示,對(duì)于鏈地址哈希表,理想情況下鍵值對(duì)平均分布在各個(gè)桶中,達(dá)到最佳查詢(xún)效率;最差情況下所有鍵值對(duì)都被存儲(chǔ)到同一個(gè)桶中,時(shí)間復(fù)雜度退化至 O(n) 。
圖 6-7 哈希沖突的最佳與最差情況
鍵值對(duì)的分布情況由哈希函數(shù)決定。回憶哈希函數(shù)的計(jì)算步驟,先計(jì)算哈希值,再對(duì)數(shù)組長(zhǎng)度取模:
index = hash(key) % capacity
觀察以上公式,當(dāng)哈希表容量 capacity 固定時(shí),哈希算法 hash() 決定了輸出值,進(jìn)而決定了鍵值對(duì)在哈希表中的分布情況。
這意味著,為了減小哈希沖突的發(fā)生概率,我們應(yīng)當(dāng)將注意力集中在哈希算法 hash() 的設(shè)計(jì)上。
為了實(shí)現(xiàn)“既快又穩(wěn)”的哈希表數(shù)據(jù)結(jié)構(gòu),哈希算法應(yīng)包含以下特點(diǎn)。
實(shí)際上,哈希算法除了可以用于實(shí)現(xiàn)哈希表,還廣泛應(yīng)用于其他領(lǐng)域中。
對(duì)于密碼學(xué)的相關(guān)應(yīng)用,為了防止從哈希值推導(dǎo)出原始密碼等逆向工程,哈希算法需要具備更高等級(jí)的安全特性。
請(qǐng)注意,“均勻分布”與“抗碰撞性”是兩個(gè)獨(dú)立的概念,滿(mǎn)足均勻分布不一定滿(mǎn)足抗碰撞性。例如,在隨機(jī)輸入 key 下,哈希函數(shù) key % 100 可以產(chǎn)生均勻分布的輸出。然而該哈希算法過(guò)于簡(jiǎn)單,所有后兩位相等的 key 的輸出都相同,因此我們可以很容易地從哈希值反推出可用的 key ,從而破解密碼。
哈希算法的設(shè)計(jì)是一個(gè)需要考慮許多因素的復(fù)雜問(wèn)題。然而對(duì)于某些要求不高的場(chǎng)景,我們也能設(shè)計(jì)一些簡(jiǎn)單的哈希算法。
simple_hash.cpp
/* 加法哈希 */
int addHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = (hash + (int)c) % MODULUS;
}
return (int)hash;
}
/* 乘法哈希 */
int mulHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = (31 * hash + (int)c) % MODULUS;
}
return (int)hash;
}
/* 異或哈希 */
int xorHash(string key) {
int hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash ^= (int)c;
}
return hash & MODULUS;
}
/* 旋轉(zhuǎn)哈希 */
int rotHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;
}
return (int)hash;
}
觀察發(fā)現(xiàn),每種哈希算法的最后一步都是對(duì)大質(zhì)數(shù) 1000000007 取模,以確保哈希值在合適的范圍內(nèi)。值得思考的是,為什么要強(qiáng)調(diào)對(duì)質(zhì)數(shù)取模,或者說(shuō)對(duì)合數(shù)取模的弊端是什么?這是一個(gè)有趣的問(wèn)題。
先拋出結(jié)論:當(dāng)我們使用大質(zhì)數(shù)作為模數(shù)時(shí),可以最大化地保證哈希值的均勻分布。因?yàn)橘|(zhì)數(shù)不會(huì)與其他數(shù)字存在公約數(shù),可以減少因取模操作而產(chǎn)生的周期性模式,從而避免哈希沖突。
舉個(gè)例子,假設(shè)我們選擇合數(shù) 9 作為模數(shù),它可以被 3 整除。那么所有可以被 3 整除的 key 都會(huì)被映射到 0、3、6 這三個(gè)哈希值。
如果輸入 key 恰好滿(mǎn)足這種等差數(shù)列的數(shù)據(jù)分布,那么哈希值就會(huì)出現(xiàn)聚堆,從而加重哈希沖突?,F(xiàn)在,假設(shè)將 modulus 替換為質(zhì)數(shù) 13 ,由于 key 和 modulus 之間不存在公約數(shù),輸出的哈希值的均勻性會(huì)明顯提升。
值得說(shuō)明的是,如果能夠保證 key 是隨機(jī)均勻分布的,那么選擇質(zhì)數(shù)或者合數(shù)作為模數(shù)都是可以的,它們都能輸出均勻分布的哈希值。而當(dāng) key 的分布存在某種周期性時(shí),對(duì)合數(shù)取模更容易出現(xiàn)聚集現(xiàn)象。
總而言之,我們通常選取質(zhì)數(shù)作為模數(shù),并且這個(gè)質(zhì)數(shù)最好足夠大,以盡可能消除周期性模式,提升哈希算法的穩(wěn)健性。
不難發(fā)現(xiàn),以上介紹的簡(jiǎn)單哈希算法都比較“脆弱”,遠(yuǎn)遠(yuǎn)沒(méi)有達(dá)到哈希算法的設(shè)計(jì)目標(biāo)。例如,由于加法和異或滿(mǎn)足交換律,因此加法哈希和異或哈希無(wú)法區(qū)分內(nèi)容相同但順序不同的字符串,這可能會(huì)加劇哈希沖突,并引起一些安全問(wèn)題。
在實(shí)際中,我們通常會(huì)用一些標(biāo)準(zhǔn)哈希算法,例如 MD5、SHA-1、SHA-2、SHA3 等。它們可以將任意長(zhǎng)度的輸入數(shù)據(jù)映射到恒定長(zhǎng)度的哈希值。
近一個(gè)世紀(jì)以來(lái),哈希算法處在不斷升級(jí)與優(yōu)化的過(guò)程中。一部分研究人員努力提升哈希算法的性能,另一部分研究人員和黑客則致力于尋找哈希算法的安全性問(wèn)題。表 6-2 展示了在實(shí)際應(yīng)用中常見(jiàn)的哈希算法。
表 6-2 常見(jiàn)的哈希算法
MD5 | SHA-1 | SHA-2 | SHA-3 | |
---|---|---|---|---|
推出時(shí)間 | 1992 | 1995 | 2002 | 2008 |
輸出長(zhǎng)度 | 128 bits | 160 bits | 256 / 512 bits | 224/256/384/512 bits |
哈希沖突 | 較多 | 較多 | 很少 | 很少 |
安全等級(jí) | 低,已被成功攻擊 | 低,已被成功攻擊 | 高 | 高 |
應(yīng)用 | 已被棄用,仍用于數(shù)據(jù)完整性檢查 | 已被棄用 | 加密貨幣交易驗(yàn)證、數(shù)字簽名等 | 可用于替代 SHA-2 |
我們知道,哈希表的 key 可以是整數(shù)、小數(shù)或字符串等數(shù)據(jù)類(lèi)型。編程語(yǔ)言通常會(huì)為這些數(shù)據(jù)類(lèi)型提供內(nèi)置的哈希算法,用于計(jì)算哈希表中的桶索引。以 Python 為例,我們可以調(diào)用 hash() 函數(shù)來(lái)計(jì)算各種數(shù)據(jù)類(lèi)型的哈希值。
Tip
請(qǐng)注意,不同編程語(yǔ)言的內(nèi)置哈希值計(jì)算函數(shù)的定義和方法不同。
built_in_hash.cpp
int num = 3;
size_t hashNum = hash<int>()(num);
// 整數(shù) 3 的哈希值為 3
bool bol = true;
size_t hashBol = hash<bool>()(bol);
// 布爾量 1 的哈希值為 1
double dec = 3.14159;
size_t hashDec = hash<double>()(dec);
// 小數(shù) 3.14159 的哈希值為 4614256650576692846
string str = "Hello 算法";
size_t hashStr = hash<string>()(str);
// 字符串 Hello 算法 的哈希值為 15466937326284535026
// 在 C++ 中,內(nèi)置 std:hash() 僅提供基本數(shù)據(jù)類(lèi)型的哈希值計(jì)算
// 數(shù)組、對(duì)象的哈希值計(jì)算需要自行實(shí)現(xiàn)
在許多編程語(yǔ)言中,只有不可變對(duì)象才可作為哈希表的 key 。假如我們將列表(動(dòng)態(tài)數(shù)組)作為 key ,當(dāng)列表的內(nèi)容發(fā)生變化時(shí),它的哈希值也隨之改變,我們就無(wú)法在哈希表中查詢(xún)到原先的 value 了。
雖然自定義對(duì)象(比如鏈表節(jié)點(diǎn))的成員變量是可變的,但它是可哈希的。這是因?yàn)閷?duì)象的哈希值通常是基于內(nèi)存地址生成的,即使對(duì)象的內(nèi)容發(fā)生了變化,但它的內(nèi)存地址不變,哈希值仍然是不變的。
細(xì)心的你可能發(fā)現(xiàn)在不同控制臺(tái)中運(yùn)行程序時(shí),輸出的哈希值是不同的。這是因?yàn)?Python 解釋器在每次啟動(dòng)時(shí),都會(huì)為字符串哈希函數(shù)加入一個(gè)隨機(jī)的鹽(Salt)值。這種做法可以有效防止 HashDoS 攻擊,提升哈希算法的安全性。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話(huà):173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: