一:跳表的简介:

跳表(SkipList,全称跳跃表)是用于有序元素序列快速搜索查找的一个数据结构,跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。它在性能上和红黑树,AVL树不相上下,但是跳表的原理非常简单,实现也比红黑树简单很多。

二:Java代码实现:

1:跳表节点设计:

类似于Map的设计方法,采用k-v键值对的方式进行,在Redis中Zset的Score就是类似于这样的原理;

class SkipNode<T>{

     int key;

     T value;
     /**
      * 规定节点key值从小到大,每一个节点存储下面的节点和右边的节点
      */
     SkipNode<T> down, right;

     public SkipNode(int key, T value){
         this.key = key;
         this.value = value;
     }

 }

2:跳表的设计:

public class SkipList<T> {

    /**
     * 跳表的头节点
     */
    SkipNode head;

    /**
     * 链表的层数
     */
    int height;

    /**
     * 所允许的链表最大层数
     */
    final int MAX_HEIGHT = 30;

    /**
     * 有1/2的概率新增节点时向上扩充一个节点
     */
    final double ENABLE_ADD_SKIP_NODE = 0.5f;

    /**
     * 产生 0 ~ 1 之间的随机浮点数,用于判断是否向上扩充节点
     */
    Random random;

    /**
     * 打印次数
     */
    int printNums;

    public SkipList(){
        head = new SkipNode(Integer.MIN_VALUE, null);
        height = 0;
        random = new Random();
        printNums = 0;
    }
}

3:查询节点方法实现:

很多时候链表也可能这样相连仅仅是某个元素或者key作为有序的标准。所以有可能链表内部存在一些value。不过修改和查询其实都是一个操作,找到关键数字(key)。并且查找的流程也很简单,设置一个临时节点temp=head。当temp不为null其流程大致如下:

  • 从temp节点出发,如果当前节点的key与查询的key相等,那么返回当前节点(如果是修改操作那么一直向下进行修改值即可)。
  • 如果key不相等,且右侧为null,那么证明只能向下(结果可能出现在下右方向),此时temp=temp.down
  • 如果key不相等,且右侧不为null,且右侧节点key小于待查询的key。那么说明同级还可向右,此时temp=temp.right
  • (否则的情况)如果key不相等,且右侧不为null,且右侧节点key大于待查询的key 。那么说明如果有结果的话就在这个索引和下个索引之间,此时temp=temp.down。

最终将按照这个步骤返回正确的节点或者null(说明没查到)。

代码如下:

/**
* 查询方法
  * @param key
  * @return
  */
 public SkipNode find(int key){
     SkipNode temp = this.head;
     while (temp != null){
     	 //找到key直接返回
         if (temp.key == key){
             return temp;
         }
         //如果右边不存在或者右边的key大于查找到key则往下查找,因为我们默认key从小到大
         if (temp.right == null || temp.right.key > key){
             temp = temp.down;
         }else {
             temp = temp.right;
         }
     }
     return null;
 }

4:删除节点方法实现:

删除需要改变链表结构所以需要处理好节点之间的联系。对于删除操作需要注意以下几点:

  • 删除当前节点和这个节点的前后节点都有关系
  • 删除当前层节点之后,下一层该key的节点也要删除,一直删除到最底层

对于这两点分析:如果找到当前节点了,它的前面一个节点怎么查找呢?我们可以不直接判断和操作节点,先找到待删除节点的左侧节点。通过这个节点即可完成删除,然后这个节点直接向下去找下一层待删除的左侧节点。设置一个临时节点temp=head,当temp不为null具体循环流程为:

  • 如果temp右侧为null,那么temp=temp.down(之所以敢直接这么判断是因为左侧有头结点在左侧,不用担心特殊情况)
  • 如果temp右侧不 为null,并且右侧的key等于待删除的key,那么先删除节点,再temp向下temp=temp.down为了删除下层节点。
  • 如果temp右侧不 为null,并且右侧key小于待删除的key,那么temp向右temp=temp.right。
  • 如果temp右侧不 为null,并且右侧key大于待删除的key,那么temp向下temp=temp.down,在下层继续查找删除节点。

