日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

数据结构☞散列表

發布時間:2024/8/1 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 数据结构☞散列表 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

散列表(Hash Table)

散列表的英文叫“Hash Table”,我們平時也叫它“哈希表”或者“Hash 表”。
散列表用的是數組支持按照下標隨機訪問數據的特性,所以散列表其實就是數組的一種擴展,由數組演化而來。可以說,如果沒有數組,就沒有散列表。

  • key:鍵或者關鍵字。
  • 散列函數(或“Hash 函數”“哈希函數”):把key值轉化為數組下標的映射方法。
  • 散列函數計算得到的值就叫作散列值(或“Hash 值”“哈希值”)。

散列函數

散列函數設計的基本要求:

  • 散列函數計算得到的散列值是一個非負整數;
  • 如果 key1 = key2,那 hash(key1) == hash(key2);
  • 如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。(往往這個條件很難辦到,key不同可能出現相同的散列值,于是出現散列沖突

解決散列沖突

1. 開放尋址法

(1)線性探測(Linear Probing)(最好O(1);最壞情況下的時間復雜度為 O(n))
  • 插入:如果某個數據經過散列函數散列之后,存儲位置已經被占用了,我們就從當前位置開始,依次往后查找,看是否有空閑位置,直到找到為止。
  • 查找:過程和插入一樣,找到對應數組下標后,對比x與數組中存儲的值是否相等,若不等則依次往后查找…
  • 刪除:刪除的元素,特殊標記為 deleted。當線性探測查找的時候,遇到標記為 deleted 的空間,并不是停下來,而是繼續往下探測。
(2)二次探測(Quadratic probing)

和線性探測(Linear Probing)類似,只不過每次步長為沖突下標的平方(i^2)。

(3)雙重散列(Double hashing)

所謂雙重散列,意思就是不僅要使用一個散列函數。我們使用一組散列函數 hash1(key),hash2(key),hash3(key)……我們先用第一個散列函數,如果計算得到的存儲位置已經被占用,再用第二個散列函數,依次類推,直到找到空閑的存儲位置。

2. 鏈表法(HashMap和Hashtable就是這樣存儲的嘛)

散列表中,每個“桶(bucket)”或者“槽(slot)”會對應一條鏈表,所有散列值相同的元素我們都放到相同槽位對應的鏈表中:

裝載因子(現在知道為什么HashMap和Hashtable有擴容因子0.75了吧!)

當散列表中數組空閑位置不多的時候,散列沖突的概率就會大大提高。

裝載因子的計算公式是:散列表的裝載因子=填入數組中的元素個數/數組長度

裝載因子越大,說明空閑位置越少,沖突越多,散列表的性能會下降。

思考題

1. 假設我們有 10 萬條 URL 訪問日志,如何按照訪問次數給 URL 排序?

遍歷 10 萬條數據,以 URL 為 key,訪問次數為 value,存入散列表,同時記錄下訪問次數的最大值 K,時間復雜度 O(N)。

如果 K 不是很大,可以使用桶排序,時間復雜度 O(N)。如果 K 非常大(比如大于 10 萬),就使用快速排序,復雜度 O(NlogN)。

2. 有兩個字符串數組,每個數組大約有 10 萬條字符串,如何快速找出兩個數組中相同的字符串?

以第一個字符串數組構建散列表,key 為字符串,value 為出現次數。再遍歷第二個字符串數組,以字符串為 key 在散列表中查找,如果 value 大于零,說明存在相同字符串。時間復雜度 O(N)。
(其實就用 Set思想就可以了, 第一個String數組 放到 set中(其實就是個散列表, key 是 String, value 無所謂) 然后 拿散列表和 第二個String數組中元素contains() 有就存在 沒有就不存在)

設計散列表

一、散列函數怎么設計?

  • **散列函數的設計不能太復雜。**過于復雜的散列函數,勢必會消耗很多計算時間,也就間接地影響到散列表的性能。
  • 散列函數生成的值要盡可能隨機并且均勻分布,這樣才能避免或者最小化散列沖突,而且即便出現沖突,散列到每個槽里的數據也會比較平均,不會出現某個槽內數據特別多的情況。
  • 數據分析法:
    處理手機號碼,因為手機號碼前幾位重復的可能性很大,但是后面幾位就比較隨機,(因為隨機,所以會分布比較均勻)我們可以取手機號的后四位作為散列值。這種散列函數的設計方法,我們一般叫做“數據分析法”。

    二、裝載因子過大了怎么辦?

    為避免低效擴容:

  • 當裝載因子觸達閾值之后,我們只申請新空間,但并不將老的數據搬移到新散列表中。
  • 當有新數據要插入時,我們將新數據插入新散列表中,并且從老的散列表中拿出一個數據放入到新散列表。每次插入一個數據到散列表,我們都重復上面的過程。經過多次插入操作之后,老的散列表中的數據就一點一點全部搬移到新散列表中了。
  • 這期間的查詢操作怎么來做呢?對于查詢操作,為了兼容了新、老散列表中的數據,我們先從新散列表中查找,如果沒有找到,再去老的散列表中查找。

  • 將一次性擴容的代價,均攤到多次插入操作中:任何情況下,插入一個數據的時間復雜度都是 O(1)。

    如何選擇沖突解決方法?

    Java 中 LinkedHashMap 就采用了鏈表法解決沖突,ThreadLocalMap 是通過線性探測的開放尋址法來解決沖突。

    1. 開放尋址法

  • 優點:開放尋址法不像鏈表法,需要拉很多鏈表。散列表中的數據都存儲在數組中,可以有效地利用 CPU 緩存加快查詢速度。而且,這種方法實現的散列表,序列化起來比較簡單。鏈表法包含指針,序列化起來就沒那么容易。
  • 缺點:開放尋址法解決沖突的散列表,刪除數據的時候比較麻煩,需要特殊標記已經刪除掉的數據。而且,在開放尋址法中,所有的數據都存儲在一個數組中,比起鏈表法來說,沖突的代價更高。所以,使用開放尋址法解決沖突的散列表,裝載因子的上限不能太大。這也導致這種方法比鏈表法更浪費內存空間。
  • 當數據量比較小、裝載因子小的時候,適合采用開放尋址法。 這也是 Java 中的ThreadLocalMap使用開放尋址法解決散列沖突的原因。

    2. 鏈表法

  • 首先,鏈表法對內存的利用率比開放尋址法要高。因為鏈表結點可以在需要的時候再創建,并不需要像開放尋址法那樣事先申請好。
  • 對于鏈表法來說,只要散列函數的值隨機均勻,即便裝載因子變成 10,也就是鏈表的長度變長了而已,雖然查找效率有所下降,但是比起順序查找還是快很多。
  • 鏈表因為要存儲指針,所以對于比較小的對象的存儲,是比較消耗內存的,(如果我們存儲的是大對象,也就是說要存儲的對象的大小遠遠大于一個指針的大小(4 個字節或者 8 個字節),那鏈表中指針的內存消耗在大對象面前就可以忽略),還有可能會讓內存的消耗翻倍。而且,因為鏈表中的結點是零散分布在內存中的,不是連續的,所以對 CPU 緩存是不友好的,這方面對于執行效率也有一定的影響。
  • 實現一個更加高效的散列表。那就是,我們將鏈表法中的鏈表改造為其他高效的動態數據結構,比如跳表、紅黑樹。這樣,即便出現散列沖突,極端情況下,所有的數據都散列到同一個桶內,那最終退化成的散列表的查找時間也只不過是 O(logn)。這樣也就有效避免了前面講到的散列碰撞攻擊。
  • 基于鏈表的散列沖突處理方法比較適合存儲大對象、大數據量的散列表,而且,比起開放尋址法,它更加靈活,支持更多的優化策略,比如用紅黑樹代替鏈表。

    HashMap

    1. 初始大小

    HashMap 默認的初始大小是 16,當然這個默認值是可以設置的,如果事先知道大概的數據量有多大,可以通過修改默認初始大小,減少動態擴容的次數,這樣會大大提高 HashMap 的性能。

    2. 裝載因子和動態擴容

    最大裝載因子默認是 0.75,當 HashMap 中元素個數超過 0.75*capacity(capacity 表示散列表的容量)的時候,就會啟動擴容,每次擴容都會擴容為原來的兩倍大小

    3. 散列沖突解決方法

    HashMap 底層采用鏈表法來解決沖突。即使負載因子和散列函數設計得再合理,也免不了會出現拉鏈過長的情況,一旦出現拉鏈過長,則會嚴重影響 HashMap 的性能。于是,在 JDK1.8 版本中,為了對 HashMap 做進一步優化,我們引入了紅黑樹。而當鏈表長度太長(默認超過 8)時,鏈表就轉換為紅黑樹。我們可以利用紅黑樹快速增刪改查的特點,提高 HashMap 的性能。**當紅黑樹結點個數少于 6個的時候,又會將紅黑樹轉化為鏈表。**因為在數據量較小的情況下,紅黑樹要維護平衡,比起鏈表來,性能上的優勢并不明顯。

    4. 散列函數

    散列函數的設計并不復雜,追求的是簡單高效、分布均勻。


    散列表和鏈表的配合使用

    LRU 緩存淘汰算法

    一個緩存(cache)系統主要包含下面這幾個操作:

    • 往緩存中添加一個數據;
    • 從緩存中刪除一個數據;
    • 在緩存中查找一個數據。

    其實很簡單:
    因為鏈表的查詢效率低,而插入和刪除的效率高。
    于是:利用數組(散列表)的查詢效率,用散列表的散列函數給雙向鏈表做一個“索引”:

    前驅和后繼指針是為了將結點串在雙向鏈表中,hnext 指針是為了將結點串在散列表的拉鏈中

    1. 查找一個數據

    散列表中查找數據的時間復雜度接近 O(1),所以通過散列表,我們可以很快地在緩存中找到一個數據。當找到數據之后,我們還需要將它移動到雙向鏈表的尾部。

    2. 刪除一個數據

    需要找到數據所在的結點,然后將結點刪除。借助散列表,我們可以在 O(1) 時間復雜度里找到要刪除的結點。因為我們的鏈表是雙向鏈表,雙向鏈表可以通過前驅指針 O(1) 時間復雜度獲取前驅結點,所以在雙向鏈表中,刪除結點只需要 O(1) 的時間復雜度。

    3.添加一個數據(鏈表不允許有重復key!!!)

    需要先看這個數據是否已經在緩存中。(用散列表去查找)如果已經在其中,需要將其移動到雙向鏈表的尾部;如果不在其中,還要看緩存有沒有滿。如果滿了,則將雙向鏈表頭部的結點刪除,然后再將數據放到鏈表的尾部;如果沒有滿,就直接將數據放到鏈表的尾部。這整個過程涉及的查找操作都可以通過散列表來完成。

    其他的操作,比如刪除頭結點、鏈表尾部插入數據等,都可以在 O(1) 的時間復雜度內完成。所以,這三個操作的時間復雜度都是 O(1)。至此,我們就通過散列表和雙向鏈表的組合使用,實現了一個高效的、支持 LRU 緩存淘汰算法的緩存系統原型。


    Redis 有序集合

    在有序集合中,每個成員對象有兩個重要的屬性,key(鍵值)和 score(分值)。我們不僅會通過 score 來查找數據,還會通過 key 來查找數據。

    Redis 有序集合的操作,那就是下面這樣:

    • 添加一個成員對象;
    • 按照鍵值來刪除一個成員對象;
    • 按照鍵值來查找一個成員對象;
    • 按照分值區間查找數據,比如查找積分在[100,356]之間的成員對象;
    • 按照分值從小到大排序成員變量;

    散列表配合跳表使用:
    1. 利用跳表:按照分值(區間)查找對象(那么鏈表必須要按分值排序)
    2. 再按照鍵值構建一個散列表:按照 key 來刪除、查找一個成員對象(時間復雜度O(1))


    LinkedHashMap(有序)

    但還是不能重復,鏈表就是不能重復!

    LinkedHashMap 是通過雙向鏈表和散列表這兩種數據結構組合實現的。

    和上面的LRU 緩存淘汰算法實現原理一樣!(只是,LinkedHashMap支持擴容,和HashMap容量、擴容什么的都一樣!)

    // 10是初始大小,0.75是裝載因子,true是表示按照訪問時間排序 HashMap<Integer, Integer> m = new LinkedHashMap<>(10, 0.75f, true); m.put(3, 11); m.put(1, 12); m.put(5, 23); m.put(2, 22);m.put(3, 26); m.get(5);for (Map.Entry e : m.entrySet()) {System.out.println(e.getKey());//1,2,3,5 }
  • 添加數據到雙鏈表尾部
    先用散列表查找鍵值是否已存在:
    若存在,則刪除存在元素,將新對象插入到雙向鏈表的尾部,并且串在對應散列表的拉鏈中;
    若不存在,則直接將新對象插入到雙向鏈表的尾部,并且串在對應散列表的拉鏈中。
  • 刪除就用散列表查詢到后刪除
  • 訪問元素后移到雙向鏈表尾部(因為構造LinedHashMap時,第三個參數accessOrder為true)
    利用散列表訪問到元素后,將元素變為雙向鏈表尾結點(但此過程元素仍然串在對應散列表的拉鏈中)
  • 為什么散列表和鏈表經常會一起使用?

    就是為了散列表存儲下的數據能夠有序遍歷!!!
    因為查找、插入、刪除這些散列表自己就可以做!!!

    思考題

    1.如果把LinkedHashMap雙向鏈表改成單鏈表,還能否正常工作呢?為什么呢?
    不能,因為在雙向鏈表中刪除元素O(1),而單鏈表中這就變成O(n)。

    2.假設獵聘網有 10 萬名獵頭,每個獵頭都可以通過做任務(比如發布職位)來積累積分,然后通過積分來下載簡歷。假設你是獵聘網的一名工程師,如何在內存中存儲這 10 萬個獵頭 ID 和積分信息,讓它能夠支持這樣幾個操作:

    • 根據獵頭的 ID 快速查找、刪除、更新這個獵頭的積分信息;
    • 查找積分在某個區間的獵頭 ID 列表;
    • 查找按照積分從小到大排名在第 x 位到第 y 位之間的獵頭 ID 列表。

    答:以積分排序構建一個跳表,再以獵頭 ID 構建一個散列表。

    1)ID 在散列表中所以可以 O(1) 查找到這個獵頭;
    2)積分以跳表存儲,跳表支持區間查詢;
    3)這點根據目前學習的知識暫時無法實現。

    3.Word 文檔中單詞拼寫檢查功能是如何實現的?

    常用的英文單詞有 20 萬個左右,假設單詞的平均長度是 10 個字母,平均一個單詞占用 10 個字節的內存空間,那 20 萬英文單詞大約占 2MB 的存儲空間,就算放大 10 倍也就是 20MB。對于現在的計算機來說,這個大小完全可以放在內存里面

    所以我們可以用散列表來存儲整個英文單詞詞典。當用戶輸入某個英文單詞時,我們拿用戶輸入的單詞去散列表中查找。 如果查到,則說明拼寫正確;如果沒有查到,則說明拼寫可能有誤,給予提示。借助散列表這種數據結構,我們就可以輕松實現快速判斷是否存在拼寫錯誤。

    我知道你頭楞了,來總結一下!

    • 需要以A屬性區間查找對象 ===> 跳表:以A屬性將對象排序構造跳表
    • 需要以B屬性快速查找(刪除 刪除前要先查找)===> 散列表:以B屬性構建散列表
    • 需要有序遍歷整體對象 ===> 雙向鏈表:按照插入(訪問)順序維護一個雙向鏈表

    萬變不離其宗:其實這些都是鏈表和數組的結合使用,數組有利于隨機訪問,鏈表有利于插入刪除。各種衍生方式的出現都是按照情況將兩者結合,揚其所長、避其所短。

    總結

    以上是生活随笔為你收集整理的数据结构☞散列表的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。