數(shù)據(jù)結(jié)構(gòu)與算法——跳表

什么是跳表

跳表全稱為跳躍列表褒颈,它允許快速查詢,插入和刪除一個(gè)有序連續(xù)元素的數(shù)據(jù)鏈表励堡。跳躍列表的平均查找和插入時(shí)間復(fù)雜度都是O(logn)谷丸。快速查詢是通過維護(hù)一個(gè)多層次的鏈表应结,且每一層鏈表中的元素是前一層鏈表元素的子集(見右邊的示意圖)刨疼。一開始時(shí),算法在最稀疏的層次進(jìn)行搜索鹅龄,直至需要查找的元素在該層兩個(gè)相鄰的元素中間揩慕。這時(shí),算法將跳轉(zhuǎn)到下一個(gè)層次扮休,重復(fù)剛才的搜索漩绵,直到找到需要查找的元素為止。

image

一張?zhí)S列表的示意圖肛炮。每個(gè)帶有箭頭的框表示一個(gè)指針, 而每行是一個(gè)稀疏子序列的鏈表止吐;底部的編號(hào)框(黃色)表示有序的數(shù)據(jù)序列。查找從頂部最稀疏的子序列向下進(jìn)行, 直至需要查找的元素在該層兩個(gè)相鄰的元素中間侨糟。

跳表的演化過程

對(duì)于單鏈表來說碍扔,即使數(shù)據(jù)是已經(jīng)排好序的,想要查詢其中的一個(gè)數(shù)據(jù)秕重,只能從頭開始遍歷鏈表不同,這樣效率很低,時(shí)間復(fù)雜度很高,是 O(n)二拐。
那我們有沒有什么辦法來提高查詢的效率呢服鹅?我們可以為鏈表建立一個(gè)“索引”,這樣查找起來就會(huì)更快百新,如下圖所示企软,我們?cè)谠兼湵淼幕A(chǔ)上,每?jī)蓚€(gè)結(jié)點(diǎn)提取一個(gè)結(jié)點(diǎn)建立索引饭望,我們把抽取出來的結(jié)點(diǎn)叫做索引層或者索引仗哨,down 表示指向原始鏈表結(jié)點(diǎn)的指針。

image

現(xiàn)在如果我們想查找一個(gè)數(shù)據(jù)铅辞,比如說 15厌漂,我們首先在索引層遍歷,當(dāng)我們遍歷到索引層中值為 14 的結(jié)點(diǎn)時(shí)斟珊,我們發(fā)現(xiàn)下一個(gè)結(jié)點(diǎn)的值為 17苇倡,所以我們要找的 15 肯定在這兩個(gè)結(jié)點(diǎn)之間。這時(shí)我們就通過 14 結(jié)點(diǎn)的 down 指針囤踩,回到原始鏈表旨椒,然后繼續(xù)遍歷,這個(gè)時(shí)候我們只需要再遍歷兩個(gè)結(jié)點(diǎn)高职,就能找到我們想要的數(shù)據(jù)钩乍。好我們從頭看一下辞州,整個(gè)過程我們一共遍歷了 7 個(gè)結(jié)點(diǎn)就找到我們想要的值怔锌,如果沒有建立索引層,而是用原始鏈表的話变过,我們需要遍歷 10 個(gè)結(jié)點(diǎn)埃元。

通過這個(gè)例子我們可以看出來,通過建立一個(gè)索引層媚狰,我們查找一個(gè)基點(diǎn)需要遍歷的次數(shù)變少了岛杀,也就是查詢的效率提高了。

那么如果我們給索引層再加一層索引呢崭孤?遍歷的結(jié)點(diǎn)會(huì)不會(huì)更少呢类嗤,效率會(huì)不會(huì)更高呢?我們?cè)囋嚲椭懒恕?/p>

image

現(xiàn)在我們?cè)賮聿檎?15辨宠,我們從第二級(jí)索引開始遗锣,最后找到 15,一共遍歷了 6 個(gè)結(jié)點(diǎn)嗤形,果然效率更高精偿。

當(dāng)然,因?yàn)槲覀兣e的這個(gè)例子數(shù)據(jù)量很小,所以效率提升的不是特別明顯笔咽,如果數(shù)據(jù)量非常大的時(shí)候搔预,我們多建立幾層索引,效率提升的將會(huì)非常的明顯叶组,感興趣的可以自己試一下拯田,這里我們就不舉例子了。

這種通過對(duì)鏈表加多級(jí)索引的機(jī)構(gòu)扶叉,就是跳表了勿锅。

跳表具體有多快

通過上邊的例子我們知道,跳表的查詢效率比鏈表高枣氧,那具體高多少呢溢十?下面我們一起來看一下。

