程序員,你應該知道的數據結構之跳表


跳表的原理

跳表也叫跳躍表,是一種動態的數據結構。如果我們需要在有序鏈表中進行查找某個值,需要遍歷整個鏈表,二分查找對鏈表不支持,二分查找的底層要求為數組,遍歷整個鏈表的時間復雜度為O(n)。我們可以把鏈表改造成B樹、紅黑樹、AVL樹等數據結構來提升查詢效率,但是B樹、紅黑樹、AVL樹這些數據結構實現起來非常復雜,里面的細節也比較多。跳表就是為了提升有序鏈表的查詢速度產生的一種動態數據結構,跳表相對B樹、紅黑樹、AVL樹這些數據結構實現起來比較簡單,但時間復雜度與B樹、紅黑樹、AVL樹這些數據結構不相上下,時間復雜度能夠達到O(logn)。

如何理解跳表?

有序鏈表
要理解跳表,我們先從有序鏈表開始,對於上圖中的有序鏈表,我們要查找值為150的節點,我們需要遍歷到鏈表的最后一個元素才能找到,用大O表示時間復雜度就為O(n)。時間復雜度比較高,有沒有優化空間呢?答案是肯定的,我們采取空間換時間的概念,對鏈表抽取出索引層,比如每兩個元素抽取一個元素構成新的有序鏈表,這樣索引層的有序鏈表相對底層的鏈表來說長度更短、間距更大,改造之后的鏈表入下圖所示:

跳表

我們在新的鏈表中查詢150,先從最上層Level3開始查找,遍歷Level3有序鏈表,遍歷兩次就查詢到了150這個節點。相對於初始鏈表來說,改造后的鏈表對查詢效率有了不少的提升。

改造后的鏈表就是跳表,跳表采用的是以空間換時間的概念,將有序鏈表隨機抽出好幾層有序鏈表,查找的時候先從最上層開始,然后一直下沉到最底層鏈表。

現在我們對跳表有了一定的認識,總結一下跳表的性質:

  • 由很多層結構組成
  • 每一層都是一個有序的鏈表
  • 最底層(Level 1)的鏈表包含所有元素
  • 如果一個元素出現在 Level i 的鏈表中,則它在 Level i 之下的鏈表也都會出現。

跳表的實現

跳表一般使用單鏈表來實現,這樣比較節約空間。我使用雙向鏈表來實現跳表,因為雙向鏈表相對單向鏈表來說比較容易理解跳表的實現。

鏈表定義

// 頭節點
private final static byte HEAD_NODE = (byte) -1;
// 數據節點
private final static byte DATA_NODE = (byte) 0;
// 尾節點
private final static byte TAIL_NODE = (byte) -1;


private static class Node{
    private Integer value;
    // 上下左右節點
    private Node up,down,left,right;
    private byte bit;

    public Node(Integer value,byte bit){
        this.value = value;
        this.bit = bit;

    }
    public Node(Integer value){
        this(value,DATA_NODE);
    }

}

跳表的搜索

跳表的搜索從最上層開始查詢,最開始的節點p指向最頂層的head節點,如果p.right.value小於等於帶查詢元素,則p指向p.right,否則指向p.down。直到p.down為空,返回該節點。

跳表查找示意圖:

1)p=headhead.right.value=15,15<30,繼續往右邊查找,p=head.right

2)p.right.value=150150 > 30,往下一層查找,p=p.down

3)p.right.value=3030=30,繼續往右邊查找,p=p.right

4)p.right.value=150150 > 30,往下一層查找,p=p.down

5)p.right.value=5050 > 30,由於p.down為空,所以就返回該節點

代碼實現

/**
 * 通用查找
 * 查找出最后一個小於該元素或者等於元素的節點
 * @param element 待查找的數
 * @return
 */
private Node find(Integer element){
    Node current = head;
    for (;;){
        while (current.right.bit !=TAIL_NODE && current.right.value <= element){
            current = current.right;
        }

        if (current.down !=null){// 找到最底層節點
            current = current.down;
        }else{
            break;
        }
    }
    return current;// current.value <= element <current.right.value
}
/**
 * 獲取某個元素
 * @param element 
 * @return
 */
public Integer get(Integer element){
    Node node = this.find(element);
    return (node.value==element)?node.value:null;
}

跳表的插入

理解了跳表的查找后,在來看跳表的插入就相對來說比較簡單,我們先找出最后一個小於等於插入值的節點,我們將值插入到該節點的右邊。除了最底層鏈表上的插入外,每個新插入的節點都會有一個隨機的層高,我們還需要維護層高之間的節點關系,因為新節點的左邊第一個元素不一定有層高,所以我們要往左邊查找到第一個node.up不為空的節點,維護節點之間的關系,然后將節點作為新的節點,繼續往上加層。

我們以值55,層高為2的新節點為例,演示索引建立的過程。

