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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

为什么redis取出来是null_跳表:为什么Redis一定要用跳表来实现有序集合

發(fā)布時間:2025/3/19 数据库 19 豆豆
生活随笔 收集整理的這篇文章主要介紹了 为什么redis取出来是null_跳表:为什么Redis一定要用跳表来实现有序集合 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

上兩節(jié)我們講了二分查找算法。當(dāng)時我講到,因為二分查找底層依賴的是數(shù)組隨機訪問的特性,所以只能用數(shù)組來實現(xiàn)。如果數(shù)據(jù)存儲在鏈表中,就真的沒法用二分查找算法了嗎?

實際上,我們只需要對鏈表稍加改造,就可以支持類似“二分”的查找算法。我們把改造之后的數(shù)據(jù)結(jié)構(gòu)叫作跳表(Skip list),也就是今天要講的內(nèi)容。

跳表這種數(shù)據(jù)結(jié)構(gòu)對你來說,可能會比較陌生,因為一般的數(shù)據(jù)結(jié)構(gòu)和算法書籍里都不怎么會講。但是它確實是一種各方面性能都比較優(yōu)秀的動態(tài)數(shù)據(jù)結(jié)構(gòu),可以支持快速插入,刪除,查找操作,寫起來也不復(fù)雜,甚至可以替代紅黑樹(Red-black-tree)。

Redis中的有序集合(Sorted Set)就是用跳表來實現(xiàn)的。如果你有一定基礎(chǔ),應(yīng)該知道紅黑樹也可以實現(xiàn)地插入,刪除和查找操作。那Redis為什么會選擇用跳表來實現(xiàn)有序集合呢?為什么不用紅黑樹呢?學(xué)完今天的內(nèi)容,你就知道答案了。

如何理解“跳表”?

對于一個單鏈表來講,即便鏈表中存儲的數(shù)據(jù)是有序的,如果我們要想在其中查找某個數(shù)據(jù),也只能從頭到尾遍歷鏈表。這樣查找效率就會很低,時間復(fù)雜度很高,是O(n)。

那怎么來提高查找效率呢?如果像圖中那樣,對鏈表建立一級“索引”,查找起來是不是就會更快一些呢?每兩個結(jié)點提取一個結(jié)點到上一級,我們把抽出來的那一級叫做索引或索引層。你可以看我畫圖。圖中的down表示down指針,指向下一級結(jié)點。

如果我們現(xiàn)在要查找某個結(jié)點,比如17。我們可以先在索引層遍歷,當(dāng)遍歷到索引層中值為16,我們發(fā)現(xiàn)下一個結(jié)點是18,那要查找的結(jié)點17肯定就在這兩個結(jié)點之間。然后我們通過索引層結(jié)點的down指針,下降到原始鏈表這一層,繼續(xù)遍歷。這個時候,我們只需要再遍歷2個結(jié)點,就可以找等于17的這個結(jié)點了。這樣原來如果要查找17,需要遍歷10個結(jié)點,現(xiàn)在只需要7個結(jié)點。

從這個例子里,我們看出,加來一層索引之后,查找一個結(jié)點需要的結(jié)點個數(shù)減少了,也就是說查找效率提高了。那如果我們再加一級索引呢?效率會不會提升更多呢?

跟前面建立第一級索引的方式相似,我們在第一級索引的基礎(chǔ)之上,每兩個結(jié)點就抽出一個結(jié)點到第二級索引。現(xiàn)在我們再來查找17,需要6個結(jié)點了,需要遍歷的結(jié)點數(shù)量又減少了。

我舉的例子數(shù)據(jù)量不大,所以即便加了兩級索引,查找效率的提升也并不明顯。為了讓你能真切地感受索引提升查找效率。我畫了一個包含64個結(jié)點的鏈表,按照前面講的這種思路,建立了五級索引。

從圖中我們可以看出,原來沒有索引的時候,查找62需要遍歷62個結(jié)點,現(xiàn)在只需要遍歷11個結(jié)點,速度是不是提高了很多?所以,當(dāng)鏈表的長度n比較大時,比如1000,10000的時候,在構(gòu)建索引之后,查找效率的提升就會非常明顯。

