Word這種文本編輯器你平時應該常常見吧,那你有沒有留意過它的拼寫檢查功能呢?一旦咱們在Word裏輸入一個個錯誤的英文單詞,它就會用標紅的放式提示「拼寫錯誤」。java
Word的這個單詞拼寫檢查功能,雖然很小但卻很是實用。你有沒有想過,這個功能是如何實現的呢?
其實啊,一點兒都不難。只要你學完今天的內容,散列表(Hash Table)。你就能像微軟Office的工程師同樣,輕鬆實現這個功能。算法
散列表用的是數組支持按照下標隨機訪問數據的特性,因此三列表其實就是數組的一種擴展,因爲數組演化而來,能夠說、若是沒有數組、就沒有散列表數組
按照編號查找選手信息,效率是否是很高 時間複雜度就是 O(1)數據結構
參賽編號(6位數) 年級(前2位)+班級(中間2位)+編號(最後2位)編輯器
截取後兩位做爲數組下標來存取選手信息 取參賽編號的後兩位,做爲數組下標,來讀取數組中的數據。函數
這就是典型的散列思想:性能
其中、參賽選手的編號咱們叫做鍵(key)或者關鍵字,咱們用它來表示一個選手、spa
咱們把參賽編號轉化爲數組下標的映射方法就叫作散列函數(或Hash函數 哈希函數)、而散列函數計算獲得的值就叫做散列值設計
咱們能夠總結出這樣的規律:散列表用的就是數組支持按照下標隨機訪問時候、時間複雜度是O(1) 的特性。咱們經過散列函數把元素的鍵值映射爲下標,而後將數據存儲在數組中對應小標的位blog
置。當咱們按照鍵值查詢元素時,咱們用一樣的散列函數,將鍵值轉化數組下標、從對應的數組下標的位置數據
散列函數、顧明思義、它是一個函數、咱們能夠把它定義成hash(key),其中key表示元素的鍵值、hash(key)的
值表示通過散列函數計算獲得的散列值
那第一個例子中、編號就是數組下標、因此hash(key)就等於key、改造後的例子,寫成散列函數以下
int hash(String key) { // 獲取後兩位字符 string lastTwoChars = key.substr(length-2, length); // 將後兩位字符轉換爲整數 int hashValue = convert lastTwoChas to int-type; return hashValue; }
若是參賽選手的編號是隨機生成的6位數字、又或者用的是a到z之間的字符串,該如何構造散列函數呢?我總結了三點散列函數設計的基本要求
一、散列函數計算的到的散列值是一個非負整數
二、若是key1 = key2,那 hash(key1) == hash(key2);
三、若是key1 ≠ key2,那 hash(key1) ≠ hash(key2);
第三點理解起來可能會有問題,我着重說一下。這個要求看起來合情合理、可是在真實的狀況下、要想找到一個不一樣的key對應的散列值都不同的三類函數、幾乎是不可能的。即使像業界著名的MD五、SHA、CRC等哈希算法,也沒法徹底避免這種散列衝突、並且、由於數組的存儲空間有限、也會加大散列衝突的機率
因此咱們幾乎沒法找到一個完美的無衝突的散列函數、即使能找到、付出的時間成本、計算成本也是很大的、因此針對散列衝突問題、咱們須要經過其餘途徑來解決
若是出現了散列衝突、咱們就從新探測一個空閒位置、將其插入、那如何從新探測新的位置呢?
當某個數據通過散列函數散列滯後、存儲位置已經被佔用了、咱們就從當前位置開始、依次日後查找,看是否有空閒位置,直到找到爲止
黃色的色塊表示空閒位置、橙色的色塊表示已經存儲了數據
一、從圖中能夠看出、散列表的大小爲十、在元素x插入散列表以前,已經6個元素插入到散列表中
二、x通過hash算法以後,被三列到位置下標爲7的位置、可是這個位置已經有數據了、因此就產生了衝突,
三、因而咱們就順序地日後一個一個找,看有沒有空閒的位置、遍歷到尾部都沒有找到空閒的位置
四、因而咱們再從表頭開始找,直到找到空閒位置2,因而將其插入到這個位置
散列函數求出要查找元素的鍵值對應的散列值
而後比較數組中下標爲散列值的元素和要查找的元素若是相等、則說明就是咱們要找的元素
不然就數序日後一次查找、若是遍歷數組中的空閒位置,尚未找到,就說明要查找的元素並無在列表中
找到一個空閒位置,咱們就能夠認定散列表中不存在這個數據。
可是,若是這個空閒位置是咱們後來刪除的,就會致使原來的查找算法失效。原本存在的數據,會被認定爲不存在。這個問題如何解決呢?
咱們能夠將刪除的元素、特殊標記爲deleted,當線性探測的時候、遇到標記爲deleted的空間、並非停下來、而是繼續往下探測
最壞狀況下的時間複雜度爲 O(n)
一、在刪除和查找時,也有可能會線性探測整張散列表,才能找到要查找或者刪除的數據
二、對於開放尋址的衝突解決方法、除了線性探測方法以外,還有另外兩種比較經典的探測方法二次探測和雙重散列
線性探測 步長是 1 hash(key)+0,hash(key)+1,hash(key)+2...
二次探測 步長是 1 hash(key)+0,hash(key)+12,hash(key)+22
雙重散列 使用一組散列函數 hash1(key),hash2(key),hash3(key)……若是第一個散列函數計算機獲得的存儲位置已被佔用在用第二個散列函數、依次類推、直到找到空閒的存儲位置
無論採用哪一種探測方法,當散列表中的空閒位置很少的時候、散列衝突的機率就會大大提升、爲了儘量保證散列表的操做效率,通常狀況下,咱們會盡量保證散列表中有必定比例的空閒槽位、
咱們用裝載引子(load factor)來表示空位的多少
裝載因子的計算機公式是:
散列表的裝載因子 = 填入表中的元素個數 / 散列表的長度
裝載因子越大,說明空閒位置越少,衝突越多,散列表的性能會降低
鏈表是一種更加經常使用的散列衝突解決辦法、相比開放尋址法,它要簡單不少、咱們來看這個圖,在散列表中,每一個桶或者槽
會對應一條鏈表,全部散列值相同的元素咱們都放到相同槽位對應的鏈表中
當插入的時候,咱們只須要經過散列函數計算出對應的散列槽位,將其插入到對應鏈表中便可,插入時間複雜度是O(1)
當查找、刪除一個元素時、咱們一樣經過散列函數計算出對應的槽位、而後遍歷鏈表查找或者刪除、那查找或刪除操做的時間複雜度是多少呢?
實際上、這兩個操做的時間複雜度跟鏈表的長度K成正比,也就是O(k)對於散列比較均勻的散列函數來講理論上講k=n/m,其中 n 表示散列中數據的個數,m 表示散列表中「槽」的個數。
鏈表是一種更加經常使用的散列衝突解決辦法、相比開放尋址法,它要簡單不少、咱們來看這個圖,在散列表中,每一個桶或者槽
會對應一條鏈表,全部散列值相同的元素咱們都放到相同槽位對應的鏈表中
有了前面這些基本知識儲備,咱們來看一下開篇的思考題:Word中檔中單詞拼寫檢查功能是如何實現的?
經常使用的英語單詞有20萬個左右,假設單詞的平均速度是10個字母,平均一個單詞佔用10個字節的內存空間,那20萬英個單詞大約佔2MB的存儲空間,就算放大10倍也就是20MB。
對於如今的計算機來講,這個如今徹底能夠放在內存裏面。因此咱們能夠用散列表來存儲整個英文單詞詞典。
當用戶輸入某個英文單詞時,咱們拿用戶輸入的單詞去散列表中查找。若是查到,則說明拼寫正確;若是沒有查到,則說明拼寫可能有誤,給予提示。
藉助散列表這種數據結構,咱們就能夠輕鬆實現快速判斷是否存在拼寫錯誤。