跳跃表

常见数据结构的实现【1】:跳跃表

 

知乎的小伙伴们好,这是我在知乎写的第一篇文章哈。我写这篇文章的目的主要是和大家分享一些想法,交流学习一下。

这系列的文章是分析常见数据结构的实现,包括跳跃表、二叉堆、前缀树、红黑树等等。。。数据结构这门课在学习与工作中都非常重要,所以我觉得有必要把自己的想法拿出来和大家分享交流,互相学习。

下面就开始正题吧!(第一次写文章,如果某些地方语言、思路表达不当或者有误,希望大家包容一下吧哈哈)

介绍

链表是一种基本的数据结构,而跳跃表是一种特殊的有序链表。

  • 跳跃表是由多层有序链表组合而成的,最底一层的链表保存了所有的数据,每向上的一层链表依次保存了下一层链表的部分数据。
  • 相邻的两层链表中元素相同的节点之间存在引用关系,一般是上层节点中存在一个指向下层节点的引用
  • 跳跃表的目的在于提高了查询效率,同时也牺牲了一定的存储空间。

下面的一张图是我自己绘制的跳跃表,每层包含了头节点并且是单向链表:

分析

按照上面的图,所有节点包含以下内容:存储的元素,指向同一层中下一个节点的引用,指向下一层中对应节点的引用。

那么我们可以如下构建节点类和跳跃表类:

//跳跃表的实现
//元素有序且不重复
public class SkipList {

    private Node head;//顶层头节点    
    private int rate;//相邻两层元素个数的比例    
    private int level;//跳跃表层数    
    private int length;//底层节点个数    
    private int size;//所有层节点个数
    private final boolean order;//true表示正序,false表示逆序    
    private Random random;//随机数    
    private Stack<Node> stack;//保存查询时遍历的节点
    
    //节点类
    private static class Node {
        
        private Comparable comparable;
        private Node right;//同一层的右边节点
        private Node down;//下一层的对应节点
        
        public Node(Comparable comparable) {
            this.comparable = comparable;
            this.right = null;
            this.down = null;
        }
    }
    
    public SkipList(int rate, int level, boolean order) {
        this.rate = rate;
        this.level = level;
        this.length = 0;
        this.size = 0;
        this.order = order;
        this.random = new Random();
        this.stack = new Stack<Node>();
        this.head = new Node(null);//头节点的值默认为null
        Node temp = head;
        for (int i = 1; i < level; i++) {
            temp.down = new Node(null);
            temp = temp.down;
        }
    }

}
  • [公式] 表示相邻两层链表的元素数量之比,也就是下一层每添加多少个元素时,就向上一层继续添加一个元素。这个值可以由个人决定
  • [公式] 表示有序链表保存元素的顺序,对于整型数据,从前向后,元素从小到大为 [公式] ,对于字符型数据,从前向后,元素的字典序依次靠后为 [公式] ,等等
  • [公式] 用于添加操作时生成随机数的种子
  • [公式] 表示查询操作时保存查询路径上某些节点的栈, [公式] 类可以自己实现

[公式] 类构造方法的最后几句代码表示初始化几个头节点,其中 [公式] 指向最上层的头节点。我的实现中跳跃表保存的元素不重复,如果想要保存重复的元素,只需要在原来的基础上稍作修改即可

查找元素

添加、删除操作都依赖于查找操作,所以先介绍查找操作

查找操作是自顶向下、从左向右的,也就是先在最上一层链表中从左向右查找,然后依次向下层查找,直到最下一层的某个节点结束。具体的操作依赖于保存元素的顺序,假设保存的是整型数据, [公式] 为 [公式] ,那么元素从前向后依次变大:

  • 先在最上一层查找小于所指定元素的最右的节点,将节点入栈,然后转向下一层
  • 在当前层继续向后查找满足上述条件的节点,同样将其入栈,向下一层继续查找,直到查找到最下一层满足条件的节点

从上面的分析中可以知道, [公式] 中保存的是查找路径中每层最右边的节点,最底层的那个节点除外。