前面講的這種鏈表加多級索引的結(jié)構(gòu),就是跳表。我通過例子給你展示了跳表是如何減少查詢次數(shù)的,現(xiàn)在你應(yīng)該比較清晰地知道,跳表確實是可以提高查詢效率的。接下里,我會定量地分析一下,用跳表查詢到底有多快。

用跳表查詢到底有多快?

前面我講過,算法的執(zhí)行效率可以通過時間復(fù)雜度來度量,這里依舊可以用。我們知道,在一個單鏈表中查詢某個數(shù)據(jù)的時間復(fù)雜度是O(n)。那在一個具有多級索引的跳表中,查詢某個數(shù)據(jù)的時間復(fù)雜度是多少呢?

這個時間復(fù)雜度的分析方法比較難想到。我把問題分解一下,先來看這樣的一個問題,如果鏈表里有n個結(jié)點,會有多少級索引呢?

按照我們剛才講的,每兩個結(jié)點會抽出一個結(jié)點作為上一級索引的結(jié)點,那第一級索引的結(jié)點個數(shù)大約就是n/2,第二級索引的結(jié)點個數(shù)大約就是n/4,第三級索引的結(jié)點個數(shù)大約就是n/8,依次類推,也就是說,第k級索引的結(jié)點個數(shù)是第k-1級索引的結(jié)點個數(shù)的1/2,那第k級索引結(jié)點的個數(shù)的就是n/(2^k)。

假設(shè)索引有h級,最高級的索引有2個結(jié)點。通過上面的公式,我們可以得到n/(2^h) =2,從而求得h = log2n-1。如果包含原始鏈表這一層,整個跳表的高度就是log2n。我們在跳表中查詢某個數(shù)據(jù)的時候,如果每一層都要遍歷m個結(jié)點,那在跳表中查詢一個數(shù)據(jù)的時間復(fù)雜度就是O(m*logn)。

那這個m的值是多少呢?按照前面這種索引結(jié)構(gòu),我們每一級索引都最多只需要遍歷3個結(jié)點,也就是說m=3,為什么是3呢?我解釋一下。

假設(shè)我們要查找的數(shù)據(jù)是x,在第k級索引中,我們遍歷到y(tǒng)的結(jié)點之后,發(fā)現(xiàn)x大于y,小于后面的結(jié)點z,所以我們通過y的down指針,從第k級索引下降到第k-1級索引。在第k-1級 索引中,y和z之間只有3個結(jié)點(包含y和z),所以,我們在k-1級索引中最多只需要遍歷3個結(jié)點,依次類推,每一級索引都最多只需要遍歷3個結(jié)點。

通過上面的分析,我們得到m=3,所以在跳表中查找任意數(shù)據(jù)的時間復(fù)雜度就是O(logn)。這個查找的時間復(fù)雜度跟二分查找是一樣的。換句話說,我們其實是基于單鏈表實現(xiàn)的二分查找,是不是很神奇?不過,天下沒有免費的午餐,這種查詢效率的提升,前提是建立了很多級索引,也就是我們在第6節(jié)講過的空間換時間的設(shè)計思路。

跳表是不是很浪費內(nèi)存?

比起單純的單鏈表,跳表需要存儲多級索引,肯定要消耗更多的存儲空間。那到底需要消耗多少額外的存儲空間呢?我們來分析一下跳表的空間復(fù)雜度。

跳表的空間復(fù)雜度分析并不難,我在前面說了,假設(shè)原始鏈表大小為n,那第一級索引大約有n/2個結(jié)點,第二級索引大約有n/4個結(jié)點,以此類推,每上升一級就減少一半,直到剩下2個結(jié)點。如果我們每層索引的結(jié)點數(shù)寫出來,就是一個等比數(shù)列。

