数据结构之跳表

Posted 前端手艺人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之跳表相关的知识,希望对你有一定的参考价值。

什么是跳表

链表加多级索引。 
跳表能够实现二分查找的效率。

代码实现

图1:

注意:
1 节点1虽然看着是有三个,本质上是只有一个。为了便于看三个层级的关系,才多画了两个。(也是很多网上的图示没解释清楚)
2 层级之间的跳跃是通过节点里的数组(也就是forward)存放的指针,forward[0]是1层头指针,forward[1]是2层头指针...(很多书上都会有一个down指针,导致理解偏差)
java:

package skiplist;/**只处理了整数,且无重复元素**/public class SkipList {
    private static final int MAX_LEVEL = 16;
    // 当前的索引层级
    private int levelCount = 1;
    // 头链表: forwards分别指向每一个层级的头:插入的时候会自动更新
    private Node head = new Node();
    private Random r = new Random();

    public void printAll() {
        Node p =head;
        while(p.forwards[0] != null) {
            if(i==0) {
                System.out.println("头节点的forward指向");
            }else {
                System.out.println("第"+i+"个节点的forward指向");
            }
            System.out.print(p.forwards[0] + " ");
            if(p.forwards[1] != null) {
                System.out.print(p.forwards[1] + " ");
            }
            if(p.forwards[2]!= null) {
                System.out.print(p.forwards[2] + " ");
            }
            if(p.forwards[3]!= null) {
                System.out.print(p.forwards[3] + " ");
            }
            if(p.forwards[4]!= null) {
                System.out.print(p.forwards[4] + " ");
            }
            if(p.forwards[5]!= null) {
                System.out.print(p.forwards[5] + " ");
            }
            if(p.forwards[6]!= null) {
                System.out.print(p.forwards[6] + " ");
            }
            p = p.forwards[0];
            i++;
            System.out.println();
        }
        System.out.println();
    }

    // 结合图1看
    public Node find(int value) {
        Node p = head;
        // 注意这里的forwards是next p还是指向自己
        for(int i =levelCount-1; i>=0; i--){
            while(p.forwards[i]!=null && p.forwards[i].data<value) {
                p = p.forwards[i];
            }
        }
        // 找得节点一定是在原始链表的那一层(逻辑上有多层,本质上节点只有一层)。所以forwards[0](包含了所有的节点)
        if(p.forwards[0]!=null && p.forwards[0].data==value) {
            return p.forwards[0];
        }else {
            return null;
        }
    }

    public void insert(int value) {
        // 通过随机函数,插入部分索引层级,维持跳表的平衡性(避免两个节点之间的没有索引,导致查找退化为链表,时间复杂度变为O(n))
        int level = randomLevel();
        // 注意每一个索引层相同的节点,其实是同一个节点。
        // 所以这里新节点只有一个,而不是level个
        Node newNode = new Node();
        newNode.data = value;
        // 记录该节点的所在最大层级
        newNode.maxLevel = level;

        // 创建了4个node内存,保存了4个节点
        Node update[] = new Node[level];
        for(int i=0;i<level;i++) {
            update[i]=head;
        }

        Node p = head;
        // level必须减1 因为我们的forwards是0对应层级1
        for(int i=level-1; i>=0; i--) {
            while(p.forwards[i]!=null && p.forwards[i].data < value) {
                p = p.forwards[i];
            }
            // 指向要插入位置的前一个节点(第一次插入的时候,就是head节点)
            update[i] = p;
        }
        // update[i]是要插入位置的上一个节点
        // 修改node在每个层级的指针指向
        for(int i =0; i<level; i++) {
            newNode.forwards[i] = update[i].forwards[i];
            // 注意: 当插入该层第一个元素的时候,会更新头结点(update[i]就是头结点)
            update[i].forwards[i] = newNode;
        }

        // 更新当前的索引层级
        if (levelCount < level) levelCount = level;
    }

    public void delete(int value) {
        // 每一级一个update
        Node[] update = new Node[levelCount];
        Node p = head;
        // 从最顶索引往下遍历,update指向上一个位置
        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];
                }
            }
        }
    }

    private int randomLevel() {
        int level = 1;
        for(int i=1; i < MAX_LEVEL;i++) {
            if(r.nextInt()%2 == 1) {
                level++;
            }
        }
        return level;
    }

    public class Node {
        private int data = -1;
        // 注意几点: 最大层级包含原始链表,原始链表为层级1
        // p.forwards[层级值] 当前层级的前向指针:forwards[层级值]相当于next
        // p.forwards[层级值-1] 指向下一层级的指针: forwards[层级值-1]相当于down
        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();
        }
    }}