1)我們先找出最后一個小於或者等於插入值的節點,即圖中的node節點,然后將新節點插入到node的右邊,即newnode節點。

2)node=50時,node.up為空,所以node = node.left

3)node=30時,node.up不為空,此時node=node.up

4)此時node已經移到level230節點上,在node的右邊建立indexNode節點,作為新插入值的lebel2層的索引

5)將indexNode節點作為新的newnode節點繼續向上添加索引,由於我們新插入節點的層高為2,所以將結束循環,新增元素完畢。

代碼實現

/**
 * 添加元素
 * @param element
 */
public void add(Integer element){
    //查找出前一個節點
    Node node = this.find(element);
    // 新建節點
    Node newNode = new Node(element);
    // 新節點與前后兩節點建立關系
    newNode.left = node;
    newNode.right = node.right;
    node.right.left = newNode;
    node.right = newNode;

    int currentLevel = 1;
    // 建立索引層,隨機建立層高
    while (random.nextDouble() < 0.5d){

        // 索引大於當前索引層
        if (currentLevel >= level){
            level++;
            // 最頂層索引的頭指針
            Node topHead = new Node(null,HEAD_NODE);
            // 最頂層索引的尾指針
            Node topTail = new Node(null,TAIL_NODE);

            topHead.right = topTail;
            topHead.down = head;
            head.up = topHead;
            topTail.left = topHead;
            topTail.down = tail;
            tail.up = topTail;
            head = topHead;
            tail = topTail;
        }
        // 一直往左邊找到索引層
        while (node !=null && node.up == null){
            node = node.left;
        }
        node = node.up;
        // 新建索引節點,與當前左邊節點建立關系
        Node indexNode = new Node(element);
        indexNode.left = node;
        indexNode.right = node.right;
        indexNode.down = newNode;
        node.right.left = indexNode;
        node.right = indexNode;
        newNode.up = indexNode;
        // 將索引節點作為新的節點,繼續往上建立索引(如果有索引的話)
        newNode = indexNode;
        currentLevel++;
        // 當前層高大於最高層高時,跳出循環
        if (currentLevel > MAX_LEVEL) break;
    }
    size ++;
}

跳表的刪除

跳表的刪除就比較簡單,找到該節點,從最底層開始刪除,直到node.up為空。

代碼實現

/**
 * 刪除跳表
 * @param element 待刪除的元素
 */
public void delete(Integer element){
    Node node = this.find(element);
    // 沒有找到該元素,直接返回
    if (node.value != element) return;
    // 刪除元素,將元素的左右鏈表建立關系
    node.left.right = node.right;
    node.right.left = node.left;
    // 判斷是否有索引層,
    while (node.up !=null){
        node.up.left.right = node.up.right;
        node.up.right.left = node.up.left;
        node = node.up;
    }

}

跳表的時間復雜度

對於有序鏈表來說,時間復雜度為O(n),我們將鏈表改造成跳表之后,時間復雜度為多少呢?首先來分析對於有n個節點的鏈表,需要建立多少級索引?如果我們每兩個節點會提取一個節點作為一個索引節點,那么第一級索引節點的個數為n/2,第二級索引節點的個數為n/4,依此類推,則第K級的索引節點的個數為n/(2^k)。

假設索引有h級,且第h級的索引節點個數為2,如下圖所示。則我們可以得出n/(2^h)=2,這樣可以得到h=logn-1(這里的log是指以2為底),加上鏈表本身的一層,則整個跳表的高度為logn。我們在跳表中查詢某個數據時,如果每一層都需要遍歷m個節點,那么在跳表中查詢某個數的時間復雜度為O(m*log(n))。

跳表的空間復雜度

比起單純的單鏈表,跳表需要存儲多級索引,肯定要消耗更多的存儲空間。我們來看看跳表的空間復雜度,假設原始鏈表大小為 n,每兩個元素直接提取一個索引,那第一級索引大約有 n/2 個結點,第二級索引大約有 n/4 個結點,以此類推,每上升一級就減少一半,直到剩下 2 個結點。如果我們把每層索引的結點數寫出來,就是一個等比數列。這幾級索引的結點總和就是 n/2+n/4+n/8…+8+4+2=n-2。所以,跳表的空間復雜度是 O(n)。也就是說,如果將包含 n 個結點的單鏈表構造成跳表,我們需要額外再用接近 n 個結點的存儲空間。

以上就是跳表相關的知識,我使用雙向鏈表來實現跳表,相對來說能夠更好的理解跳表的思想,在使用是用單鏈表實現就可以了,單鏈表比雙鏈表節約空間。

示例代碼:GitHub

如果您發現文中錯誤,還請多多指教。歡迎關注個人公眾號,一起交流學習。

最后

打個小廣告,金九銀十跳槽季,平頭哥給大家整理了一份較全面的 Java 學習資料,歡迎掃碼關注微信公眾號:「平頭哥的技術博文」領取,祝各位升職加薪。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM