跳表之美

570 阅读3分钟

前言

最近业务开发用到了 redis中的 zset 有序集合,来实现积分排名的功能。 早有耳闻zset是由跳表实现,代码简单可读性高,查找插入删除等性能不逊色一系列平衡树。

要聊跳表,则要从二分查找说起。

二分查找

 二分查找一般用在一个有序的数组,查找 某个目标数值是否存在的场景。

数组有个特点,可以通过下标直接访问元素。每次目标值通过和区间中部元素大小对比,如未命中,则将待查找的元素缩小为之前的一半,一直重复,直到找到要查找的元素为止。平均查找时间复杂度为 O(logn)

image.png

那一个有序的链表,如何使用二分查找算法,实现查找目标值呢?

跳表背景

实际上,只需要对链表加多级索引进行稍加改造,也可以支持二分的思想,而改造之后的链表就是鼎鼎大名的跳表。

链表建索引,可以在原始链表的基础上,每隔两三个节点就提到新索引层上,实现“二分”思想。

天下没有免费的午餐,跳表建了多级索引,是以空间换时间。每次查找后数据都会缩小为原来的一半。

   image.png

13147762-5a612da22814f257.png

跳表全称为跳跃列表,它允许快速查询,插入和删除一个有序连续元素的数据链表。

跳表实现代码并不复杂,可读性较高。

例如完全可以手写一个跳表查找某目标元素

public Node searchNode(int target) {
    Node re = null;
    Node temp = head;
    while (temp != null) {
        if (temp.value != target) {
            //右边节点值比target大,则往下走
            if (temp.right != null && temp.right.value <= target) {
                temp =  temp.right;
            } else {
                temp = temp.down;
            }
        } else {
            re = temp;
            temp = temp.down;
        }
    }
    return re;
}

 

跳表到底需要消耗多少额外储存空间呢?

假如以每隔两结点来建索引来看,结点总和为 n/2 + n/4+n/8+…+8+4+2 等比数列求和 = n-1 ,将近用接近 n 个结点的额外空间。

可以采用提高每隔结点个数组成索引,减少相应的存储空间。

但实际开发中,链表存储的有可能是很大的对象,而索引结点只存了几个指针罢了,和大对象比起来,完全可以忽略消耗。

 突然联想到,B+树中的非叶子结点中全是指针,并没有数据,数据存在叶子节点里。

跳表索引动态更新

随着数据不停的插入或者删除,如果没有动态的更新索引,有可能出现两个索引节点之间数据非常多的情况。极端时候,跳表可能会重新退化成链表。

image.png

  红黑树,AVL树等,也是用二分查找的思想,构建平衡二叉树。它们通过左右旋的方式保持左右子树大小平衡,达到索引动态更新。而跳表是通过  随机函数 来维护所谓的平衡性。

举例子,当往跳表中插入数据的时候,可以选择插入部分索引层中,如何选择加入哪些层呢? 跳表采用用随机函数决定,比如随机函数Random 生成了 k ,就添加到 1 - k 级索引层中。  以下图为例,往跳表中插入数据 6 ,假设随机数为2 ,就插入从下往上的两层。

image.png  

总结

 平常使用的一些数据结构,其实都有设计的思想原理。只有自己开始思考,总结和归纳,才会真正的掌握。

先前深究过B+树的数据结构,而在学习跳表前,会自然而然想,会不会跳表也是用二分查找的思想呢。所以当有不知道的知识点时,往知识树上挂,学习会更为系统和全面。