數(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)存消耗因苹。跳表的代碼比起紅黑樹來說苟耻,要簡單、易讀扶檐。

鏈表加多級索引的結(jié)構(gòu)凶杖,就是跳表

對于一個單鏈表款筑,即便鏈表中存儲的數(shù)據(jù)是有序的智蝠,如果要查找某個數(shù)據(jù),也只能從頭到尾遍歷奈梳,時間復(fù)雜度是O(n)杈湾。

對于鏈表建立一級“索引”,沒兩個結(jié)點提取一個結(jié)點到上一級攘须,把抽出來的那一級叫做索引索引層漆撞。圖中的 down表示down指針,指向下一級結(jié)點于宙。

這樣就可以先在索引層遍歷浮驳,然后通過索引層結(jié)點的down指針,下降到原始鏈表這一層捞魁,繼續(xù)遍歷抹恳。

比如要查找16,當(dāng)在索引層遍歷到13時署驻,發(fā)現(xiàn)索引層下一個結(jié)點是17大于目標(biāo)16奋献,則可從13的down指針下降到原始鏈表繼續(xù)遍歷健霹。這樣只需要再遍歷2個結(jié)點,就可以找到值等于16的這個結(jié)點了瓶蚂。原來查找16糖埋,需要遍歷10個結(jié)點,加入一層索引后只需要遍歷7個結(jié)點窃这。

加上一層索引之后瞳别,查找一個結(jié)點需要遍歷的結(jié)點個數(shù)減少了,查找效率提高了杭攻。繼續(xù)再加一級索引祟敛,在第一級索引的基礎(chǔ)之上,每兩個結(jié)點就抽出一個結(jié)點到第二級索引≌捉猓現(xiàn)在再查找16馆铁,只需要遍歷6個結(jié)點了,需要遍歷的結(jié)點數(shù)量又減少了锅睛。

下圖是一個包含64個結(jié)點的鏈表埠巨,建立五級索引。

在五級索引的作用下现拒,查找62只需要遍歷11個結(jié)點辣垒。當(dāng)鏈表的長度n比較大時,比如1000印蔬、10000的時候勋桶,在構(gòu)建多級索引之后,查找效率的提升就會非常明顯侥猬。

跳表的時間復(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 = logn - 1。加上原始鏈表這一層运褪,整個跳表的高度就是logn惊楼。在跳表中查詢某個數(shù)據(jù)的時候玖瘸,如果每一層都要遍歷m個結(jié)點,那在跳表中查詢一個數(shù)據(jù)的時間復(fù)雜度就是O(m * logn)檀咙。

按照每兩個結(jié)點提取一個結(jié)點到上一級建立索引這種數(shù)據(jù)結(jié)構(gòu)雅倒,每一級索引都最多只需要遍歷3個結(jié)點,那么m = 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é)點。

所以在跳表中查詢?nèi)我鈹?shù)據(jù)的時間復(fù)雜度就是O(logn)膳算。

跳表的空間復(fù)雜度分析

假設(shè)原始鏈表大小為n座硕,那第一級索引大約有n / 2個結(jié)點,第二級索引大約有n / 4個結(jié)點涕蜂,以此類推华匾,每上升一級就減少一半,直到剩下2個結(jié)點机隙。每層索引的結(jié)點數(shù)為:
n / 2, n / 4, n / 8, ..., 8, 4, 2
這幾級索引的結(jié)點總和就是 n / 2 + n / 4 + n / 8 + ... + 8 + 4 + 2 = n - 2蜘拉。所以跳表的空間復(fù)雜度是O(n)。

將包含n個結(jié)點的單鏈表構(gòu)造成跳表有鹿,需要額外再用接近n個結(jié)點的存儲空間旭旭。

如果每三個結(jié)點或五個結(jié)點,抽一個結(jié)點到上級索引:

那第一級索引需要大約n / 3個結(jié)點葱跋,第二級索引需要大約n / 9個結(jié)點持寄。每往上一級,索引結(jié)點個數(shù)都除以3娱俺。為了方便計算稍味,假設(shè)最高一級的索引結(jié)點個數(shù)是1。每3個結(jié)點抽一個荠卷,每層索引的結(jié)點數(shù)為:
n / 3, n / 9, n / 27, ..., 9, 3, 1
總的索引結(jié)點個數(shù)為n / 3 + n / 9 + n / 27 + ...+ 9 + 3 + 1 = n / 2模庐。空間復(fù)雜度依然是O(n)油宜,但比每兩個結(jié)點抽一個結(jié)點的索引構(gòu)建方法掂碱,減少了一半的索引結(jié)點存儲空間怜姿。

在實際的軟件開發(fā)中,原始鏈表中存儲的有可能是很大的對象顶吮,而索引結(jié)點只需要存儲關(guān)鍵值和幾個指針社牲,并不需要存儲對象,所以當(dāng)對象比索引結(jié)點大很多時悴了,那索引占用的額外空間就可以忽略了搏恤。

跳表動態(tài)的插入和刪除

跳表插入、刪除操作的時間復(fù)雜度是O(logn)湃交。

對于刪除操作熟空,如果這個結(jié)點在索引中也有出現(xiàn),刪除原始鏈表中的結(jié)點之后還要刪除對應(yīng)的索引搞莺。

查找要刪除的結(jié)點的時候息罗,一定要獲取前驅(qū)結(jié)點。(雙向鏈表不需要考慮這個問題)

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

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

作為一種動態(tài)數(shù)據(jù)結(jié)構(gòu),需要某種手段來維護(hù)索引與原始鏈表大小之間的平衡:

如果鏈表中結(jié)點多了岁歉,索引結(jié)點就相應(yīng)地增加一些得运,避免復(fù)雜度退化,以及查找锅移、插入熔掺、刪除操作性能下降。

往跳表中插入數(shù)據(jù)的時候非剃,可以同時將這個數(shù)據(jù)插入到部分索引層中置逻。通過一個隨機函數(shù),來決定將這個結(jié)點插入到哪幾級索引中备绽,比如隨機函數(shù)生成了值k券坞,就將這個結(jié)點添加到第一級到第k級索引中。

Redis用跳表實現(xiàn)有序集合

Redis中的有序集合是通過跳表來實現(xiàn)的疯坤,嚴(yán)格點講报慕,其實還用到了散列表深浮。

Redis中的有序集合支持的核心操作主要有:

  • 插入一個數(shù)據(jù)压怠;
  • 刪除一個數(shù)據(jù);
  • 查找一個數(shù)據(jù)飞苇;
  • 按照區(qū)間查找數(shù)據(jù)菌瘫;
  • 迭代輸出有序序列蜗顽。

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

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

跳表相對紅黑樹而言代碼更容易實現(xiàn)古沥,簡單就意味著可讀性好瘸右,不容易出錯。還有岩齿,跳表更加靈活太颤,它可以通過改變索引構(gòu)建策略,有效平衡執(zhí)行效率和內(nèi)存消耗纯衍。

跳表的簡易代碼實現(xiàn)

public class SkipList {

  private static final float SKIPLIST_P = 0.5f;
  private static final int MAX_LEVEL = 16;

  private int levelCount = 1;

  private Node cls = new Node();  // 帶頭鏈表

  public Node find(int value) {
    Node p = cls;
    for (int i = levelCount - 1; i >= 0; --i) {
      while (p.forwards[i] != null && p.forwards[i].data < value) {
        p = p.forwards[i];
      }
    }

    if (p.forwards[0] != null && p.forwards[0].data == value) {
      return p.forwards[0];
    } else {
      return null;
    }
  }

  public void insert(int value) {
    int level = randomLevel();
    Node newNode = new Node();
    newNode.data = value;
    newNode.maxLevel = level;
    Node update[] = new Node[level];
    for (int i = 0; i < level; ++i) {
      update[i] = cls;
    }

    // record every level largest value which smaller than insert value in update[]
    Node p = cls;
    for (int i = level - 1; i >= 0; --i) {
      while (p.forwards[i] != null && p.forwards[i].data < value) {
        p = p.forwards[i];
      }
      update[i] = p;// use update save node in search path
    }

    // in search path node next node become new node forwords(next)
    for (int i = 0; i < level; ++i) {
      newNode.forwards[i] = update[i].forwards[i];
      update[i].forwards[i] = newNode;
    }

    // update node hight
    if (levelCount < level) levelCount = level;
  }

