散列表知识点总结
最近在復習數據結構以及在上極客時間的網課,對散列表的只是進行部分總結:
1.當使用散列表進行存儲元素時,主要使用散列函數對鍵值進行計算得到存儲位置。基于“鴿巢原理”,我們不可避免地必須處理沖突的問題,解決沖突的方法一般有兩種:
? ? ?① 鏈表法:每個槽對應一條鏈表,當散列函數計算后會進入某個槽,每個槽有一個指向鏈表的頭指針,所以插入鏈表時直接插入頭部就行,因此插入的效率是O(1)。查找一個元素時,同樣先對其鍵值進行散列函數計算,然后進入對應的槽中查找,假設我們有n個元素,一共有m個槽。那么每個槽平均而言就有n/m個元素。查找的話就取決于這個m了,當槽的數目和元素的數組接近時,O(n/m+1)查找的效率就接近O(1)。刪除的話和查找和類似,主要就是那個刪除的操作。如果采用單鏈表存儲的話,找到待刪除元素時還得從鏈表頭找到待刪除元素的前一個元素。而如果采用雙鏈表的話效率會更高。刪除的效率也是接近O(1)。
? ? ? 缺點:最壞的情況就是所有的元素都進入了一個槽,那么在散列表中的查找就退化成了在鏈表中查找,效率也就成了O(n)。
? ? ? 優化方法:每個槽內的元素采用“紅黑樹”或者“跳表”這種數據結構存儲,這樣我們就能保證在最壞情況下依然能有基本的效率保證。假如我們用紅黑樹存儲的話,查找最壞只不過退化為O(logn)。
? ? ②開放尋址法:當發生沖突時,我們繼續往后探測,如果找到空閑位置就插入。如果沒找到就一直探測。那么使用開放尋址法處理沖突的散列表如何執行查找操作呢?首先通過散列函數計算得到的散列值那個位置開始查找,如果當前位置的值就等于待找值的話就直接返回,如果不等于就繼續往后探測,如果一直找到空閑位置還未找到待找值,則返回未找到。插入的過程與查找類似,如果發生沖突,就不斷繼續探測,直到遇到位置為空閑位置就插入。刪除的操作有些特別,我們不能簡單地就把元素置為空閑,因為我們查找是以遇到空閑位置結束的。如果我們在某個值之前的位置執行了刪除操作,那么我們重新查找該值得時候將會顯示查不到。因為在它之前遇到了空閑位置。所以刪除我們不能簡單將位置置為空。而是將其給一個特殊的標志位deleted,當查找遇到deleted的位置時不要停止繼續往后查找。
? ? 缺點:當插入元素越來越多時,散列表發生沖突的概率就會越來越大,空閑位置越來越少,線性探測時間也越來越久。極端情況下,我們可能需要探測整個散列表,所以最壞情況下的時間復雜度為O(n)。同理在刪除和查找時,也有可能會線性探測整張散列表,才能找到要查找或者刪除的數據。
? ?③其他還有二次探測法和雙重散列法? 二次探測是每次以02、12、22。。。。步進行探測。雙重散列是使用多個散列函數,如果一個散列函數計算出來的位置被占了,就接著使用第二個。。以此類推直到找到空閑位置。
2.散列函數設計基本要求
? ?① 散列函數計算得到的散列值是一個非負整數
? ?② 如果key1==key2,那么hash(key1)==hash(key2);
? ?③? 如果 key1!=key2,那么hash(key1)!=hash(key2);
? 第三點要求不一定能滿足,沒有完美的散列函數能做到,既然避免不了沖突,我們就是用那兩種解決沖突的方法。
? ④ 散列函數生成的值要盡可能均勻分布在散列表中。
? ⑤ 根據關鍵字的長度、特點、分布、還有散列表的大小等綜合設計
? ⑥散列函數不能太復雜,否則計算時間太長,影響散列表的效率。
習題:假設我們有10萬條URL訪問日志,如何按照訪問次數給URL排序?
? ? ? ? 答:先將這10萬條URL日志放入散列表,key為URL,vlaue為訪問次數。同時記錄下訪問次數的最大值K,時間復雜度為O(n),如果K不是很大的話我們可以采用計數排序,時間復雜度為O(n)。如果k很大的話就采用快速排序,時間復雜度為O(nlogn)。
? ? ? ? ?有兩個字符串數組,每個數組大約有10萬條字符串,如何快速查找兩個數組中相同的字符串?
? ? ? ? 答:先對一個數組遍歷構建散列表,key為字符串,value為訪問次數。然后遍歷另一個數組,以字符串為key在散列表中查找,如果訪問次數大于0,說明存在相同字符串。時間復雜度為O(N)。
? 3.裝載因子過大了怎么辦?
? 答:我們知道隨著裝載因子不斷變大,散列表的效率會越來越差。使用開放尋址法解決沖突的散列表為了找到空閑位置需要不停探測,而使用鏈表法解決沖突的散列表隨著裝填因子的變大,在每個槽里存儲的對象數目也越來越多。查找效率也減少了好多。所以我們一般要在散列表的裝填因子達到某個閾值時,進行動態擴容,與Vector的擴容相類似,都是將原來的元素搬到新的內存中去,這段新的內存可能是原內存的兩倍。這樣裝填因子就成了原來的一半。與數組的擴容不同的是,數組只需要簡單的搬運元素就行,而散列表的擴容由于散列表的大小發生了改變,需要對所有元素進行重新散列。復雜度的分析也與數組擴容類似,正常插入的效率一般是O(1),在最壞情況下需要進行擴容,重新計算散列位置。那么效率成了O(n)。采用均攤分析的話,時間復雜度接近O(1)。如果我們對于空間消耗較為敏感的話,既然有動態擴容我們也可以動態縮容,當裝填因子小于某個值時,我們開啟縮容操作。
?4.裝填因子閾值的設置
? 答:要綜合權衡時間、空間復雜度、如果內存不緊張、對執行效率要求很高的話,可以降低裝填因子的閾值;相反如果內存空間緊張,對執行效率要求不高,可以增加裝填因子閾值。
? 開放尋址法裝填因子不能大于1? ? ?鏈表法可以大于1
5.如何避免低效擴容?
? ?答:當我們的用戶要求較高時,不允許出現較為明顯卡頓時。采用一次性擴容的政策就顯得有些局限。盡管大多數操作都能很快執行,但是偶爾的一次插入會特別慢,用戶體驗就會下降。所以我們可以將擴容操作穿插在插入操作的過程中,分批完成。當裝填因子達到閾值時,我們只申請新空間,不搬運元素。當有新元素插入時,將其將入到新的散列表中,同時從老的散列表中拿出一個元素放入新的散列表中。每一次插入操作都這樣重復。經過多次操作老的散列表所有元素就都搬到了新的散列表中去,這樣每次插入操作都會很快了。采用了新的搬運操作的話,查找方法就要發生改變,當我們要查找一個元素時,我們先在新的散列表中查找,沒找到再去舊的散列表中找。避免了一次性擴容耗時過久的代價。在新的實現方式下,所有的插入操作都有了O(1)的效率。
6.比較開放尋址法和鏈表法優缺點
? 開放尋址法? 優點:散列表的數據都存放在數組中,可以有效利用CPU的緩存加快查詢速度。而且香斷鏈表法它不需要很多指針,也避免了不必要的開銷。
缺點:刪除操作比較麻煩,相對于鏈表法,所有的數據都存儲在一個數組中,沖突的代價更高。所以采用開放尋址法的散列表不能有太高的裝填因子,也使得其比鏈表法更浪費內存空間。
? ?總結:當數據量小,裝填因子小的時候采用開放尋址法。
? 鏈表法? 優點:對內存的利用率比開放尋址法更高,因為鏈表結點可以在需要的時候再進行創建,并不需要像開放尋址法那樣事先申請好。鏈表法的裝填因子可以很大,最多就是鏈表的長度變長而已,效率會下降,但是相比順序查找還是很快。
? 鏈表由于不是順序存儲所以無法使用CPU緩存,而且鏈表的每個結點都需要有一個指針的消耗。當然如果我們存儲的是大對象的話這一點消耗完全是可以忽略的。
? ? 總結:鏈表法比較適合大對象、大數據量的散列表,而且比起開放尋址法,它更加靈活,支持更多的優化策略,比如紅黑樹代替鏈表。
7.工業化的散列表實例
? ?Java的HashMap的默認大小為16,默認裝填因子為0.75。當每個槽的鏈表長度大于8時采用紅黑樹存儲,小于6時采用鏈表,因為當數據量較小時,紅黑樹要維持平衡,比起鏈表性能并無太大優勢。
? C++ STL 中的hash_map 也有動態擴容,但是并沒有采用紅黑樹,也沒有退化采用的鏈表。直接采用就是鏈表法。
?
總結
- 上一篇: 安卓平台下的GPS架构介绍及驱动移植记录
- 下一篇: 西风显卡全面上架京东自营,RTX 406