之後有面試官問你跳躍表,你就把這篇文章扔給他

假如咱們要用某種數據結構來維護一組有序的int型數據的集合,而且但願這個數據結構在插入、刪除、查找等操做上可以儘量着快速,那麼,你會用什麼樣的數據結構呢?redis

數組

一種很簡單的方法應該就是採用數組了,在查找方面,用數組存儲的話,採用二分法能夠在 O(logn) 的時間裏找到指定的元素,不過數組在插入、刪除這些操做中比較不友好,找到目標位置所需時間爲 O(logn) ,進行插入和刪除這個動做所需的時間複雜度爲 O(n) ,由於都須要移動移動元素,因此最終所須要的時間複雜度爲 O(n) 。 例如對於下面這個數組:數組

插入元素 3bash

鏈表

另一種簡單的方法應該就是用鏈表了,鏈表在插入、刪除的支持上就相對友好,當咱們找到目標位置以後,插入、刪除元素所需的時間複雜度爲 O(1) ,注意,我說的是找到目標位置以後,插入、刪除的時間複雜度才爲O(1)。數據結構

但鏈表在查找上就不友好了,不能像數組那樣採用二分查找的方式,只能一個一個結點遍歷,因此加上查找所需的時間,插入、刪除所需的總的時間複雜度爲O(n)。dom

假如咱們可以提升鏈表的查找效率,使鏈表的查找的時間複雜度儘量接近 O(logn) ,那鏈表將會是很棒的選擇。學習

提升鏈表的查找速度

那鏈表的查找速度能夠提升嗎?測試

對於下面這個鏈表ui

假如咱們要查找元素9,按道理咱們須要從頭結點開始遍歷,一共遍歷8個結點才能找到元素9。可否採起某些策略,讓咱們遍歷5次之內就找到元素9呢?請你們花一分鐘時間想一下如何實現?this

因爲元素的有序的,咱們是能夠經過增長一些路徑來加快查找速度的。例如spa

經過這種方法,咱們只須要遍歷5次就能夠找到元素9了(紅色的線爲查找路徑)。

還能繼續加快查找速度嗎?

答是能夠的,再增長一層就好了,這樣只須要4次就能找到了,這就如同咱們搭地鐵的時候,去某個站點時,有快線和慢線幾種路線,經過快線 + 慢線的搭配,咱們能夠更快着到達某個站點。

固然,還能在增長一層,

基於這種方法,對於具備 n 個元素的鏈表,咱們能夠採起 ** (logn + 1) 層指針路徑的形式**,就能夠實如今 O(logn) 的時間複雜度內,查找到某個目標元素了,這種數據結構,咱們也稱之爲跳躍表,跳躍表也能夠算是鏈表的一種變形,只是它具備二分查找的功能。

插入與刪除

上面例子中,9個結點,一共4層,能夠說是理想的跳躍表了,不過隨着咱們對跳躍表進行插入/刪除結點的操做,那麼跳躍表結點數就會改變,意味着跳躍表的層數也會動態改變。

這裏咱們面臨一個問題,就是新插入的結點應該跨越多少層?

這個問題已經有大牛替咱們解決好了,採起的策略是經過拋硬幣來決定新插入結點跨越的層數:每次咱們要插入一個結點的時候,就來拋硬幣,若是拋出來的是正面,則繼續拋,直到出現負面爲止,統計這個過程當中出現正面的次數,這個次數做爲結點跨越的層數。

經過這種方法,能夠儘量着接近理想的層數。你們能夠想一下爲啥會這樣呢?

插入

例如,咱們要插入結點 3,4,經過拋硬幣知道3,4跨越的層數分別爲 0,2 (層數從0開始算),則插入的過程以下:

插入 3,跨越0層。

插入 4,跨越2層。

刪除

解決了插入以後,咱們來看看刪除,刪除就比較簡單了,例如咱們要刪除4,那咱們直接把4及其所跨越的層數刪除就好了。

小結

跳躍表的插入與刪除至此都講完了,總結下跳躍表的有關性質:

(1). 跳躍表的每一層都是一條有序的鏈表.

(2). 跳躍表的查找次數近似於層數,時間複雜度爲O(logn),插入、刪除也爲 O(logn)。

(3). 最底層的鏈表包含全部元素。

(4). 跳躍表是一種隨機化的數據結構(經過拋硬幣來決定層數)。

(5). 跳躍表的空間複雜度爲 O(n)。

跳躍表 vs 二叉查找樹

有人可能會說,也能夠採用二叉查找樹啊,由於查找查找樹的插入、刪除、查找也是近似 O(logn) 的時間複雜度。

不過,二叉查找樹是有可能出現一種極端的狀況的,就是若是插入的數據恰好一直有序,那麼全部節點會偏向某一邊。例如