举个例子,在第一张图中,查找元素 [公式] 时,查找路径为 [公式] , [公式] 中保存的元素依次为 [公式] 。查找元素 [公式] 时,查找路径为 [公式] , [公式] 中保存的元素依次为 [公式] 。特殊情况下,头节点也会入栈

为什么这里设计成这样的查找路径,以及栈中保存查找路径上每层最右的节点?主要是在单链表的情况下,查找元素时可以统一元素存在和不存在两种情况,同时添加、删除元素时方便改变节点之间的引用关系。下面贴出查找操作的代码:

    //查询元素,自顶向下
    //正序时,返回底层【小于】给定值的最大的节点,包含头节点
    //逆序时,返回底层【大于】给定值的最小的节点,包含头节点
    private Node search(Comparable comparable) {
        stack.clear();
        Node temp = head;//从顶层开始
        while (true) {
            while (temp.right != null) {
                if (order && temp.right.comparable.compareTo(comparable) >= 0)//正序时查找当前层【小于】给定值的最大的节点
                    break;
                if (!order && temp.right.comparable.compareTo(comparable) <= 0)//逆序时查找当前层【大于】给定值的最小的节点
                    break;
                temp = temp.right;
            }
            if (temp.down == null)//找到底层的节点
                break;
            stack.push(temp);//stack保存遍历路径中每一层最右边的节点,除底层外
            temp = temp.down;//转到下一层
        }
        return temp;
    }

如果理解了查找操作的过程,那么上面的代码就很容易看懂了。查找操作返回的是最下一层满足那个条件的节点(有可能是头节点)。

添加元素

在我的实现中,跳跃表不保存重复的元素,所以只有当所指定元素不存在时,才执行添加操作。

添加操作是自底向上的,并且根据指定的 [公式] 按照一定的概率向上层添加节点。添加时需要维护同层节点之间的关系,同时也要维护当前节点与下一层对应节点的关系。这里只需要注意的一点是,什么条件下才能向上层继续添加?只有当随机数满足条件并且当前层不是最上一层时,才能继续添加。

    //添加元素
    //若元素已存在,则返回,保证无重复元素
    public void insert(Comparable comparable) {
        Node temp = search(comparable);
        if (temp.right != null && temp.right.comparable.compareTo(comparable) == 0)//元素已存在
            return;
        Node node = new Node(comparable);
        Node other;
        //根据随机数,自底向上添加每层的新节点
        while (true) {
            node.right = temp.right;
            temp.right = node;//当前层添加完毕
            size++;
            if (random.nextInt(rate) != 0 || stack.isEmpty())
                break;
            //若随机数为0且还未到顶层,则向上层添加元素
            temp = stack.pop();
            other = node;
            node = new Node(comparable);
            node.down = other;
        }
        length++;
        return;
    }

再强调一下,查找操作返回的是最下一层满足条件的节点。注意下, [公式] 循环里面向上层添加单个节点的过程实际上分解到了两次循环中,先维护上下层节点的关系,再维护同层节点的关系。

删除元素

当所指定元素存在时,删除操作需要删除所有层中的对应元素。如果某一层中不存在指定的元素,那么上面的所有层中肯定也不会存在,因此可以直接跳出循环,操作结束。

    //删除元素
    //若元素不存在,则返回,否则删除所有层中包含的元素
    public void delete(Comparable comparable) {
        Node temp = search(comparable);
        if (temp.right == null || temp.right.comparable.compareTo(comparable) != 0)//元素不存在
            return;
        while (true) {
            if (temp.right == null || temp.right.comparable.compareTo(comparable) != 0) //当前层的元素不存在
                break;
            //从底层开始,依次删除每层的元素
            temp.right = temp.right.right;
            size--;
            if (stack.isEmpty())//到达顶层
                break;
            temp = stack.pop();//转到上一层
        }
        length--;
        return;
    }

后记

从上面的讨论中可以看到,跳跃表的查找、添加、删除操作其实并不难理解,这个数据结构比较简单。当然这篇文章是我的个人理解,欢迎感兴趣的读者一起来交流,提出建议。后面我会介绍其他一些常用数据结构的实现,希望大家继续关注哦~

posted @ 2021-05-29 17:56  abcdefghijklmnop  阅读(191)  评论(0编辑  收藏  举报