代码如下:

/**
  * 删除方法
  * @param key
  * @return
  */
 public boolean delete(int key){
     SkipNode temp = this.head;
     //记录是否删除成功
     boolean isDelete = false;
     while (temp != null){
         if (temp.right == null || temp.right.key > key){
             temp = temp.down;
             //如果右边的节点key为删除的key,则进行删除,使得当前节点的right指针指向要删除节点的右指针
         }else if (temp.right.key == key){
             temp.right = temp.right.right;
             isDelete = true;
             temp = temp.down;
         }else {
             temp = temp.right;
         }
     }
     return isDelete;
 }

5:插入节点方法实现:

插入需要考虑是否插入索引,插入几层等问题。我们使用随机化的方法去判断是否向上层插入索引。即产生一个[0~1]的随机数如果小于等于0.5就向上插入索引,插入完毕后再次使用随机数判断是否向上插入索引。运气好这个值可能是多层索引,运气不好只插入最底层(这是100%插入的)。但是索引也不能不限制高度,我们一般会设置索引最高值如果大于这个值就不往上继续添加索引了。主要流程如下:

  • 首先通过上面查找的方式,找到待插入的左节点。插入的话最底层肯定是需要插入的,所以通过链表插入节点(需要考虑是否为末尾节点)
  • 插入完这一层,需要考虑上一层是否插入,首先判断当前索引层级,如果大于最大值那么就停止(比如已经到最高索引层了)。否则设置一个随机数1/2的概率向上插入一层索引(因为理想状态下的就是每2个向上建一个索引节点)。
  • 继续(2)的操作,直到概率退出或者索引层数大于最大索引层。

在具体向上插入的时候,实质上还有非常重要的细节需要考虑。首先如何找到上层的待插入节点 ?

这个各个实现方法可能不同,如果有左、上指向的指针那么可以向左向上找到上层需要插入的节点,但是如果只有右指向和下指向的我们也可以借助查询过程中记录下降的节点。因为曾经下降的节点倒序就是需要插入的节点,最底层也不例外(因为没有匹配值会下降为null结束循环)。在这里我使用栈这个数据结构进行存储。此外还有一点需要注意,如果这个节点添加的索引突破当前最高层,跳表的head需要改变了,这时需要新建一个节点作为新的head,将它的down指向老head,将这个head节点加入栈中(也就是这个节点作为下次后面要插入的节点)。

代码如下:

/**
* 插入方法
 * @param node
 */
public void insert(SkipNode node){
    if (node == null){
        return;
    }
    SkipNode findNode = this.find(node.key);
    //如果查到了,相当于修改,覆盖原来的节点的值
    if (findNode != null){
        findNode.value = node.value;
        return;
    }
    //记录一路下来的指针,这些指针如果向上扩充节点的话需要修改其右指针
    Stack<SkipNode> skipDownNodeStack = new Stack<>();
    SkipNode temp = this.head;
    while (temp != null){
    	//记录需head节点到要插入的位置一路向下需要的指针,向右则不需要
        if (temp.right == null || temp.right.key > node.key){
            skipDownNodeStack.add(temp);
            temp = temp.down;
        }else {
            temp = temp.right;
        }
    }
    //记录上一层节点的down指针需要指向的节点,最开始时null,因为处于最下面一层
    SkipNode downNode = null;
    int nowHeight = 0;
    while (!skipDownNodeStack.empty()){
    	//此时弹出的是插入位置的左侧节点
        SkipNode popSkipNode = skipDownNodeStack.pop();
        //新插入的节点
        SkipNode insertSkipNode = new SkipNode(node.key, node.value);
		//新插入的节点的下指针指向上一个新插入的节点
        insertSkipNode.down = downNode;
        
        //插入操作
        insertSkipNode.right = popSkipNode.right;
        popSkipNode.right = insertSkipNode;

		//更新下指针为当前插入的指针,于是下一次插入的指针指向当前指针
        downNode = insertSkipNode;
        //判断是否还继续向上插入新的指针
        if (this.random.nextDouble() > this.ENABLE_ADD_SKIP_NODE || ++nowHeight > this.MAX_HEIGHT){
            break;
        }
        //如果插入的新指针层数已经高于当前层数了,则需要更新头节点head
        if (nowHeight > this.height){
            this.height = nowHeight;
            SkipNode newHeadSkipNode = new SkipNode(Integer.MIN_VALUE, null);
            newHeadSkipNode.down = this.head;
            //新的头指针也需要指向最高层新增的节点
            skipDownNodeStack.add(newHeadSkipNode);
            this.head = newHeadSkipNode;
        }
    }
}

