日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

 

前言

在頭條創作了一個月左右的時間,收獲了50+粉絲,很是開心,我會把數據結構與算法的文章更新到底,第一次看我文章的同仁如果覺得不錯的話就關注一下我哦,你的支持就是我創作的動力。

 

一、什么是跳表

我們在之前介紹了十分優秀的二分查找算法,但是它只能作用于有序數組上,查找起來比較方便,但是數組中插入和刪除元素是比較麻煩的;那么有沒有辦法讓二分查找也能作用于有序鏈表上,從而達到查找、插入和刪除元素都十分的快速呢?

對于普通的有序列表來說,當然不能實現我們的目標,如下查找的時間復雜度為O(n);

原始有序單鏈表

我們可以基于原始鏈表建立一個 索引層,比如每兩個節點提取一個節點到索引層:

帶一級索引的跳表

如此,兩種數據結構我們查找元素16的比較次數分別為10次和8次,確實能提高查詢速度;我們更近一步,再次建立第二級索引:

帶二級索引的跳表 帶二級索引的跳表

此時查找元素16比較的次數只需要比較7次即可;

如果在大數據量的有序鏈表中,我們建立很多層索引,使得最高層索引只有兩個節點,那么就實現了類似二分查找的算法思想,此時這種數據結構就被成為跳表。

二、跳表性能分析

2.1 時間復雜度

假設有序鏈表總節點個數為n,我們建立索引時每兩個節點提取一個索引節點;那么第一級索引共有n/2個節點;

第k級索引共有(n/2^k)個節點,假設k為最高級索引,為2個節點,那么2=(n/2^k),n=2(k+1),k=logn-1,如果原始鏈表也算進去的話,k=logn正好是整個數據結構的高度。

假設每一層需要遍歷m個節點,那么時間復雜度就可以表示為O(mlogn),但是推斷m是常量級別的,因此可以忽略,那么整個查找過程時間復雜度就是O(logn),竟然和二分查找是一樣的高效!

2.2 空間復雜度

第一級索引需要n/2個節點,第二級需要n/2^2個節點,依次類推,第k級索引需要的節點個數為n/2^k,這正好是一個等比數列,等比數列求和的結果是n-2,所以空間復雜度為O(n),這是一個以空間換時間的策略;

我們可以每3個節點或者每4個節點往上提取一個索引節點,如此可以適當減少需要的額外空間,但是空間復雜度仍然為O(n);如果有序鏈表存儲的是大對象,那么索引節點中無需存放整個大對象,只要存儲對象的指針即可,所以此時空間復雜度就顯得不那么重要了;

2.3 跳表的插入和刪除

跳表因為是順序鏈表,所以真正插入和刪除的時間復雜度都是O(1),但是找到需要插入節點的位置或者找到待刪除的節點時間復雜度為O(logn);

跳表在刪除的時候,除了需要刪除原始有序鏈表中的節點,還需要同步刪除k級索引中的全部該索引節點;

跳表在插入元素式,極端情況下會導致兩個索引節點中存在大量的原始節點,時間效率極有可能會退化為單鏈表的O(n),所以需要動態地平衡和更新k級索引節點;

三、跳表使用場景

redis在存儲有序集合的時候就用到了跳表+散列表的數據結構,跳表和紅黑樹相比,插入、刪除、查找的時間復雜度相同,但是跳表在按照區間查找時明顯具有效率優勢,而且跳表實現起來比紅黑樹要簡單易懂,不容易出錯。

紅黑樹等常用數據結構在程序語言中都是封裝好的,我們想用的話直接拿來用即可,比如HashMap,但是跳表卻沒有對應的封裝好的數據結構,想用的話開發者必須自己去實現。

四、代碼實現跳表Skiplist以及優化

代碼來源于極客時間《數據結構和算法之美》

github地址:
https://github.com/wangzheng0822/algo(數據結構和算法必知必會的50個代碼實現)

4.1 作者王爭給出的跳表實現方式

