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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

20 | 散列表(下):为什么散列表和链表经常会一起使用?

發(fā)布時間:2023/12/10 编程问答 45 豆豆
生活随笔 收集整理的這篇文章主要介紹了 20 | 散列表(下):为什么散列表和链表经常会一起使用? 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

有兩種數(shù)據(jù)結(jié)構(gòu),散列表和鏈表經(jīng)常會被放在一起使用。常見的使用方式有:

  • 用鏈表來實(shí)現(xiàn) LRU 緩存淘汰算法,鏈表實(shí)現(xiàn)的 LRU 緩存淘汰算法的時間復(fù)雜度是 O(n),通過散列表可以將這個時間復(fù)雜度降低到 O(1)
  • Redis 的有序集合是使用跳表來實(shí)現(xiàn)的,跳表可以看作一種改進(jìn)版的鏈表。當(dāng)Redis 有序集合不僅使用了跳表,還用到了散列表。
  • Java中的LinkedHashMap 容器也用到了散列表和鏈表
  • 問題一:散列表和鏈表都是如何組合起來使用的?

    問題二:為什么散列表和鏈表會經(jīng)常放到一塊使用,形影不離?

    LRU 緩存淘汰算法

    LRU緩存算法的核心點(diǎn):

    • 需要維護(hù)一個按照訪問時間從大到小有序排列的鏈表結(jié)構(gòu)
    • 當(dāng)緩存空間不夠,需要淘數(shù)據(jù)的時候?qū)㈡湵眍^部的結(jié)點(diǎn)刪除。
    • 當(dāng)要緩存某個數(shù)據(jù)的時候,先在鏈表中查找這個數(shù)據(jù)。如果沒有找到,則直接將數(shù)據(jù)放到鏈表的尾部;如果找到了,我們就把它移動到鏈表的尾部

    因?yàn)椴檎覕?shù)據(jù)需要遍歷鏈表,所以單純用鏈表實(shí)現(xiàn)的 LRU 緩存淘汰算法的時間復(fù)雜很高,是 O(n)

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

    往緩存中添加一個數(shù)據(jù);

    從緩存中刪除一個數(shù)據(jù);

    在緩存中查找一個數(shù)據(jù);

    這三個操作都要涉及查找操作,如果單純地采用鏈表的話,時間復(fù)雜度只能是 O(n)。如果我們將散列表和鏈表兩種數(shù)據(jù)結(jié)構(gòu)組合使用,可以將這三個操作的時間復(fù)雜度都降低到 O(1)具體的結(jié)構(gòu)就是下面這個樣子:

    使用雙向鏈表存儲數(shù)據(jù),鏈表中的每個結(jié)點(diǎn)處理存儲數(shù)據(jù)(data)、前驅(qū)指針(prev)、后繼指針(next)之外,還新增了一個特殊的字段 hnext。這個 hnext 有什么作用呢?

    散列表是通過鏈表法解決散列沖突的,所以每個結(jié)點(diǎn)會在兩條鏈中。一個鏈?zhǔn)莿倓偽覀兲岬降?/span>雙向鏈表,另一個鏈?zhǔn)巧⒘斜碇械?strong>拉鏈。前驅(qū)和后繼指針是為了將結(jié)點(diǎn)串在雙向鏈表中,hnext 指針是為了將結(jié)點(diǎn)串在散列表的拉鏈中

    這種雙鏈表和散列表組合的結(jié)構(gòu)中緩存的三個操作,是如何做到時間復(fù)雜度是 O(1) 的?

  • 如何查找一個數(shù)據(jù)。我們前面講過,散列表中查找數(shù)據(jù)的時間復(fù)雜度接近 O(1),所以通過散列表,我們可以很快地在緩存中找到一個數(shù)據(jù)。當(dāng)找到數(shù)據(jù)之后,我們還需要將它移動到雙向鏈表的尾部。
  • 如何刪除一個數(shù)據(jù)。我們需要找到數(shù)據(jù)所在的結(jié)點(diǎn),然后將結(jié)點(diǎn)刪除。借助散列表,我們可以在 O(1) 時間復(fù)雜度里找到要刪除的結(jié)點(diǎn)。因?yàn)槲覀兊逆湵硎请p向鏈表,雙向鏈表可以通過前驅(qū)指針 O(1) 時間復(fù)雜度獲取前驅(qū)結(jié)點(diǎn),所以在雙向鏈表中,刪除結(jié)點(diǎn)只需要 O(1) 的時間復(fù)雜度。
  • 如何添加一個數(shù)據(jù)。添加數(shù)據(jù)到緩存稍微有點(diǎn)麻煩,我們需要先看這個數(shù)據(jù)是否已經(jīng)在緩存中。如果已經(jīng)在其中,需要將其移動到雙向鏈表的尾部;如果不在其中,還要看緩存有沒有滿。如果滿了,則將雙向鏈表頭部的結(jié)點(diǎn)刪除,然后再將數(shù)據(jù)放到鏈表的尾部;如果沒有滿,就直接將數(shù)據(jù)放到鏈表的尾部
  • 這整個過程涉及的查找操作都可以通過散列表來完成。其他的操作,比如刪除頭結(jié)點(diǎn)、鏈表尾部插入數(shù)據(jù)等,都可以在 O(1) 的時間復(fù)雜度內(nèi)完成。所以,這三個操作的時間復(fù)雜度都是 O(1)。至此,我們就通過散列表和雙向鏈表的組合使用,實(shí)現(xiàn)了一個高效的、支持 LRU 緩存淘汰算法的緩存系統(tǒng)原型。

    工業(yè)級實(shí)例

    Redis 有序集合(zSet)

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

    比如用戶積分排行榜有這樣一個功能:可以通過用戶 ID 來查找積分信息,也可以通過積分區(qū)間來查找用戶 ID 或者姓名信息。這里包含 ID、姓名和積分的用戶信息,就是成員對象,用戶 ID 就是 key,積分就是 score

    如果我們細(xì)化一下 Redis 有序集合的操作,那就是下面這樣:

    添加一個成員對象;

    按照鍵值來刪除一個成員對象;

    按照鍵值來查找一個成員對象;

    按照分值區(qū)間查找數(shù)據(jù),比如查找積分在[100, 356]之間的成員對象;

    按照分值從小到大排序成員變量;

    如果按照分值將成員對象組織成跳表的結(jié)構(gòu),那按照鍵值來刪除、查詢成員對象就會很慢,解決方法與 LRU 緩存淘汰算法的解決方法類似。再按照鍵值構(gòu)建一個散列表,這樣按照 key 來刪除、查找一個成員對象的時間復(fù)雜度就變成了 O(1)。同時借助跳表結(jié)構(gòu),其他操作也非常高效。

    實(shí)際上Redis 有序集合的操作還有另外一類,也就是查找成員對象的排名(Rank)或者根據(jù)排名區(qū)間查找成員對象。這個功能單純用剛剛講的這種組合結(jié)構(gòu)就無法高效實(shí)現(xiàn)了。

    Java LinkedHashMap

    HashMap 底層是通過散列表這種數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的。而 LinkedHashMap 前面比 HashMap 多了一個“Linked”是不是說,LinkedHashMap 是一個通過鏈表法解決散列沖突的散列表呢?

    實(shí)際上,LinkedHashMap 并沒有這么簡單,其中的“Linked”也并不僅僅代表它是通過鏈表法解決散列沖突的。看一段代碼。你覺得這段代碼會以什么樣的順序打印 3152 這幾個 key ?原因是什么?

    HashMap<Integer, Integer> m = new LinkedHashMap<>(); m.put(3, 11); m.put(1, 12); m.put(5, 23); m.put(2, 22); for (Map.Entry e : m.entrySet()) {System.out.println(e.getKey()); }

    打印的順序315,2。散列表中數(shù)據(jù)是經(jīng)過散列函數(shù)打亂之后無規(guī)律存儲的,這里是如何實(shí)現(xiàn)按照數(shù)據(jù)的插入順序來遍歷打印?LinkedHashMap 也是通過散列表和鏈表組合在一起實(shí)現(xiàn)的。它不僅支持按照插入順序遍歷數(shù)據(jù),還支持按照訪問順序來遍歷數(shù)據(jù)。看下面這段代碼:

    // 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()); }

    打印的結(jié)果是 123,5。每次調(diào)用 put() 函數(shù),往 LinkedHashMap 中添加數(shù)據(jù)的時候,都會將數(shù)據(jù)添加到鏈表的尾部。第 8 行代碼中,再次將鍵值為 3 的數(shù)據(jù)放入到 LinkedHashMap 的時候,會先查找這個鍵值是否已經(jīng)有了,然后,再將已經(jīng)存在的 (3,11) 刪除,并且將新的 (3,26) 放到鏈表的尾部。當(dāng)?shù)?/span> 9 行代碼訪問到 key 5 的數(shù)據(jù)的時候,我們將被訪問到的數(shù)據(jù)移動到鏈表的尾部。最后的結(jié)果是這樣:

    按照訪問時間排序的 LinkedHashMap 本身就是一個支持 LRU 緩存淘汰策略的緩存系統(tǒng)LinkedHashMap 是通過雙向鏈表和散列表這兩種數(shù)據(jù)結(jié)構(gòu)組合實(shí)現(xiàn)的。LinkedHashMap 中的“Linked”實(shí)際上是指的是雙向鏈表,并非指用鏈表法解決散列沖突

    解答開篇

    為什么散列表和鏈表經(jīng)常一塊使用?

    散列表這種數(shù)據(jù)結(jié)構(gòu)雖然支持高效的插入、刪除、查找操作,但是散列表中數(shù)據(jù)都是無規(guī)律存儲的,無法支持按照某種順序快速遍歷數(shù)據(jù)。如果希望按照順序遍歷散列表中數(shù)據(jù),那需要將散列表中的數(shù)據(jù)拷貝到數(shù)組中然后排序再遍歷。而散列表是動態(tài)數(shù)據(jù)結(jié)構(gòu),不停地有數(shù)據(jù)的插入、刪除,每當(dāng)按順序遍歷散列表中的數(shù)據(jù)的時候都需要先排序效率很低。為了解決這個問題,將散列表和鏈表(或者跳表)結(jié)合在一起使用

    思考題:

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

    根據(jù)獵頭的 ID 快速查找、刪除、更新這個獵頭的積分信息;

    查找積分在某個區(qū)間的獵頭 ID 列表;

    查找按照積分從小到大排名在第 x 位到第 y 位之間的獵頭 ID 列表。

    ?

    總結(jié)

    以上是生活随笔為你收集整理的20 | 散列表(下):为什么散列表和链表经常会一起使用?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。