散列表(hash table)
散列思想:散列表用的是數組支持按照下標隨機訪問數據的特性,所以散列表其實就是數組的一種擴展,由數組演化而來,如果沒有數組,就沒有散列表。
散列表用的就是數組支持按照下標隨機訪問的時候,時間複雜度爲O(1)的特性,通過散列函數把元素的鍵值映射爲下標,然後將數據存儲在數組中對應下標的位置,當我們按照鍵值查詢元素時,用同樣的散列函數,將鍵值轉化數組下標,從對應的數組下標的位置取數據。
散列函數僞代碼
int hash(String key) {
// 獲取後兩位字符
string lastTwoChars = key.substr(length-2, length);
// 將後兩位字符轉換爲整數
int hashValue = convert lastTwoChas to int-type;
return hashValue;
}
構造散列函數設計的基本要求
1.散列函數計算得到的散列值是一個非負整數
2.如果key1 = key2,那hash(key1) == hash(key2)
3.如果 key1≠key2,那hash(key1)≠hash(key2) (這種基本無論什麼算法,都無法避免散列衝突)
散列衝突
1.開放地址法
如果出現了散列衝突,就重新探測一個空閒位置,將其插入,重新探測新的位置採用的探測方法是線性探測。(當某個數據經過散列函數,存儲位置已經佔用,就從當前位置開始,一次往後尋找,看時候有空閒位置,直到找到爲止)
還有二次探測,所謂二次探測,跟線性探測很像,線性探測每次探測的步長是 1,那它探測的下標序列就是 hash(key)+0,hash(key)+1,hash(key)+2……而二次探測探測的步長就變成了原來的“二次方”,也就是說,它探測的下標序列就是 hash(key)+0,hash(key)+12,hash(key)+22……
所謂雙重散列,意思就是不僅要使用一個散列函數。我們使用一組散列函數 hash1(key),hash2(key),hash3(key)……我們先用第一個散列函數,如果計算得到的存儲位置已經被佔用,再用第二個散列函數,依次類推,直到找到空閒的存儲位置
我們用裝載因子來表示空位的多少
裝載因子計算公式:
散列表的裝載因子 = 填入表中的元素個數 / 散列表的長度
2.鏈表法(更加常用)
當插入的時候,我們只需要通過散列函數計算出對應的散列槽位,其插入到對應鏈表中即可,所以插入的時間複雜度是 O(1)。
如何設計散列函數
散列函數的設計不能太複雜,其次,散列函數生成的值要儘可能隨機並且隨機分佈,這樣才能避免或者最小化散列衝突。
裝載因子過大怎麼辦,裝載因子越大,說明散列表中的元素越多,空閒位置越少,散列衝突的概率越大,不僅插入數據的過程要多次尋址或者拉很長的鏈,查找的過程也會變得很慢。
針對散列表,當裝載因子過大時,可以進行動態擴容。當然,對空間消耗敏感,也可以在裝載因子小於某個值的時候,啓動動態縮容。
避免低效的擴容,可以將擴容操作穿插在插入操作的過程中,分批完成,對於查詢操作,爲了兼容新,老散列表中的數據,先從新散列表中查找,再去老的散列表中查找。
兩種使用場景
使用開放地址法的是數據量比較小,裝載因子小的時候,適合採用開放尋址法。這也是java中ThreadLocalMap使用開放尋址法解決散列衝突的原因
基於鏈表的散列衝突處理方法比較適合存儲大對象,大存儲量的散列表,而且,比起開放尋址法,更加靈活,支持更多優化策略,比如用紅黑樹代替鏈表。
HashMap就是一個工業級別的散列表
1.初始大小
默認初始大小爲16,這個默認值可以設置。
2.裝載因子和動態擴容
最大裝載因子默認0.75,當hashmap中元素個數超過0.75*capacity(容量)的時候,就會啓動擴容,每次擴容爲原來的兩倍大小
3.散列衝突的解決方法
採用鏈表法來解決衝突,1.8之後,當鏈表長度太長(超過8),鏈表轉爲紅黑樹,當小於8個的時候,轉爲鏈表。
4.散列函數
int hash(Object key) {
int h = key.hashCode();
return (h ^ (h >>> 16)) & (capitity -1); //capicity 表示散列表的大小
}
LRU緩存淘汰算法
一個緩存(cache)系統主要包含
1.往緩存中添加一個數據
2.從緩存中刪除一個數據
3.在緩存中查找一個數據
每個操作都設計“查找操作”,單純採用鏈表,時間複雜度只能是O(n),將散列表和鏈表兩種數據結構組合使用,可以將時間複雜度將爲O(1),
使用雙向鏈表存儲數據。
如何查找一個數據,通過散列表,在緩存中找到一個數據,找到數據之後,將他移動到雙向鏈表的尾部
如何刪除一個數據,找到數據所在的節點,然後將結點刪除。
如何添加一個數據,先看這個數據是否在緩存中,如果在,移動到雙向鏈表尾部,如果不在,就看緩存滿沒滿,如果滿了,則將雙向鏈表頭部的結點刪掉,然後將數據放到鏈表尾部,如果沒有,則直接放到尾部。
2.Redis有序集合
2.1.什麼是有序集合?
①在有序集合中,每個成員對象有2個重要的屬性,即key(鍵值)和score(分值)。
②不僅會通過score來查找數據,還會通過key來查找數據。
2.2.有序集合的操作有哪些?
舉個例子,比如用戶積分排行榜有這樣一個功能:可以通過用戶ID來查找積分信息,也可以通過積分區間來查找用戶ID。這裏用戶ID就是key,積分就是score。所以,有序集合的操作如下:
①添加一個對象;
②根據鍵值刪除一個對象;
③根據鍵值查找一個成員對象;
④根據分值區間查找數據,比如查找積分在[100.356]之間的成員對象;
⑤按照分值從小到大排序成員變量。
這時可以按照分值將成員對象組織成跳錶結構,按照鍵值構建一個散列表。那麼上面的所有操作都非常高效。
Java LinkedHashMap
LinkedHashMap也是通過散列表和鏈表組合在一起實現的,實際上,不僅支持按照插入順序遍歷數據, 還支持按照訪問順序來遍歷數據。
LinkedHashMap是通過雙向鏈表和散列表這兩種數據結構組合實現的,LinkedHashMap中的LInked實際上就是指的雙向鏈表,並非指用鏈表法解決散列衝突。