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

歡迎訪問 生活随笔!

生活随笔

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

编程问答

每日一博 - 如何理解跳表(SkipList)

發(fā)布時間:2025/3/21 编程问答 20 豆豆
生活随笔 收集整理的這篇文章主要介紹了 每日一博 - 如何理解跳表(SkipList) 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

文章目錄

  • 什么是跳躍表SkipList
  • 跳表關(guān)鍵字
  • Why Skip List
  • Code
    • 跳表-查詢
    • 跳表-刪除
    • 跳表-插入
  • 小結(jié)
  • 完整Code


什么是跳躍表SkipList

跳躍表(簡稱跳表)由美國計算機(jī)科學(xué)家William Pugh于1989年發(fā)明

論文: Skip lists: a probabilistic alternative to balanced trees

跳表(SkipList,全稱跳躍表)是用于有序元素序列快速搜索查找的一個數(shù)據(jù)結(jié)構(gòu),跳表是一個隨機(jī)化的數(shù)據(jù)結(jié)構(gòu),實質(zhì)就是一種可以進(jìn)行二分查找的有序鏈表

跳表在原有的有序鏈表上面增加了多級索引,通過索引來實現(xiàn)快速查找。

跳表不僅能提高搜索性能,同時也可以提高插入和刪除操作的性能。它在性能上和紅黑樹,AVL樹不相上下,但是跳表的原理非常簡單,實現(xiàn)也比紅黑樹簡單很多。


跳表關(guān)鍵字

  • 隨機(jī)化
  • 有序鏈表
  • 索引
  • 二分查找

Why Skip List

地球人都知道的事兒:

  • 順序表(數(shù)組)是內(nèi)存上一塊連續(xù)的區(qū)域,基于下標(biāo),查找速度快。
  • 鏈表: 內(nèi)存上不連續(xù),通過指針相連, 插入和刪除動作效率特別高,但是查詢呢,時間復(fù)雜度o(n)

那么鏈表上查詢的時間復(fù)雜度能優(yōu)化一下嗎?

我們知道有很多算法有個思想: 空間換時間 。 如果在鏈表的上面加一層索引,讓部分節(jié)點在上層能夠直接定位到,這樣鏈表的查詢時間近乎減少一半 。

那查詢的時候,就會發(fā)生某些變化 -----------> 如果要查找某個節(jié)點, 首先需要從上一層快速定位到節(jié)點所在的一個范圍 ,如果向下查找的有個向下的指針指向真實的數(shù)據(jù),那理論上,以前有n, 現(xiàn)在就是 n/2 .

當(dāng)然了,如果節(jié)點數(shù)據(jù)量超巨,一樣很慢,可能就損耗在了,一層一層的查找上。 我們知道二分查找每次都能折半的去壓縮查找范圍, 那用上這個二分查找是不是就會快很多????

事實上,跳表就能讓鏈表擁有近乎的接近二分查找的效率的一種數(shù)據(jù)結(jié)構(gòu),其原理依然是給上面加若干層索引,優(yōu)化查找速度。

通過上圖我們可以知道,這樣的一個數(shù)據(jù)結(jié)構(gòu)對有序鏈表進(jìn)行查找都能近乎二分的性能。

究其原因就是在上面維護(hù)了多層的索引

首先在最高級索引上查找最后一個小于當(dāng)前查找元素的位置,然后再跳到次高級索引繼續(xù)查找,直到跳到最底層為止,這時候以及十分接近要查找的元素的位置了(如果查找元素存在的話)。

由于根據(jù)索引可以一次跳過多個元素,所以跳查找的查找速度也就變快了。

對于理想的跳表,每向上一層索引節(jié)點數(shù)量都是下一層的1/2.那么如果n個節(jié)點增加的節(jié)點數(shù)量(1/2+1/4+…)<n。并且層數(shù)較低,對查找效果影響不大。

但是對于這么一個結(jié)構(gòu),你可能會疑惑,這樣完美的結(jié)構(gòu)真的存在嗎?大概率不存在的,因為作為一個鏈表,少不了增刪該查的一些操作。而刪除和插入可能會改變整個結(jié)構(gòu),所以上面的這些都是理想的結(jié)構(gòu),在插入的時候是否添加上層索引是個概率問題(1/2的概率)。


Code