這種接結構會致使二叉查找樹的查找效率變爲 O(n),這會使二叉查找樹大打折扣。

跳躍表 vs 紅黑樹

紅黑能夠說是二叉查找樹的一種變形,紅黑在查找,插入,刪除也是近似O(logn)的時間複雜度,但學過紅黑樹的都知道,紅黑樹比跳躍表複雜多了,反正我是被紅黑樹虐過。在選擇一種數據結構時,有時候也是須要考慮學習成本的。

並且紅黑樹插入,刪除結點時,是經過調整結構來保持紅黑樹的平衡,比起跳躍表直接經過一個隨機數來決定跨越幾層,在時間複雜度的花銷上是要高於跳躍表的。

固然,紅黑樹並非必定比跳躍表差,在有些場合紅黑樹會是更好的選擇,因此選擇一種數據結構,關鍵還得看場合。

總上所述,維護一組有序的集合,而且但願在查找、插入、刪除等操做上儘量快,那麼跳躍表會是不錯的選擇。redis 中的數據數據即是採用了跳躍表,固然,ridis也結合了哈希表等數據結構,採用的是一種複合數據結構。

代碼以下

package skiplist;

//節點
class Node{
    int value = -1;
    int level;//跨越幾層
    Node[] next;//指向下一個節點

    public Node(int value, int level) {
        this.value = value;
        this.level = level;
        this.next = new Node[level];
    }
}
//跳躍表
public class SkipList {
    //容許的最大層數
    int maxLevel = 16;
    //頭節點,充當輔助。
    Node head = new Node(-1, 16);
    //當前跳躍表節點的個數
    int size = 0;
    //當前跳躍表的層數,初始化爲1層。
    int levelCount = 1;


    public Node find(int value) {
        Node temp = head;
        for (int i = levelCount - 1; i >= 0; i--) {
            while (temp.next[i] != null && temp.next[i].value < value) {
                temp = temp.next[i];
            }
        }
        //判斷是否有該元素存在
        if (temp.next[0] != null && temp.next[0].value == value) {
            System.out.println(value + " 查找成功");
            return temp.next[0];
        } else {
            return null;
        }
    }
    // 爲了方便,跳躍表在插入的時候,插入的節點在當前跳躍表是不存在的
    //不容許插入重複數值的節點。
    public void insert(int value) {
        int level = getLevel();
        Node newNode = new Node(value, level);
        //update用於記錄要插入節點的前驅
        Node[] update = new Node[level];

        Node temp = head;
        for (int i = level - 1; i >= 0; i--) {
            while (temp.next[i] != null && temp.next[i].value < value) {
                temp = temp.next[i];
            }
            update[i] = temp;
        }
        //把插入節點的每一層鏈接起來
        for (int i = 0; i < level; i++) {
            newNode.next[i] = update[i].next[i];
            update[i].next[i] = newNode;
        }
        //判斷是否須要更新跳躍表的層數
        if (level > levelCount) {
            levelCount = level;
        }
        size++;
        System.out.println(value + " 插入成功");
    }

    public void delete(int value) {
        Node[] update = new Node[levelCount];
        Node temp = head;

        for (int i = levelCount - 1; i >= 0; i--) {
            while (temp.next[i] != null && temp.next[i].value < value) {
                temp = temp.next[i];
            }
            update[i] = temp;
        }

        if (temp.next[0] != null && temp.next[0].value == value) {
            size--;
            System.out.println(value + " 刪除成功");
            for (int i = levelCount - 1; i >= 0; i--) {
                if (update[i].next[i] != null && update[i].next[i].value == value) {
                    update[i].next[i] = update[i].next[i].next[i];
                }
            }
        }
    }

    //打印全部節點
    public void printAllNode() {
        Node temp = head;
        while (temp.next[0] != null) {
            System.out.println(temp.next[0].value + " ");
            temp = temp.next[0];
        }
    }

    //模擬拋硬幣
    private int getLevel() {
        int level = 1;
        while (true) {
            int t = (int)(Math.random() * 100);
            if (t % 2 == 0) {
                level++;
            } else {
                break;
            }
        }
        System.out.println("當前的level = " + level);
        return level;
    }

    //測試數據
    public static void main(String[] args) {
        SkipList list = new SkipList();
        for (int i = 0; i < 6; i++) {
            list.insert(i);
        }
        list.printAllNode();
        list.delete(4);
        list.printAllNode();
        System.out.println(list.find(3));
        System.out.println(list.size + " " + list.levelCount);
    }
}

複製代碼

若是你以爲不錯,不妨點個贊讓更多人看到這篇文章,感激涕零。

最後推廣下個人公衆號:苦逼的碼農,文章都會首發於個人公衆號,公衆號已有100多篇原創文章,期待各路英雄的關注交流。

相關文章
相關標籤/搜索