這幾級索引得到結(jié)點總和就是n/2+n/4+n/8...+8+4+2=n-2。所以,跳表的空間復(fù)雜度是O(n)。也就是說,如果將包含n個結(jié)點的單鏈表構(gòu)造成跳表,我們需要額外再用接近n個結(jié)點的存儲空間。那我們有沒有辦法降低索引占用的內(nèi)存空間呢?

實際上,在軟件開發(fā)中,我們不必太在意索引占用的額外空間。在講數(shù)據(jù)結(jié)構(gòu)和算法時,我們習(xí)慣性地把要處理的數(shù)據(jù)看成整數(shù),但是在實際的軟件開發(fā)中,原始鏈表中存儲的有可能是很大的對象,而索引結(jié)點只需要存儲關(guān)鍵值和幾個指針,并不需要存儲對象,所以當(dāng)對象比索引結(jié)點大很多時,那索引占用的額外空間就可以忽略了。

高效的動態(tài)插入和刪除

跳表長什么樣子我想你應(yīng)該已經(jīng)很清楚了,它的查找操作我們剛剛也講過了。實際上,跳表這個動態(tài)數(shù)據(jù)結(jié)構(gòu),不僅支持查找操作,還支持動態(tài)的插入,刪除操作,而且插入,刪除操作的時間復(fù)雜度也是O(logn)。

我們現(xiàn)在來看下,如何在跳表中插入一個數(shù)據(jù),以及它是如何做到O(logn)的時間復(fù)雜度的?

我們知道,在單鏈表中,一旦定位好要插入的位置,插入結(jié)點的時間復(fù)雜度是很低的,就是O(1)。但是,這里為了保證原始鏈表中數(shù)據(jù)的有序性,我們需要先找到要插入的位置,這個查找操作就會比較耗時。

對于純粹的單鏈表,需要遍歷每個結(jié)點,來找到插入的位置。但是,對于跳表來說,我們講過查找某個結(jié)點的時間復(fù)雜度是O(logn),所以這里查找某個數(shù)據(jù)應(yīng)該插入的位置,方法也是類似的,時間復(fù)雜度也是O(logn)。我畫了一張圖,你可以很清晰地看到插入的過程。

好了,我們再來看刪除操作。

如果這個結(jié)點在索引中也有出現(xiàn),我們除了要刪除原始鏈表中的結(jié)點,還要刪除索引中的。因為單鏈表中的刪除操作需要拿到要刪除結(jié)點的前驅(qū)結(jié)點,然后通過指針操作完成刪除。所以在查找要刪除的結(jié)點的時候,一定要獲取前驅(qū)結(jié)點。當(dāng)然,如果我們用的雙向鏈表,就不需要考慮這個問題了。

跳表索引動態(tài)更新

當(dāng)我們不停地往跳表中插入數(shù)據(jù)時,如果我們不更新索引,就有可能出現(xiàn)某2個索引結(jié)點之間數(shù)據(jù)非常多的情況。極端情況下,跳表還會退化成單鏈表。

作為一種動態(tài)數(shù)據(jù)結(jié)構(gòu),我們需要某種手段來維護(hù)索引與原始鏈表大小之間的平衡,也就是說,如果鏈表中結(jié)點多了,索引結(jié)點就相應(yīng)地增加一些,避免復(fù)雜度退化,以及查找,插入,刪除操作性能下降。

如果你了解紅黑樹,AVL樹這樣平衡二叉樹,你就知道他們是通過左右旋的方式保持左右子樹的大小平衡(如果不了解也沒關(guān)系,我們后面會講),而跳表是通過隨機函數(shù)來維護(hù)前面提到的“平衡性”。

當(dāng)我們往跳表中插入數(shù)據(jù)的時候,我們可以選擇同時將這個數(shù)據(jù)插入到部分索引層中。如何選擇假如哪些索引層呢?

我們通過一個隨機函數(shù),來決定將這個結(jié)點插入到哪幾級索引中,比如隨機函數(shù)生成了值K,那我們就將這個結(jié)點添加到第一級到第K級這K級索引中。