三:测试:

1:简单测试代码如下:

public static void main(String[] args) {
    SkipList<Integer> list = new SkipList<Integer>();
    long b = System.currentTimeMillis();
    for(int i = 1; i <= 20; i++) {
        list.insert(new SkipList<Integer>().new SkipNode<Integer>(i, i));
    }
    list.printSkipList();
    list.delete(3);
    list.delete(6);
    list.printSkipList();
}

运行结果如下:

java里的跳表 跳表 java_数据结构

简单的性能测试代码如下:

public static void main(String[] args) {
    SkipList<Integer> list = new SkipList<Integer>();
    long b = System.currentTimeMillis();
    for(int i = 1; i <= 1000000; i++) {
        list.insert(new SkipList<Integer>().new SkipNode<Integer>(i, i));
    }
    long e = System.currentTimeMillis();
    System.out.println("耗时:" + (double)(e - b) / 1000);

    b = System.currentTimeMillis();
    for(int i = 1; i < 1000000; i++) {
        Object value = list.find(i).value;
        if (i % 2 == 1){
            list.delete(i);
        }
        if (i > 999980){
            System.out.println("find key = " + i + " value = " + value);
        }
    }
    e = System.currentTimeMillis();
    System.out.println("耗时:" + (double)(e - b) / 1000);
}

运行结果如下:

java里的跳表 跳表 java_算法_02


可以看出100w条记录的插入只用了0.453s,100w次操作也才只用了0.154s,效率极其高;

四:完整代码如下:

import java.util.Random;
import java.util.Stack;

public class SkipList<T> {

    /**
     * 跳表的头节点
     */
    SkipNode head;

    /**
     * 链表的层数
     */
    int height;

    /**
     * 所允许的链表最大层数
     */
    final int MAX_HEIGHT = 30;

    /**
     * 有1/2的概率新增节点时向上扩充一个节点
     */
    final double ENABLE_ADD_SKIP_NODE = 0.5f;

    /**
     * 产生 0 ~ 1 之间的随机浮点数,用于判断是否向上扩充节点
     */
    Random random;

    /**
     * 打印次数
     */
    int printNums;

    public SkipList(){
        head = new SkipNode(Integer.MIN_VALUE, null);
        height = 0;
        random = new Random();
        printNums = 0;
    }


    class SkipNode<T>{

        int key;

        T value;
        /**
         * 规定节点key值从小到大
         */
        SkipNode<T> down, right;

        public SkipNode(int key, T value){
            this.key = key;
            this.value = value;
        }

    }

    /**
     * 查询方法
     * @param key
     * @return
     */
    public SkipNode find(int key){
        SkipNode temp = this.head;
        while (temp != null){
            if (temp.key == key){
                return temp;
            }
            if (temp.right == null || temp.right.key > key){
                temp = temp.down;
            }else {
                temp = temp.right;
            }
        }
        return null;
    }

    /**
     * 删除方法
     * @param key
     * @return
     */
    public boolean delete(int key){
        SkipNode temp = this.head;
        boolean isDelete = false;
        while (temp != null){
            if (temp.right == null || temp.right.key > key){
                temp = temp.down;
            }else if (temp.right.key == key){
                temp.right = temp.right.right;
                isDelete = true;
                temp = temp.down;
            }else {
                temp = temp.right;
            }
        }
        return isDelete;
    }