/**
 * 跳表的一種實現方法。
 * 跳表中存儲的是正整數,并且存儲的是不重復的。
 *
 */
public class SkipList {

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

  private int levelCount = 1;

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

  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];
        }
      }
    }

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

  }

  // 理論來講,一級索引中元素個數應該占原始數據的 50%,二級索引中元素個數占 25%,三級索引12.5% ,一直到最頂層。
  // 因為這里每一層的晉升概率是 50%。對于每一個新插入的節點,都需要調用 randomLevel 生成一個合理的層數。
  // 該 randomLevel 方法會隨機生成 1~MAX_LEVEL 之間的數,且 :
  //        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 = 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();
    }
  }

}

4.2 作者ldb基于王爭的代碼給出的優化

import JAVA.util.Random;

/**
 * 1,跳表的一種實現方法,用于練習。跳表中存儲的是正整數,并且存儲的是不重復的。
 * 2,本類是參考作者zheng ,自己學習,優化了添加方法
 * 3,看完這個,我覺得再看ConcurrentSkipListMap 源碼,會有很大收獲
 */
public class SkipList2 {

    private static final int MAX_LEVEL = 16;
    private int levelCount = 1;

    /**
     * 帶頭鏈表
     */
    private Node head = new Node(MAX_LEVEL);
    private Random r = new Random();