衡量一個(gè)算法的效率我們可以用時(shí)間復(fù)雜度达吞,這里我們也用時(shí)間復(fù)雜度來比較一下鏈表和跳表张弛。前面我們已經(jīng)講過了,鏈表的查詢的時(shí)間復(fù)雜度為 O(n)酪劫,那跳表的呢吞鸭?

如果一個(gè)鏈表有 n 個(gè)結(jié)點(diǎn),如果每?jī)蓚€(gè)結(jié)點(diǎn)抽取出一個(gè)結(jié)點(diǎn)建立索引的話覆糟,那么第一級(jí)索引的結(jié)點(diǎn)數(shù)大約就是 n/2刻剥,第二級(jí)索引的結(jié)點(diǎn)數(shù)大約為 n/4,以此類推第 m 級(jí)索引的節(jié)點(diǎn)數(shù)大約為 n/(2^m)滩字。

假如一共有 m 級(jí)索引造虏,第 m 級(jí)的結(jié)點(diǎn)數(shù)為兩個(gè),通過上邊我們找到的規(guī)律麦箍,那么得出 n/(2^m)=2漓藕,從而求得 m=log(n)-1。如果加上原始鏈表挟裂,那么整個(gè)跳表的高度就是 log(n)享钞。我們?cè)诓樵兲淼臅r(shí)候,如果每一層都需要遍歷 k 個(gè)結(jié)點(diǎn)诀蓉,那么最終的時(shí)間復(fù)雜度就為 O(k*log(n))栗竖。

那這個(gè) k 值為多少呢,按照我們每?jī)蓚€(gè)結(jié)點(diǎn)提取一個(gè)基點(diǎn)建立索引的情況渠啤,我們每一級(jí)最多需要遍歷兩個(gè)個(gè)結(jié)點(diǎn)狐肢,所以 k=2。為什么每一層最多遍歷兩個(gè)結(jié)點(diǎn)呢埃篓?

因?yàn)槲覀兪敲績(jī)蓚€(gè)結(jié)點(diǎn)提取一個(gè)結(jié)點(diǎn)建立索引处坪,最高一級(jí)索引只有兩個(gè)結(jié)點(diǎn),然后下一層索引比上一層索引兩個(gè)結(jié)點(diǎn)之間增加了一個(gè)結(jié)點(diǎn),也就是上一層索引兩結(jié)點(diǎn)的中值同窘,看到這里是不是想起來我們前邊講過的二分查找玄帕,每次我們只需要判斷要找的值在不在當(dāng)前結(jié)點(diǎn)和下一個(gè)結(jié)點(diǎn)之間即可。


image

如上圖所示想邦,我們要查詢紅色結(jié)點(diǎn)裤纹,我們查詢的路線即黃線表示出的路徑查詢,每一級(jí)最多遍歷兩個(gè)結(jié)點(diǎn)即可丧没。

所以跳表的查詢?nèi)我鈹?shù)據(jù)的時(shí)間復(fù)雜度為 O(2*log(n))鹰椒,前邊的常數(shù) 2 可以忽略,為 O(log(n))呕童。

跳表是用空間來?yè)Q時(shí)間

跳表的效率比鏈表高了漆际,但是跳表需要額外存儲(chǔ)多級(jí)索引,所以需要的更多的內(nèi)存空間夺饲。

跳表的空間復(fù)雜度分析并不難奸汇,如果一個(gè)鏈表有 n 個(gè)結(jié)點(diǎn),如果每?jī)蓚€(gè)結(jié)點(diǎn)抽取出一個(gè)結(jié)點(diǎn)建立索引的話往声,那么第一級(jí)索引的結(jié)點(diǎn)數(shù)大約就是 n/2擂找,第二級(jí)索引的結(jié)點(diǎn)數(shù)大約為 n/4,以此類推第 m 級(jí)索引的節(jié)點(diǎn)數(shù)大約為 n/(2^m)浩销,我們可以看出來這是一個(gè)等比數(shù)列贯涎。

這幾級(jí)索引的結(jié)點(diǎn)總和就是 n/2+n/4+n/8…+8+4+2=n-2,所以跳表的空間復(fù)雜度為 o(n)慢洋。

那么我們有沒有辦法減少索引所占的內(nèi)存空間呢塘雳?可以的,我們可以每三個(gè)結(jié)點(diǎn)抽取一個(gè)索引且警,或者沒五個(gè)結(jié)點(diǎn)抽取一個(gè)索引粉捻。這樣索引結(jié)點(diǎn)的數(shù)量減少了礁遣,所占的空間也就少了斑芜。

跳表的插入和刪除

我們想要為跳表插入或者刪除數(shù)據(jù),我們首先需要找到插入或者刪除的位置祟霍,然后執(zhí)行插入或刪除操作杏头,前邊我們已經(jīng)知道了,跳表的查詢的時(shí)間復(fù)雜度為 O(logn)沸呐,因?yàn)檎业轿恢弥蟛迦牒蛣h除的時(shí)間復(fù)雜度很低醇王,為 O(1),所以最終插入和刪除的時(shí)間復(fù)雜度也為 O(longn)崭添。

