算法与数据结构之美—跳表


二分查找底层依赖于数组随机访问的特性,如果数组存储在链表中,那么如何采用二分查找算法?今天这篇博文就会介绍-跳表,这种支持快速插入、删除、查找操作;
Redis种的有序数组就是采用跳表来实现的;

理解跳表

对于一个单链表,即便链表中存储的数据是有序的,但是查找数据的效率很低,时间复杂度很高是O(n),如下图所示:
单链表
那么对于链表建立多级索引,查找效率是不是更高一点呢?例如每两个结点提取一个结点到上一级,就把抽离出来的那一级叫作索引或索引层
一级索引
二级索引

跳表查询效率

跳表
对于单链表的查询效率为O(n),假设链表中有n个结点,如果按照每隔两个结点抽出一个结点作为上一级索引的结点,那么第一级索引的结点个数为n/2,第二级索引的结点个数为n/4,第k级索引的结点个数就是n/(2^k),最终采用跳表的查询效率是O(logn),查询效率的提升是建立在多级索引之上的,采用空间换时间的概念。

跳表的内存消耗

跳表内存
对于多级索引的结点-等比数列求和,跳表的空间复杂度是O(n),只需要额外的接近n个结点的存储空间;

那么如何降低索引占用的内存空间?

上面是每隔两个结点,抽取一个结点到上级索引中,那么可以减少索引结点,每隔三个结点或者五个结点抽取一个索引结点;
n/3+n/9+n/27+…+9+3+1=n/2,所需要的内存空间缩小了一半;

高效的动态插入和删除

动态插入
查找某个结点的时间复杂度是O(log(n)),对于单链表的删除操作一定要获取到要删除结点的前驱结点,然后通过指针操作来完成删除;对于双向链表就不需要考虑这些问题了;

跳表索引动态更新

跳表索引动态更新
作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平衡;当链表中结点数增加,索引结点也相应的增加,避免复杂度的退化,以及查找、插入、删除操作的性能下降;
当我们往跳表中插入一个数据的时候,可以同时选择将这个数据插入到部分索引层中,
采用随机函数,来决定将结点插入到哪几级索引中,比如随机函数生成了K,那我们就将这个结点添加到第一级到第k级之间的索引中;
跳表的动态插入过程

代码实现

import java.util.Random;

public class SkipList {
    private static final int MAX_LEVEL = 16;

    private int levelCount = 1;

    //带头链表
    private Node head = new Node();

    private Random r = new Random();

    //跳表查询
    public Node find(int value){
        Node p = head;
        for(int i = levelCount-1;i>=0;--i)
        {
            while(p.forward[i]!=null&&p.forward[i].data<value){
                p = p.forward[i];
            }
        }
        if(p.forward[0]!=null&&p.forward[0].data==value)
            return p.forward[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.forward[i]!=null&&p.forward[i].data<value){
                p = p.forward[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.forward[i] = update[i].forward[i];
            update[i].forward[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.forward[i] != null && p.forward[i].data < value) {
                p = p.forward[i];
            }
            update[i] = p;
        }

        if (p.forward[0] != null && p.forward[0].data == value) {
            for (int i = levelCount - 1; i >= 0; --i) {
                if (update[i].forward[i] != null && update[i].forward[i].data == value) {
                    update[i].forward[i] = update[i].forward[i].forward[i];
                }
            }
        }
    }

    // 随机 level 次,如果是奇数层数 +1,防止伪随机
    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.forward[0] != null) {
            System.out.print(p.forward[0] + " ");
            p = p.forward[0];
        }
        System.out.println();
    }


    public class Node {
        private int data = -1;
        private Node forward[] = 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();
        }
    }
}

解答开篇

Redis中的有序集合支持的核心操作:

  • 插入数据
  • 删除数据
  • 查找数据
  • 按照区间查找数据
  • 迭代输出有序序列
    Redis之所以用跳表来实现有序集合,还有其他原因,比如,跳表更容易代码实现。虽然跳表的实现也不简单,但比起红黑树来说还是好懂、好写多了,而简单就意味着可读性好,不容易出错。还有,跳表更加灵活,它可以通过改变索引构建策略,有效平衡执行效率和内存消耗。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值