    public Node find(int value) {
        Node p = head;
        // 從最大層開始查找,找到前一節點,通過--i,移動到下層再開始查找
        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;
        }
    }

    /**
     * 優化了作者zheng的插入方法
     *
     * @param value 值
     */
    public void insert(int value) {
        int level = head.forwards[0] == null ? 1 : randomLevel();
        // 每次只增加一層,如果條件滿足
        if (level > levelCount) {
            level = ++levelCount;
        }
        Node newNode = new Node(level);
        newNode.data = value;
        Node update[] = new Node[level];
        for (int i = 0; i < level; ++i) {
            update[i] = head;
        }

        Node p = head;
        // 從最大層開始查找,找到前一節點,通過--i,移動到下層再開始查找
        for (int i = levelCount - 1; i >= 0; --i) {
            while (p.forwards[i] != null && p.forwards[i].data < value) {
                // 找到前一節點
                p = p.forwards[i];
            }
            // levelCount 會 > level,所以加上判斷
            if (level > i) {
                update[i] = p;
            }

        }
        for (int i = 0; i < level; ++i) {
            newNode.forwards[i] = update[i].forwards[i];
            update[i].forwards[i] = newNode;
        }

    }

    /**
     * 優化了作者zheng的插入方法2
     *
     * @param value 值
     */
    public void insert2(int value) {
        int level = head.forwards[0] == null ? 1 : randomLevel();
        // 每次只增加一層,如果條件滿足
        if (level > levelCount) {
            level = ++levelCount;
        }
        Node newNode = new Node(level);
        newNode.data = value;
        Node p = head;
        // 從最大層開始查找,找到前一節點,通過--i,移動到下層再開始查找
        for (int i = levelCount - 1; i >= 0; --i) {
            while (p.forwards[i] != null && p.forwards[i].data < value) {
                // 找到前一節點
                p = p.forwards[i];
            }
            // levelCount 會 > level,所以加上判斷
            if (level > i) {
                if (p.forwards[i] == null) {
                    p.forwards[i] = newNode;
                } else {
                    Node next = p.forwards[i];
                    p.forwards[i] = newNode;
                    newNode.forwards[i] = next;
                }
            }

        }

    }

    /**
     * 作者zheng的插入方法,未優化前,優化后參見上面insert()
     *
     * @param value
     * @param level 0 表示隨機層數,不為0,表示指定層數,指定層數
     *              可以讓每次打印結果不變動,這里是為了便于學習理解
     */
    public void insert(int value, int level) {
        // 隨機一個層數
        if (level == 0) {
            level = randomLevel();
        }
        // 創建新節點
        Node newNode = new Node(level);
        newNode.data = value;
        // 表示從最大層到低層,都要有節點數據
        newNode.maxLevel = level;
        // 記錄要更新的層數,表示新節點要更新到哪幾層
        Node update[] = new Node[level];
        for (int i = 0; i < level; ++i) {
            update[i] = head;
        }

        /**
         *
         * 1,說明:層是從下到上的,這里最下層編號是0,最上層編號是15
         * 2,這里沒有從已有數據最大層(編號最大)開始找,(而是隨機層的最大層)導致有些問題。
         *    如果數據量為1億,隨機level=1 ,那么插入時間復雜度為O(n)
         */
        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]表示當前層節點的前一節點,因為要找到前一節點,才好插入數據
            update[i] = p;
        }

        // 將每一層節點和后面節點關聯
        for (int i = 0; i < level; ++i) {
            // 記錄當前層節點后面節點指針
            newNode.forwards[i] = update[i].forwards[i];
            // 前一個節點的指針,指向當前節點
            update[i].forwards[i] = newNode;
        }

        // 更新層高
        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];
                }
            }
        }
    }

    /**
     * 隨機 level 次,如果是奇數層數 +1,防止偽隨機
     *
     * @return
     */
    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 void printAll_beautiful() {
        Node p = head;
        Node[] c = p.forwards;
        Node[] d = c;
        int maxLevel = c.length;
        for (int i = maxLevel - 1; i >= 0; i--) {
            do {
                System.out.print((d[i] != null ? d[i].data : null) + ":" + i + "-------");
            } while (d[i] != null && (d = d[i].forwards)[i] != null);
            System.out.println();
            d = c;
        }
    }

    /**
     * 跳表的節點,每個節點記錄了當前節點數據和所在層數數據
     */
    public class Node {
        private int data = -1;
        /**
         * 表示當前節點位置的下一個節點所有層的數據,從上層切換到下層,就是數組下標-1,
         * forwards[3]表示當前節點在第三層的下一個節點。
         */
        private Node forwards[];

        /**
         * 這個值其實可以不用,看優化insert()
         */
        private int maxLevel = 0;

        public Node(int level) {
            forwards = new Node[level];
        }

        @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();
        }
    }

    public static void main(String[] args) {
        SkipList2 list = new SkipList2();
        list.insert(1, 3);
        list.insert(2, 3);
        list.insert(3, 2);
        list.insert(4, 4);
        list.insert(5, 10);
        list.insert(6, 4);
        list.insert(8, 5);
        list.insert(7, 4);
        list.printAll_beautiful();
        list.printAll();
        /**
         * 結果如下:
         * null:15-------
         * null:14-------
         * null:13-------
         * null:12-------
         * null:11-------
         * null:10-------
         * 5:9-------
         * 5:8-------
         * 5:7-------
         * 5:6-------
         * 5:5-------
         * 5:4-------                    
         * 8:4-------
         * 4:3-------5:3-------6:3-------7:3-------8:3-------
         * 1:2-------2:2-------          4:2-------5:2-------6:2-------7:2-------8:2-------
         * 1:1-------2:1-------3:1-------4:1-------5:1-------6:1-------7:1-------8:1-------
         * 1:0-------2:0-------3:0-------4:0-------5:0-------6:0-------7:0-------8:0-------
         * { data: 1; levels: 3 } { data: 2; levels: 3 } { data: 3; levels: 2 } { data: 4; levels: 4 }
         * { data: 5; levels: 10 } { data: 6; levels: 4 } { data: 7; levels: 4 } { data: 8; levels: 5 }
         */
        // 優化后insert()

        SkipList2 list2 = new SkipList2();
        list2.insert2(1);
        list2.insert2(2);
        list2.insert2(6);
        list2.insert2(7);
        list2.insert2(8);
        list2.insert2(3);
        list2.insert2(4);
        list2.insert2(5);
        System.out.println();
        list2.printAll_beautiful();


    }

}

分享到:
標簽:數據結構
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定