我么通過圖看一下插入的過程寓娩。


image

刪除操作的話,如果這個(gè)結(jié)點(diǎn)在索引中也有出現(xiàn),我們除了要?jiǎng)h除原始鏈表中的結(jié)點(diǎn)棘伴,還要?jiǎng)h除索引中的寞埠。因?yàn)閱捂湵碇械膭h除操作需要拿到要?jiǎng)h除結(jié)點(diǎn)的前驅(qū)結(jié)點(diǎn),然后通過指針操作完成刪除焊夸。所以在查找要?jiǎng)h除的結(jié)點(diǎn)的時(shí)候仁连,一定要獲取前驅(qū)結(jié)點(diǎn)。當(dāng)然阱穗,如果我們用的是雙向鏈表饭冬,就不需要考慮這個(gè)問題了。

如果我們不停的向跳表中插入元素揪阶,就可能會(huì)造成兩個(gè)索引點(diǎn)之間的結(jié)點(diǎn)過多的情況昌抠。結(jié)點(diǎn)過多的話,我們建立索引的優(yōu)勢(shì)也就沒有了鲁僚。所以我們需要維護(hù)索引與原始鏈表的大小平衡扰魂,也就是結(jié)點(diǎn)增多了,索引也相應(yīng)增加蕴茴,避免出現(xiàn)兩個(gè)索引之間結(jié)點(diǎn)過多的情況劝评,查找效率降低。

跳表是通過一個(gè)隨機(jī)函數(shù)來維護(hù)這個(gè)平衡的倦淀,當(dāng)我們向跳表中插入數(shù)據(jù)的的時(shí)候蒋畜,我們可以選擇同時(shí)把這個(gè)數(shù)據(jù)插入到索引里,那我們插入到哪一級(jí)的索引呢撞叽,這就需要隨機(jī)函數(shù)姻成,來決定我們插入到哪一級(jí)的索引中。

這樣可以很有效的防止跳表退化愿棋,而造成效率變低科展。

跳表的代碼實(shí)現(xiàn)

最后我們來看一下跳變用代碼怎么實(shí)現(xiàn)。

package skiplist;

import java.util.Random;

/**
 * 跳表的一種實(shí)現(xiàn)方法糠雨。
 * 跳表中存儲(chǔ)的是正整數(shù)才睹,并且存儲(chǔ)的是不重復(fù)的。
 */
public class SkipList {

  private static final int MAX_LEVEL = 16;

  private int levelCount = 1;

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

  private Random r = new Random();

  public Node find(int value) {
    Node p = head;
    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] = head;
    }

    // record every level largest value which smaller than insert value in update[]
    Node p = head;
    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 = head;
    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];
        }
      }
    }
  }

  // 隨機(jī) level 次甘邀,如果是奇數(shù)層數(shù) +1琅攘,防止偽隨機(jī)
 private int randomLevel() {
    int level = 1;
    for (int i = 1; i < MAX_LEVEL; ++i) {
      if (r.nextInt() % 2 == 1) {
        level++;
      }
    }

    return level;
  }

  public void printAll() {
    Node p = head;
    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)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市松邪,隨后出現(xiàn)的幾起案子坞琴,更是在濱河造成了極大的恐慌,老刑警劉巖逗抑,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剧辐,死亡現(xiàn)場(chǎng)離奇詭異寒亥,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)荧关,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門护盈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人羞酗,你說我怎么就攤上這事腐宋。” “怎么了檀轨?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵胸竞,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我参萄,道長(zhǎng)卫枝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任讹挎,我火速辦了婚禮校赤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘筒溃。我一直安慰自己马篮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布怜奖。 她就那樣靜靜地躺著浑测,像睡著了一般。 火紅的嫁衣襯著肌膚如雪歪玲。 梳的紋絲不亂的頭發(fā)上迁央,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音滥崩,去河邊找鬼岖圈。 笑死,一個(gè)胖子當(dāng)著我的面吹牛钙皮,可吹牛的內(nèi)容都是我干的蜂科。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼株灸,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼崇摄!你這毒婦竟也來了擎值?” 一聲冷哼從身側(cè)響起慌烧,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鸠儿,沒想到半個(gè)月后屹蚊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體厕氨,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年汹粤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了命斧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嘱兼,死狀恐怖国葬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情芹壕,我是刑警寧澤汇四,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站踢涌,受9級(jí)特大地震影響通孽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜睁壁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一背苦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧潘明,春花似錦行剂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至牲阁,卻和暖如春固阁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背城菊。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工备燃, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人凌唬。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓并齐,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親客税。 傳聞我的和親對(duì)象是個(gè)殘疾皇子况褪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容