18 | 散列表(上):Word文档中的单词拼写检查功能是如何实现的?
問(wèn)題引入
在 Word 里輸入一個(gè)錯(cuò)誤的英文單詞,它就會(huì)用標(biāo)紅的方式提示“拼寫(xiě)錯(cuò)誤”,Word 文本編輯器的拼寫(xiě)檢查功能是如何實(shí)現(xiàn)的呢?散列表(Hash Table)
散列表
散列表定義:散列表的英文叫“Hash Table”,也稱為 “哈希表”或者“Hash 表”。散列表用的是數(shù)組支持按照下標(biāo)隨機(jī)訪問(wèn)數(shù)據(jù)的特性,散列表其實(shí)就是數(shù)組的一種擴(kuò)展,由數(shù)組演化而來(lái)。數(shù)組是散列表的基礎(chǔ)。
散列思想
場(chǎng)景:假如有 89 名選手參加學(xué)校運(yùn)動(dòng)會(huì)。為了方便記錄成績(jī),每個(gè)選手胸前都會(huì)貼上自己的參賽號(hào)碼。這 89 名選手的編號(hào)依次是 1 到 89。現(xiàn)在我們希望編程實(shí)現(xiàn)這樣一個(gè)功能,通過(guò)編號(hào)快速找到對(duì)應(yīng)的選手信息。怎么做?
?
方案:把?89 名選手的信息放在數(shù)組里。編號(hào)為 1 的選手,我們放到數(shù)組中下標(biāo)為 1 的位置;編號(hào)為 k 的選手放到數(shù)組中下標(biāo)為 k 的位置。因?yàn)閰①惥幪?hào)跟數(shù)組下標(biāo)一一對(duì)應(yīng),當(dāng)我們需要查詢參賽編號(hào)為 x 的選手的時(shí)候,我們只需要將下標(biāo)為 x 的數(shù)組元素取出來(lái)就可以了,時(shí)間復(fù)雜度就是 O(1)。
這樣按照編號(hào)查找選手信息已經(jīng)用到了散列思想。參賽編號(hào)是自然數(shù),并且與數(shù)組的下標(biāo)形成一一映射,利用數(shù)組支持根據(jù)下標(biāo)隨機(jī)訪問(wèn)的時(shí)候,時(shí)間復(fù)雜度是 O(1) 這一特性,就可以實(shí)現(xiàn)快速查找編號(hào)對(duì)應(yīng)的選手信息
新場(chǎng)景:假設(shè)參賽編號(hào)不能設(shè)置得這么簡(jiǎn)單,要加上年級(jí)、班級(jí)這些更詳細(xì)的信息用 6 位數(shù)字來(lái)表示。比如 051167前兩位 05 表示年級(jí),中間兩位 11 表示班級(jí),最后兩位還是原來(lái)的編號(hào) 1 到 89。這個(gè)時(shí)候如何存儲(chǔ)選手信息,能夠支持通過(guò)編號(hào)來(lái)快速查找選手信息?
新方案:不能直接把編號(hào)作為數(shù)組下標(biāo),但可以截取參賽編號(hào)的后兩位作為數(shù)組下標(biāo),來(lái)存取選手信息數(shù)據(jù)。當(dāng)通過(guò)參賽編號(hào)查詢選手信息用同樣的方法,取參賽編號(hào)的后兩位,作為數(shù)組下標(biāo),來(lái)讀取數(shù)組中的數(shù)據(jù)。
典型的散列思想:參賽選手的編號(hào)我們叫做鍵(key)或者關(guān)鍵字。我們用它來(lái)標(biāo)識(shí)一個(gè)選手。我們把參賽編號(hào)轉(zhuǎn)化為數(shù)組下標(biāo)的映射方法就叫作散列函數(shù)(或“Hash 函數(shù)”“哈希函數(shù)”),而散列函數(shù)計(jì)算得到的值就叫做散列值。
總結(jié):散列表依靠數(shù)組支持按照下標(biāo)隨機(jī)訪問(wèn)的時(shí)間復(fù)雜度是 O(1) 的特性。通過(guò)散列函數(shù)把元素的鍵值映射為下標(biāo),然后將數(shù)據(jù)存儲(chǔ)在數(shù)組中對(duì)應(yīng)下標(biāo)的位置。當(dāng)按照鍵值查詢?cè)貢r(shí),用同樣的散列函數(shù),將鍵值轉(zhuǎn)化數(shù)組下標(biāo),從對(duì)應(yīng)的數(shù)組下標(biāo)的位置取數(shù)據(jù)。
散列函數(shù)
散列函數(shù)在散列表中起著非常關(guān)鍵的作用。散列函數(shù)可以定義為hash(key),其中 key 表示元素的鍵值,hash(key) 的值表示經(jīng)過(guò)散列函數(shù)計(jì)算得到的散列值。
前面的例子散列函數(shù)偽代碼:
int hash(String key) {// 獲取后兩位字符string lastTwoChars = key.substr(length-2, length);// 將后兩位字符轉(zhuǎn)換為整數(shù)int hashValue = convert lastTwoChas to int-type;return hashValue; }如何構(gòu)造散列函數(shù)?
三點(diǎn)散列函數(shù)設(shè)計(jì)的基本要求:
第三點(diǎn)要求看起來(lái)合情合理,但是在真實(shí)的情況下,要想找到一個(gè)不同的 key 對(duì)應(yīng)的散列值都不一樣的散列函數(shù),幾乎是不可能的。即便像業(yè)界著名的MD5、SHA、CRC等哈希算法,也無(wú)法完全避免這種散列沖突。因?yàn)閿?shù)組的存儲(chǔ)空間有限,也會(huì)加大散列沖突的概率。針對(duì)散列沖突問(wèn)題,需要通過(guò)其他途徑來(lái)解決。
散列沖突
常用的散列沖突解決方法有兩類,開(kāi)放尋址法(open addressing)和鏈表法(chaining)
1、開(kāi)放尋址法
開(kāi)放尋址法的核心思想是,如果出現(xiàn)了散列沖突,我們就重新探測(cè)一個(gè)空閑位置,將其插入。那如何重新探測(cè)新位置?個(gè)比較簡(jiǎn)單的探測(cè)方法,線性探測(cè)(Linear Probing)。當(dāng)我們往散列表中插入數(shù)據(jù)時(shí),如果某個(gè)數(shù)據(jù)經(jīng)過(guò)散列函數(shù)散列之后,存儲(chǔ)位置已經(jīng)被占用了,我們就從當(dāng)前位置開(kāi)始,依次往后查找,看是否有空閑位置,直到找到為止。
查找:在散列表中查找元素的過(guò)程有點(diǎn)兒類似插入過(guò)程。通過(guò)散列函數(shù)求出要查找元素的鍵值對(duì)應(yīng)的散列值,然后比較數(shù)組中下標(biāo)為散列值的元素和要查找的元素。如果相等,則說(shuō)明就是我們要找的元素;否則就順序往后依次查找。如果遍歷到數(shù)組中的空閑位置,還沒(méi)有找到,就說(shuō)明要查找的元素并沒(méi)有在散列表中
刪除:查找操作在查找的時(shí)候,一旦我們通過(guò)線性探測(cè)方法,找到一個(gè)空閑位置,我們就可以認(rèn)定散列表中不存在這個(gè)數(shù)據(jù)。但是,如果這個(gè)空閑位置是我們后來(lái)刪除的,就會(huì)導(dǎo)致原來(lái)的查找算法失效。本來(lái)存在的數(shù)據(jù),會(huì)被認(rèn)定為不存在。這個(gè)問(wèn)題如何解決呢?我們可以將刪除的元素,特殊標(biāo)記為 deleted。當(dāng)線性探測(cè)查找的時(shí)候,遇到標(biāo)記為 deleted 的空間,并不是停下來(lái),而是繼續(xù)往下探測(cè)。
當(dāng)散列表中插入的數(shù)據(jù)越來(lái)越多時(shí),散列沖突發(fā)生的可能性就會(huì)越來(lái)越大,空閑位置會(huì)越來(lái)越少,線性探測(cè)的時(shí)間就會(huì)越來(lái)越久。極端情況下,我們可能需要探測(cè)整個(gè)散列表,所以最壞情況下的時(shí)間復(fù)雜度為 O(n)
兩種比較經(jīng)典的探測(cè)方法:二次探測(cè)(Quadratic probing)和雙重散列(Double hashing)。
二次探測(cè):線性探測(cè)每次探測(cè)的步長(zhǎng)是 1,那它探測(cè)的下標(biāo)序列就是 hash(key)+0,hash(key)+1,hash(key)+2……而二次探測(cè)探測(cè)的步長(zhǎng)就變成了原來(lái)的“二次方”,也就是說(shuō),它探測(cè)的下標(biāo)序列就是 hash(key)+0,hash(key)+12,hash(key)+22……
雙重散列:不僅要使用一個(gè)散列函數(shù)。使用一組散列函數(shù) hash1(key),hash2(key),hash3(key)……先用第一個(gè)散列函數(shù),如果計(jì)算得到的存儲(chǔ)位置已經(jīng)被占用,再用第二個(gè)散列函數(shù),依次類推,直到找到空閑的存儲(chǔ)位置。
當(dāng)散列表中空閑位置不多的時(shí)候,散列沖突的概率就會(huì)大大提高。為了盡可能保證散列表的操作效率,一般情況下,我們會(huì)盡可能保證散列表中有一定比例的空閑槽位。用裝載因子(load factor)來(lái)表示空位的多少。裝載因子的計(jì)算公式是:
散列表的裝載因子=填入表中的元素個(gè)數(shù)/散列表的長(zhǎng)度裝載因子越大,說(shuō)明空閑位置越少,沖突越多,散列表的性能會(huì)下降。
2、鏈表法
鏈表法是一種更加常用的散列沖突解決辦法,相比開(kāi)放尋址法,它要簡(jiǎn)單很多。如下圖在散列表中,每個(gè)“桶(bucket)”或者“槽(slot)”會(huì)對(duì)應(yīng)一條鏈表,所有散列值相同的元素我們都放到相同槽位對(duì)應(yīng)的鏈表中。
- 當(dāng)插入的時(shí)候,通過(guò)散列函數(shù)計(jì)算出對(duì)應(yīng)的散列槽位,將其插入到對(duì)應(yīng)鏈表中即可,插入的時(shí)間復(fù)雜度是 O(1)。
- 當(dāng)查找、刪除一個(gè)元素時(shí),同樣通過(guò)散列函數(shù)計(jì)算出對(duì)應(yīng)的槽,然后遍歷鏈表查找或者刪除。那查找或刪除操作的時(shí)間復(fù)雜度是多少呢?實(shí)際上,這兩個(gè)操作的時(shí)間復(fù)雜度跟鏈表的長(zhǎng)度 k 成正比,也就是 O(k)。對(duì)于散列比較均勻的散列函數(shù)來(lái)說(shuō),理論上講,k=n/m,其中 n 表示散列中數(shù)據(jù)的個(gè)數(shù),m 表示散列表中“槽”的個(gè)數(shù)。
解答開(kāi)篇
Word 文檔中單詞拼寫(xiě)檢查功能是如何實(shí)現(xiàn)的?常用的英文單詞有 20 萬(wàn)個(gè)左右,假設(shè)單詞的平均長(zhǎng)度是 10 個(gè)字母,平均一個(gè)單詞占用 10 個(gè)字節(jié)的內(nèi)存空間,那 20 萬(wàn)英文單詞大約占 2MB 的存儲(chǔ)空間,就算放大 10 倍也就是 20MB。對(duì)于現(xiàn)在的計(jì)算機(jī)來(lái)說(shuō),這個(gè)大小完全可以放在內(nèi)存里面??梢杂蒙⒘斜韥?lái)存儲(chǔ)整個(gè)英文單詞詞典。當(dāng)用戶輸入某個(gè)英文單詞時(shí),我們拿用戶輸入的單詞去散列表中查找。如果查到,則說(shuō)明拼寫(xiě)正確;如果沒(méi)有查到,則說(shuō)明拼寫(xiě)可能有誤,給予提示。借助散列表這種數(shù)據(jù)結(jié)構(gòu),我們就可以輕松實(shí)現(xiàn)快速判斷是否存在拼寫(xiě)錯(cuò)誤。
總結(jié)
以上是生活随笔為你收集整理的18 | 散列表(上):Word文档中的单词拼写检查功能是如何实现的?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Jenkins教程:使用Jenkins进
- 下一篇: 28 | 堆和堆排序:为什么说堆排序没有