在實現(xiàn)本跳表的過程為了便于操作,我們將跳表的頭結(jié)點(head)的key設(shè)為int的最小值(一定滿足左小右大方便比較)。

對于每個節(jié)點的設(shè)置,設(shè)置成SkipNode類,為了防止初學(xué)者將next向下還是向右搞混,直接設(shè)置right,down兩個指針。

class SkipNode<T> {int key;T value;SkipNode right,down;//右下個方向的指針public SkipNode (int key,T value) {this.key=key;this.value=value;} }

跳表的結(jié)構(gòu)和初始化, 其主要參數(shù)和初始化方法為:

public class SkipList <T> {SkipNode headNode;//頭節(jié)點,入口int highLevel;//當(dāng)前跳表索引層數(shù)Random random;// 用于投擲硬幣final int MAX_LEVEL = 32;//最大的層SkipList(){random=new Random();headNode=new SkipNode(Integer.MIN_VALUE,null);highLevel=0;}//其他方法 }


跳表-查詢

很多時候鏈表也可能這樣相連僅僅是某個元素或者key作為有序的標(biāo)準(zhǔn)。所以有可能鏈表內(nèi)部存在一些value。不過修改和查詢其實都是一個操作,找到關(guān)鍵數(shù)字(key)。并且查找的流程也很簡單,設(shè)置一個臨時節(jié)點team=head。當(dāng)team不為null其流程大致如下:

  • (1) 從team節(jié)點出發(fā),如果當(dāng)前節(jié)點的key與查詢的key相等,那么返回當(dāng)前節(jié)點(如果是修改操作那么一直向下進(jìn)行修改值即可)。

  • (2) 如果key不相等,且右側(cè)為null,那么證明只能向下(結(jié)果可能出現(xiàn)在下右方向),此時team=team.down

  • (3) 如果key不相等,且右側(cè)不為null,且右側(cè)節(jié)點key小于待查詢的key。那么說明同級還可向右,此時team=team.right

  • (4)(否則的情況)如果key不相等,且右側(cè)不為null,且右側(cè)節(jié)點key大于待查詢的key 。那么說明如果有結(jié)果的話就在這個索引和下個索引之間,此時team=team.down。

最終將按照這個步驟返回正確的節(jié)點或者null(說明沒查到)。


例如上圖查詢12節(jié)點.

