《資料結構與演算法之美》專欄閱讀筆記5——散列表和雜湊函式
這應該是看完最呆(沒有想到的那種呆~)的一個小章節了,給作者鼓掌,講的好好。果然抽象能力才是王道
文章目錄
1、散列表
核心:散列表用的是陣列支援按照下標隨機訪問資料的特性。
這個例子舉的好好~不抄了,粘原文,重點是下面的亮條條(廣告
跟著學了幾個排序演算法後,此時此刻看到散列表想到的是計數排序呢,因為都是想著法兒地給元素和陣列下標搞關係。
1.1、小概念
- 鍵:也叫關鍵字,就是最終放到資料結構中的元素啦
- 雜湊函式:也叫雜湊函式。算命先生,告訴鍵應該去陣列的哪個坑裡蹲著
- 雜湊值:也叫雜湊值。鍵蹲的那個坑
1.2、雜湊函式
如果把元素都對應到了陣列中,查詢的時間複雜度就是O(1),看著很酷呢~
雜湊函式需要滿足三點基本要求:
- 雜湊函式計算得到的雜湊值是一個非負整數
- 如果key1 = key2,那麼hash(key1) = hash(key2)
- 如果key1 != key2,那麼hash(key1) != hash(key2)
(補充一條:簡單不燒腦更好)
1.3、雜湊衝突
實際中,比較難滿足第三點要求,當存在key1 != key2,hash(key1) != hash(key2)時稱作雜湊衝突。解決雜湊衝突常見的兩種辦法:
- 開放定址法
思路:出現衝突就重新探測空閒可用的位置來儲存資料。一種簡單的線性探測方法如下。
刪除操作:因為使用開放定址法的時候,key相同的資料儲存在同一個位置(但是我們不曉得是有幾個相同的資料儲存在這一份資料中),所以刪除的時候不能直接刪,而是標記位deleted,避免被定址覆蓋了。(會有很多的空間浪費吧~不環保,差評!)
【效能分析】因為算完還需要找合適的位置,最壞的情況可能需要挨個兒找一遍,O(n)啦
【更好的辦法】
二次探測:探測步長變成n^2。
雙重雜湊:使用一組雜湊函式,挨個算,知道有一個函式算出來沒被佔用的位置為止(這組雜湊函式應該很不容易吧)
- 連結串列法
思路:將雜湊值相同的元素用連結串列存起來,數組裡儲存的是這個連結串列的頭的資訊。
1.4、裝載因子
裝載因子可以用來表示陣列中空位的多少,裝在因子越大,說明空閒位置越少,衝突越多,散列表效能會下降。
load factor = 填入表中的元素的個數 / 散列表長度
2、實際應用中的散列表注意事項
2.1、雜湊函式的設計原則
- 不能太複雜,避免消耗太多的計算時間
- 生成的雜湊值要儘可能隨機並且均勻分佈
2.2、裝載因子過大
動態擴容。
散列表的擴容需要重新計算雜湊位置,搬移資料。裝載因子特別小時,如果對空間消耗敏感,還可以動態縮容。
2.3、如何避免低效地擴容
避免一次性擴容,將新資料插入新的散列表的過程中搬移舊資料到新的散列表
2.4、解決衝突的方案選擇
【開放定址法】
- 優點
資料儲存在陣列中,可以利用CPU快取加快查詢速度。
序列化相對簡單 - 缺點
刪除資料比較麻煩,更浪費記憶體空間。 - 適用場景
資料量比較小,裝載因子小
【連結串列法】
- 優點
記憶體利用率高
對大裝載因子的容忍度更高 - 缺點
消耗記憶體
非連續儲存,對CPU快取不友好。可以通過使用其他資料結構來替代連結串列優化效率
- 適用場景
儲存大物件、大資料量的散列表
2.5、設計一個工業級的雜湊函式
像這種要考慮很多方面的問題,大致都只能給個方針啥的,作者給的,我抄過來啦~遇到的時候還能回來翻翻看:
一個工業級的散列表需要滿足以下要求:
- 支援快速的查詢、插入、刪除操作
- 記憶體佔用合理,不能浪費過多的記憶體空間
- 效能穩定,極端情況下,散列表的效能也不會退化到無法接受的情況
如何實現:
- 設計一個合適的雜湊函式
- 定義裝載因子閾值,並且設計動態擴容策略
- 選擇合適的雜湊衝突解決辦法
舉栗子的時候給了個HashMap的雜湊值計算的方法,看完頭皮發麻。評論區有小牛角給了分析呢,好好看完,就抄過來啦
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
找了一下位運算技巧:面試常用位運算技巧
位運算的這些沒有找到可以總結的辦法,所以記不住啦~回頭找機會再瞄一瞄吧。
**TODO:**LRU實現
3、雜湊演算法
原理:將任意長度的二進位制值串對映為固定長度的二進位制值串的規則。
一個優秀的雜湊演算法要滿足的幾點要求:
- 從雜湊值不能反向推導處原始資料
- 對輸入資料非常敏感,哪怕原始資料只修改了一個Bit,雜湊值也大不相同
- 雜湊衝突概率很小
- 執行效率高
應用
- 安全加密
- 唯一標識
- 資料校驗
- 雜湊函式
- 負載均衡
- 資料分片
- 分散式儲存
一致性雜湊