  public void delete(int value) {
    Node[] update = new Node[levelCount];
    Node p = cls;
    for (int i = levelCount - 1; i >= 0; --i) {
      while (p.forwards[i] != null && p.forwards[i].data < value) {
        p = p.forwards[i];
      }
      update[i] = p;
    }

    if (p.forwards[0] != null && p.forwards[0].data == value) {
      for (int i = levelCount - 1; i >= 0; --i) {
        if (update[i].forwards[i] != null && update[i].forwards[i].data == value) {
          update[i].forwards[i] = update[i].forwards[i].forwards[i];
        }
      }
    }

    while (levelCount>1&&cls.forwards[levelCount]==null){
      levelCount--;
    }

  }

  // 理論來講栋齿,一級索引中元素個數(shù)應(yīng)該占原始數(shù)據(jù)的 50%,二級索引中元素個數(shù)占 25%襟诸,三級索引12.5% 瓦堵,一直到最頂層。
  // 因為這里每一層的晉升概率是 50%歌亲。對于每一個新插入的節(jié)點菇用,都需要調(diào)用 randomLevel 生成一個合理的層數(shù)。
  // 該 randomLevel 方法會隨機生成 1~MAX_LEVEL 之間的數(shù)陷揪,且 :
  //        50%的概率返回 1
  //        25%的概率返回 2
  //      12.5%的概率返回 3 ...
  private int randomLevel() {
    int level = 1;

    while (Math.random() < SKIPLIST_P && level < MAX_LEVEL)
      level += 1;
    return level;
  }

  public void printAll() {
    Node p = cls;
    while (p.forwards[0] != null) {
      System.out.print(p.forwards[0] + " ");
      p = p.forwards[0];
    }
    System.out.println();
  }

  public class Node {
    private int data = -1;
    private Node forwards[] = new Node[MAX_LEVEL];
    private int maxLevel = 0;

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("{ data: ");
      builder.append(data);
      builder.append("; levels: ");
      builder.append(maxLevel);
      builder.append(" }");

      return builder.toString();
    }
  }

}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末惋鸥,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子悍缠,更是在濱河造成了極大的恐慌卦绣,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件飞蚓,死亡現(xiàn)場離奇詭異滤港,居然都是意外死亡,警方通過查閱死者的電腦和手機趴拧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門溅漾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來山叮,“玉大人,你說我怎么就攤上這事添履∑ň螅” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵暮胧,是天一觀的道長锐借。 經(jīng)常有香客問我,道長往衷,這世上最難降的妖魔是什么瞎饲? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮炼绘,結(jié)果婚禮上嗅战,老公的妹妹穿的比我還像新娘。我一直安慰自己俺亮,他們只是感情好驮捍,可當(dāng)我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著脚曾,像睡著了一般东且。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上本讥,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天珊泳,我揣著相機與錄音,去河邊找鬼拷沸。 笑死色查,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的撞芍。 我是一名探鬼主播秧了,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼序无!你這毒婦竟也來了验毡?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤帝嗡,失蹤者是張志新(化名)和其女友劉穎晶通,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哟玷,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡狮辽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片隘竭。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖讼渊,靈堂內(nèi)的尸體忽然破棺而出动看,到底是詐尸還是另有隱情,我是刑警寧澤爪幻,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布菱皆,位于F島的核電站,受9級特大地震影響挨稿,放射性物質(zhì)發(fā)生泄漏仇轻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一奶甘、第九天 我趴在偏房一處隱蔽的房頂上張望篷店。 院中可真熱鬧,春花似錦臭家、人聲如沸疲陕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蹄殃。三九已至,卻和暖如春你踩,著一層夾襖步出監(jiān)牢的瞬間诅岩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工带膜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吩谦,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓膝藕,卻偏偏與公主長得像逮京,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子束莫,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,573評論 2 353