    /**
     * 插入方法
     * @param node
     */
    public void insert(SkipNode node){
        if (node == null){
            return;
        }
        SkipNode findNode = this.find(node.key);
        if (findNode != null){
            findNode.value = node.value;
            return;
        }
        Stack<SkipNode> skipDownNodeStack = new Stack<>();
        SkipNode temp = this.head;
        while (temp != null){
            if (temp.right == null || temp.right.key > node.key){
                skipDownNodeStack.add(temp);
                temp = temp.down;
            }else {
                temp = temp.right;
            }
        }
        SkipNode downNode = null;
        int nowHeight = 0;
        while (!skipDownNodeStack.empty()){
            SkipNode popSkipNode = skipDownNodeStack.pop();
            SkipNode insertSkipNode = new SkipNode(node.key, node.value);

            insertSkipNode.down = downNode;
            insertSkipNode.right = popSkipNode.right;
            popSkipNode.right = insertSkipNode;

            downNode = insertSkipNode;
            if (this.random.nextDouble() > this.ENABLE_ADD_SKIP_NODE || ++nowHeight > this.MAX_HEIGHT){
                break;
            }
            if (nowHeight > this.height){
                this.height = nowHeight;
                SkipNode newHeadSkipNode = new SkipNode(Integer.MIN_VALUE, null);
                newHeadSkipNode.down = this.head;
                skipDownNodeStack.add(newHeadSkipNode);
                this.head = newHeadSkipNode;
            }
        }
    }

    /**
     * 打印跳表
     */
    public void printSkipList(){
        if (this.head == null){
            return;
        }
        System.out.println("--------------------------- print for " + (++printNums) + " ---------------------------");
        SkipNode lastColumnHeadSkipNode = this.head;
        while (lastColumnHeadSkipNode.down != null){
            lastColumnHeadSkipNode = lastColumnHeadSkipNode.down;
        }
        SkipNode columnHeadSkipNode = this.head;
        while (columnHeadSkipNode != null){
            SkipNode tempLastColumnHeadSkipNode = lastColumnHeadSkipNode.right;
            SkipNode tempColumnHeadSkipNode = columnHeadSkipNode.right;
            System.out.printf("%-8s", "Head->");
            while(tempColumnHeadSkipNode != null){
                while (tempLastColumnHeadSkipNode != null && tempLastColumnHeadSkipNode.key != tempColumnHeadSkipNode.key){
                    System.out.printf("%-10s", "");
                    tempLastColumnHeadSkipNode = tempLastColumnHeadSkipNode.right;
                }
                System.out.printf("{%-3s:%-3s} ", tempColumnHeadSkipNode.key, tempColumnHeadSkipNode.value);
                tempColumnHeadSkipNode = tempColumnHeadSkipNode.right;
                tempLastColumnHeadSkipNode = tempLastColumnHeadSkipNode.right;
            }
            columnHeadSkipNode = columnHeadSkipNode.down;
            System.out.println();
        }
    }

    public static void main(String[] args) {
        SkipList<Integer> list=new SkipList<Integer>();
        long b = System.currentTimeMillis();
        for(int i = 1; i <= 1000000; i++) {
            list.insert(new SkipList<Integer>().new SkipNode<Integer>(i, i));
        }
        long e = System.currentTimeMillis();
        System.out.println("耗时:" + (double)(e - b) / 1000);

        b = System.currentTimeMillis();
        for(int i = 1; i < 1000000; i++) {
            Object value = list.find(i).value;
            if (i % 2 == 1){
                list.delete(i);
            }
            if (i > 999980){
                System.out.println("find key = " + i + " value = " + value);
            }
        }
        e = System.currentTimeMillis();
        System.out.println("耗时:" + (double)(e - b) / 1000);
//        list.printSkipList();
//        list.delete(3);
//        list.delete(6);
//        list.printSkipList();
    }

}