隨機函數(shù)的選擇很有講究,從概率上來講,能夠保證跳表的索引大小和數(shù)據(jù)大小平衡性,不至于性能過度退化。至于隨機函數(shù)的選擇,我就不展開講解了。

跳表的實現(xiàn)還是稍微有點復(fù)雜,跳表的實現(xiàn)并不是我們這節(jié)的重點。

解答開篇

今天的內(nèi)容到此就講完了。現(xiàn)在,我來講解一下開篇的思考題:為什么Redis要用跳表來實現(xiàn)有序集合,而不是紅黑樹。

Redis中的有序集合是通過跳表來實現(xiàn)的,嚴(yán)格點講,其實還用到了散列表。不過散列表我們后面才會講到,所以我們現(xiàn)在暫且忽略這部分。如果你去查看Redis的開發(fā)手冊,就會發(fā)現(xiàn),Redis中的有序集合支持的核心操作主要有下面這幾個:

·插入一個數(shù)據(jù)

·刪除一個數(shù)據(jù)

·查找一個數(shù)據(jù)

·按照區(qū)間查找數(shù)據(jù)(比如查找值在[100,356]之間的數(shù)據(jù))

·迭代輸出有序序列。

其中插入,刪除,查找以及迭代輸出有序序列這幾個操作,紅黑樹也可以完成,時間復(fù)雜度跟跳表是一樣的。但是,按照區(qū)間來查找數(shù)據(jù)這個操作,紅黑樹的效率沒有跳表高。

對于按照區(qū)間查找數(shù)據(jù)這個操作,跳表可以做到O(logn)的時間復(fù)雜度定位區(qū)間的起點,然后在原始鏈表中順序往后遍歷就可以了。這樣做非常高效。

當(dāng)然,Redis之所以用跳表來實現(xiàn)有序集合,還有其他原因,比如,跳表更容易代碼實現(xiàn)。雖然跳表的實現(xiàn)也不簡單,但比起紅黑樹來說還是好懂,好寫多了,而簡單就意味著可讀性好,不容易出錯。還有,跳表更加靈活,它可以通過改變索引構(gòu)建策略,有效平衡執(zhí)行效率和內(nèi)存消耗。

不過,跳表也不能完全替代紅黑樹。因為紅黑樹比跳表的出現(xiàn)要早一些,很多編程語言中的map類型都是通過紅黑樹來實現(xiàn)的,我們做業(yè)務(wù)開發(fā)的是,直接拿來用就可以了,不用費勁自己去實現(xiàn)一個紅黑樹,但是跳表并沒有一個現(xiàn)成的實現(xiàn),所以,在開發(fā)中,如果你想使用跳表,必須要自己實現(xiàn)。

內(nèi)容小結(jié)

今天我們講了跳表這種數(shù)據(jù)結(jié)構(gòu)。跳表使用空間換時間的設(shè)計思路,通過構(gòu)建多級索引來提高查詢的效率,實現(xiàn)了基于鏈表的“二分查找”。跳表時候一種動態(tài)數(shù)據(jù)結(jié)構(gòu),支持快速地插入,刪除,查找操作,時間復(fù)雜度都是O(logn)。

跳表的空間復(fù)雜度是O(n)。不過,跳表的實現(xiàn)非常靈活,可以通過改變索引構(gòu)建策略,有效平衡執(zhí)行效率和內(nèi)存消耗。雖然跳表的代碼實現(xiàn)并不簡單,但是作為一種動態(tài)數(shù)據(jù)結(jié)構(gòu),比起紅黑樹來說,實現(xiàn)要簡單多了。所以很多時候,我們?yōu)榱舜a的簡單,易讀,比起紅黑樹,我們更傾向用跳表。

參考文獻(xiàn)

王爭老師 《數(shù)據(jù)結(jié)構(gòu)與算法之美》

與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖

總結(jié)

以上是生活随笔為你收集整理的为什么redis取出来是null_跳表:为什么Redis一定要用跳表来实现有序集合的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

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