  • 第一步從head出發(fā)發(fā)現(xiàn)右側(cè)不為空,且7<12,向右;
  • 第二步右側(cè)為null向下;
  • 第三步節(jié)點7的右側(cè)10<12繼續(xù)向右;
  • 第四步10右側(cè)為null向下;
  • 第五步右側(cè)12小于等于向右。
  • 第六步起始發(fā)現(xiàn)相等返回節(jié)點結(jié)束。

代碼如下

public SkipNode search(int key) {SkipNode team=headNode;while (team!=null) {if(team.key==key){return team;}else if(team.right==null)//右側(cè)沒有了,只能下降{team=team.down;}else if(team.right.key>key)//需要下降去尋找{team=team.down;}else //右側(cè)比較小向右{team=team.right;}}return null; }


跳表-刪除

刪除操作比起查詢稍微復(fù)雜一丟丟,但是比插入簡單。刪除需要改變鏈表結(jié)構(gòu)所以需要處理好節(jié)點之間的聯(lián)系。對于刪除操作需要謹(jǐn)記以下幾點:

  • (1)刪除當(dāng)前節(jié)點和這個節(jié)點的前后節(jié)點都有關(guān)系

  • (2)刪除當(dāng)前層節(jié)點之后,下一層該key的節(jié)點也要刪除,一直刪除到最底層

根據(jù)這兩點分析一下:如果找到當(dāng)前節(jié)點了,它的前面一個節(jié)點怎么查找呢?這個總不能再遍歷一遍吧!有的使用四個方向的指針(上下左右)用來找到左側(cè)節(jié)點。是可以的,但是這里可以特殊處理一下 ,不直接判斷和操作節(jié)點,先找到待刪除節(jié)點的左側(cè)節(jié)點。通過這個節(jié)點即可完成刪除,然后這個節(jié)點直接向下去找下一層待刪除的左側(cè)節(jié)點。

設(shè)置一個臨時節(jié)點team=head,當(dāng)team不為null具體循環(huán)流程為:

  • (1)如果team右側(cè)為null,那么team=team.down(之所以敢直接這么判斷是因為左側(cè)有頭結(jié)點在左側(cè),不用擔(dān)心特殊情況)

  • (2)如果team右側(cè)不 為null,并且右側(cè)的key等于待刪除的key,那么先刪除節(jié)點,再team向下team=team.down為了刪除下層節(jié)點。

  • (3)如果team右側(cè)不 為null,并且右側(cè)key小于待刪除的key,那么team向右team=team.right。

  • (4)如果team右側(cè)不 為null,并且右側(cè)key大于待刪除的key,那么team向下team=team.down,在下層繼續(xù)查找刪除節(jié)點。

例如上圖刪除10節(jié)點,

  • 首先team=head從team出發(fā),7<10向右(team=team.right后面省略);
  • 第二步右側(cè)為null只能向下;
  • 第三部右側(cè)為10在當(dāng)前層刪除10節(jié)點然后向下繼續(xù)查找下一層10節(jié)點;
  • 第四步8<10向右;
  • 第五步右側(cè)為10刪除該節(jié)點并且team向下。
  • team為null說明刪除完畢退出循環(huán)。
public void delete(int key)//刪除不需要考慮層數(shù) {SkipNode team=headNode;while (team!=null) {if (team.right == null) {//右側(cè)沒有了,說明這一層找到,沒有只能下降team=team.down;}else if(team.right.key==key)//找到節(jié)點,右側(cè)即為待刪除節(jié)點{team.right=team.right.right;//刪除右側(cè)節(jié)點team=team.down;//向下繼續(xù)查找刪除}else if(team.right.key>key)//右側(cè)已經(jīng)不可能了,向下{team=team.down;}else { //節(jié)點還在右側(cè)team=team.right;}} }


跳表-插入

插入操作在實現(xiàn)起來是最麻煩的,需要的考慮的東西最多。

查詢,不需要動索引;
刪除,每層索引如果有刪除就是了。

插入不一樣了,插入需要考慮是否插入索引,插入幾層等問題。

由于需要插入刪除所以我們肯定無法維護(hù)一個完全理想的索引結(jié)構(gòu),因為它耗費的代價太高。但我們使用隨機(jī)化的方法去判斷是否向上層插入索引

即產(chǎn)生一個[0-1]的隨機(jī)數(shù)如果小于0.5就向上插入索引,插入完畢后再次使用隨機(jī)數(shù)判斷是否向上插入索引。運氣好這個值可能是多層索引,運氣不好只插入最底層(這是100%插入的)。但是索引也不能不限制高度,我們一般會設(shè)置索引最高值如果大于這個值就不往上繼續(xù)添加索引了。

其流程為

  • (1)首先通過上面查找的方式,找到待插入的左節(jié)點。插入的話最底層肯定是需要插入的,所以通過鏈表插入節(jié)點(需要考慮是否為末尾節(jié)點)

  • (2)插入完這一層,需要考慮上一層是否插入,首先判斷當(dāng)前索引層級,如果大于最大值那么就停止(比如已經(jīng)到最高索引層了)。否則設(shè)置一個隨機(jī)數(shù)1/2的概率向上插入一層索引(因為理想狀態(tài)下的就是每2個向上建一個索引節(jié)點)。

  • (3)繼續(xù)(2)的操作,直到概率退出或者索引層數(shù)大于最大索引層。

具體向上插入的時候,實質(zhì)上還有非常重要的細(xì)節(jié)需要考慮。首先如何找到上層的待插入節(jié)點 ?

這個各個實現(xiàn)方法可能不同,如果有左、上指向的指針那么可以向左向上找到上層需要插入的節(jié)點,但是如果只有右指向和下指向的我們也可以巧妙的借助查詢過程中記錄下降的節(jié)點。因為曾經(jīng)下降的節(jié)點倒序就是需要插入的節(jié)點,最底層也不例外(因為沒有匹配值會下降為null結(jié)束循環(huán))。在這里我使用棧這個數(shù)據(jù)結(jié)構(gòu)進(jìn)行存儲,當(dāng)然使用List也可以。

下圖就是給了一個插入示意圖。

其次如果該層是目前的最高層索引,需要繼續(xù)向上建立索引應(yīng)該怎么辦?

首先跳表最初肯定是沒索引的,然后慢慢添加節(jié)點才有一層、二層索引,但是如果這個節(jié)點添加的索引突破當(dāng)前最高層,該怎么辦呢?

這時候需要注意了,跳表的head需要改變了,新建一個ListNode節(jié)點作為新的head,將它的down指向老head,將這個head節(jié)點加入棧中(也就是這個節(jié)點作為下次后面要插入的節(jié)點),就比如上面的9節(jié)點如果運氣夠好再往上建立一層節(jié)點,會是這樣的。


插入上層的時候注意所有節(jié)點要新建(拷貝),除了right的指向down的指向也不能忘記,down指向上一個節(jié)點可以用一個臨時節(jié)點作為前驅(qū)節(jié)點。如果層數(shù)突破當(dāng)前最高層,頭head節(jié)點(入口)需要改變。

代碼如下

public void add(SkipNode node) {int key=node.key;SkipNode findNode=search(key);if(findNode!=null)//如果存在這個key的節(jié)點{findNode.value=node.value;return;}Stack<SkipNode>stack=new Stack<SkipNode>();//存儲向下的節(jié)點,這些節(jié)點可能在右側(cè)插入節(jié)點SkipNode team=headNode;//查找待插入的節(jié)點 找到最底層的哪個節(jié)點。while (team!=null) {//進(jìn)行查找操作 if(team.right==null)//右側(cè)沒有了,只能下降{stack.add(team);//將曾經(jīng)向下的節(jié)點記錄一下team=team.down;}else if(team.right.key>key)//需要下降去尋找{stack.add(team);//將曾經(jīng)向下的節(jié)點記錄一下team=team.down;}else //向右{team=team.right;}}int level=1;//當(dāng)前層數(shù),從第一層添加(第一層必須添加,先添加再判斷)SkipNode downNode=null;//保持前驅(qū)節(jié)點(即down的指向,初始為null)while (!stack.isEmpty()) {//在該層插入nodeteam=stack.pop();//拋出待插入的左側(cè)節(jié)點SkipNode nodeTeam=new SkipNode(node.key, node.value);//節(jié)點需要重新創(chuàng)建nodeTeam.down=downNode;//處理豎方向downNode=nodeTeam;//標(biāo)記新的節(jié)點下次使用if(team.right==null) {//右側(cè)為null 說明插入在末尾team.right=nodeTeam;}//水平方向處理else {//右側(cè)還有節(jié)點,插入在兩者之間nodeTeam.right=team.right;team.right=nodeTeam;}//考慮是否需要向上if(level>MAX_LEVEL)//已經(jīng)到達(dá)最高級的節(jié)點啦break;double num=random.nextDouble();//[0-1]隨機(jī)數(shù)if(num>0.5)//運氣不好結(jié)束break;level++;if(level>highLevel)//比當(dāng)前最大高度要高但是依然在允許范圍內(nèi) 需要改變head節(jié)點{highLevel=level;//需要創(chuàng)建一個新的節(jié)點SkipNode highHeadNode=new SkipNode(Integer.MIN_VALUE, null);highHeadNode.down=headNode;headNode=highHeadNode;//改變headstack.add(headNode);//下次拋出head}} }


小結(jié)

對于上面,跳表完整分析就結(jié)束啦,當(dāng)然,你可能看到不同品種跳表的實現(xiàn),還有的用數(shù)組方式表示上下層的關(guān)系這樣也可以,但本文只定義right和down兩個方向的鏈表更純正化的講解跳表。

對于跳表以及跳表的同類競爭產(chǎn)品:紅黑樹,為啥Redis的有序集合(zset) 使用跳表呢?因為跳表除了查找插入維護(hù)和紅黑樹有著差不多的效率,它是個鏈表,能確定范圍區(qū)間,而區(qū)間問題在樹上可能就沒那么方便查詢啦。

而JDK中跳躍表ConcurrentSkipListSet和ConcurrentSkipListMap。


完整Code

import java.util.Random; import java.util.Stack; class SkipNode<T> {int key;T value;SkipNode right,down;//左右上下四個方向的指針public SkipNode (int key,T value) {this.key=key;this.value=value;}} public class SkipList <T> {SkipNode headNode;//頭節(jié)點,入口int highLevel;//層數(shù)Random random;// 用于投擲硬幣final int MAX_LEVEL = 32;//最大的層SkipList(){random=new Random();headNode=new SkipNode(Integer.MIN_VALUE,null);highLevel=0;}public SkipNode search(int key) {SkipNode team=headNode;while (team!=null) {if(team.key==key){return team;}else if(team.right==null)//右側(cè)沒有了,只能下降{team=team.down;}else if(team.right.key>key)//需要下降去尋找{team=team.down;}else //右側(cè)比較小向右{team=team.right;}}return null;}public void delete(int key)//刪除不需要考慮層數(shù){SkipNode team=headNode;while (team!=null) {if (team.right == null) {//右側(cè)沒有了,說明這一層找到,沒有只能下降team=team.down;}else if(team.right.key==key)//找到節(jié)點,右側(cè)即為待刪除節(jié)點{team.right=team.right.right;//刪除右側(cè)節(jié)點team=team.down;//向下繼續(xù)查找刪除}else if(team.right.key>key)//右側(cè)已經(jīng)不可能了,向下{team=team.down;}else { //節(jié)點還在右側(cè)team=team.right;}}}public void add(SkipNode node){int key=node.key;SkipNode findNode=search(key);if(findNode!=null)//如果存在這個key的節(jié)點{findNode.value=node.value;return;}Stack<SkipNode>stack=new Stack<SkipNode>();//存儲向下的節(jié)點,這些節(jié)點可能在右側(cè)插入節(jié)點SkipNode team=headNode;//查找待插入的節(jié)點 找到最底層的哪個節(jié)點。while (team!=null) {//進(jìn)行查找操作if(team.right==null)//右側(cè)沒有了,只能下降{stack.add(team);//將曾經(jīng)向下的節(jié)點記錄一下team=team.down;}else if(team.right.key>key)//需要下降去尋找{stack.add(team);//將曾經(jīng)向下的節(jié)點記錄一下team=team.down;}else //向右{team=team.right;}}int level=1;//當(dāng)前層數(shù),從第一層添加(第一層必須添加,先添加再判斷)SkipNode downNode=null;//保持前驅(qū)節(jié)點(即down的指向,初始為null)while (!stack.isEmpty()) {//在該層插入nodeteam=stack.pop();//拋出待插入的左側(cè)節(jié)點SkipNode nodeTeam=new SkipNode(node.key, node.value);//節(jié)點需要重新創(chuàng)建nodeTeam.down=downNode;//處理豎方向downNode=nodeTeam;//標(biāo)記新的節(jié)點下次使用if(team.right==null) {//右側(cè)為null 說明插入在末尾team.right=nodeTeam;}//水平方向處理else {//右側(cè)還有節(jié)點,插入在兩者之間nodeTeam.right=team.right;team.right=nodeTeam;}//考慮是否需要向上if(level>MAX_LEVEL)//已經(jīng)到達(dá)最高級的節(jié)點啦break;double num=random.nextDouble();//[0-1]隨機(jī)數(shù)if(num>0.5)//運氣不好結(jié)束break;level++;if(level>highLevel)//比當(dāng)前最大高度要高但是依然在允許范圍內(nèi) 需要改變head節(jié)點{highLevel=level;//需要創(chuàng)建一個新的節(jié)點SkipNode highHeadNode=new SkipNode(Integer.MIN_VALUE, null);highHeadNode.down=headNode;headNode=highHeadNode;//改變headstack.add(headNode);//下次拋出head}}}public void printList() {SkipNode teamNode=headNode;int index=1;SkipNode last=teamNode;while (last.down!=null){last=last.down;}while (teamNode!=null) {SkipNode enumNode=teamNode.right;SkipNode enumLast=last.right;System.out.printf("%-8s","head->");while (enumLast!=null&&enumNode!=null) {if(enumLast.key==enumNode.key){System.out.printf("%-5s",enumLast.key+"->");enumLast=enumLast.right;enumNode=enumNode.right;}else{enumLast=enumLast.right;System.out.printf("%-5s","");}}teamNode=teamNode.down;index++;System.out.println();}}public static void main(String[] args) {SkipList<Integer>list=new SkipList<Integer>();for(int i=1;i<20;i++){list.add(new SkipNode(i,666));}list.printList();list.delete(4);list.delete(8);list.printList();} }

參考: https://www.toutiao.com/a6910597347328426503

總結(jié)

以上是生活随笔為你收集整理的每日一博 - 如何理解跳表(SkipList)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

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