javascript:

const MAX_LEVEL = 4;function Node() {
  this.forwards = new Array(MAX_LEVEL);
  this.data = null;
  this.maxlevel = 0;}function SkipList() {
  this.levelCount = 1;
  this.head = new Node();}SkipList.prototype.find = function(value) {
  var p = this.head;
  for (var i = this.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;
  }};SkipList.prototype.insert = function(value) {
  var level = Math.floor(Math.random() * MAX_LEVEL + 1);
  var newNode = new Node();
  newNode.data = value;
  newNode.maxlevel = level;

  // js数组是动态创建内存的
  var update = [];
  for (var i = 0; i < level; i++) {
    update[i] = this.head;
  }

  var p = this.head;
  for (var i = level - 1; i >= 0; i--) {
    while (p.forwards[i] != null && p.forwards[i].data < value) {
      p = p.forwards[i];
    }
    update[i] = p;
  }
  // console.log(JSON.stringify(update));
  for (var i = 0; i < level; i++) {
    newNode.forwards[i] = update[i].forwards[i];
    update[i].forwards[i] = newNode;
  }
  if (this.levelCount < this.level) this.levelCount = this.level;};SkipList.prototype.delete = function(value) {
  var update = [];
  var p = head;
  for (var i = this.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 (var i = this.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];
      }
    }
  }};SkipList.prototype.printAll = function() {
  var p = this.head;
  console.log("print all....");
  while (p.forwards[0] != null) {
    console.log(JSON.stringify(p.forwards[0], null, 4) + " ");
    p = p.forwards[0];
  }};var a = new SkipList();a.insert(123);a.insert("aaa");a.printAll();var result = a.find(123);console.log("打印结果");JSON.stringify(result, null, 4);

查询时间复杂度

O(logn)
每两个节点抽出一个节点作为上一层索引的节点,假设最高级的索引有两个节点,n/2^k=2 => 推出k=logn-1 索引的层数,加上链表这一层,高度为logn,而每一层最多遍历三个节点,则时间复杂度为3*logn

空间复杂度

n/2+n/4+n/8+...+2=n-2 =》 O(n)
如果每三个抽出一个节点,则空间复杂度会更少。 在实际情况中, 链表中可能存的是一个大对象,而索引通常只存储关键值和几个指针。

支持快速插入和删除

很显然,查找的时间复杂度是O(logn) 链表中删除和插入操作时间复杂度都是O(1)
根据乘法法则,时间复杂度为O(logn)

跳表索引动态更新

如果不断的插入,而不更新索引,则跳表可能退化成链表
通过随机函数,决定节点同时插入到哪几级索引中

作业

为什么Redis要用跳表而不是红黑树来实现有序集合?
首先我们要看有序集合支持哪几个操作
1 插入 
2 删除 
3 查找
4 根据区间查找数据->红黑树的效率没有跳表高
5 迭代输出有序区间
很多编程语言中的Map都是红黑树实现,跳表需要自己实现

作业

如果每三个抽出一个节点,时间复杂度是多少?


点击阅读原文,关注作者可以获得持续的后续文章更新。

以上是关于数据结构之跳表的主要内容,如果未能解决你的问题,请参考以下文章

程序员,你应该知道的数据结构之跳表

Redis源码之跳表数据结构

SkipList 跳表

数据结构与算法之深入解析“设计跳表”的求解思路与算法示例

SkipList跳表基本原理

每日一题1206. 设计跳表