Redis 跳跃表

什么是跳跃表
跳跃表是一种有序的数据结构,它通过在每个节点中维持多个指向其他的几点指针,从而达到快速访问队尾目的。跳跃表的效率可以和平衡树想媲美了,最关键是它的实现相对于平衡树来说,代码的实现上简单很多。

跳跃表用在哪
说真的,跳跃表在 Redis 中使用不是特别广泛,只用在了两个地方。一是实现有序集合键,二是集群节点中用作内部数据结构

跳跃表原理
我们先来看一下一张完整的跳跃表的图。(图片来自《Redis 设计与实现》)
完整跳跃表
跳跃表的 level 是如何定义的?
跳跃表 level 层级完全是随机的。一般来说,层级越多,访问节点的速度越快

跳跃表的插入
首先我们需要插入几个数据。链表开始时是空的。
链表开始
插入 level = 3,key = 1
当我们插入 level = 3,key = 1 时,结果如下:
level = 3,key = 1
插入 level = 1,key = 2
当继续插入 level = 1,key = 2 时,结果如下
level = 1,key = 2
插入 level = 2,key = 3
当继续插入 level = 2,key = 3 时,结果如下
level = 2,key = 3
插入 level = 3,key = 5
当继续插入 level = 3,key = 5 时,结果如下
level = 3,key = 5
插入 level = 1,key = 66
当继续插入 level = 1,key = 66 时,结果如下
level = 1,key = 66
插入 level = 2,key = 100
当继续插入 level = 2,key = 100 时,结果如下
level = 2,key = 100
上述便是跳跃表插入原理,关键点就是层级–使用抛硬币的方式,感觉还真是挺随机的。每个层级最末端节点指向都是为 null,表示该层级到达末尾,可以往下一级跳。

跳跃表的查询

现在我们要找键为 66 的节点的值。那跳跃表是如何进行查询的呢?

跳跃表的查询是从顶层往下找,那么会先从第顶层开始找,方式就是循环比较,如过顶层节点的下一个节点为空说明到达末尾,会跳到第二层,继续遍历,直到找到对应节点。

如下图所示红色框内,我们带着键 66 和 1 比较,发现 66 大于 1。继续找顶层的下一个节点,发现 66 也是大于五的,继续遍历。由于下一节点为空,则会跳到 level 2。
顶层遍历
上层没有找到 66,这时跳到 level 2 进行遍历,但是这里有一个点需要注意,遍历链表不是又重新遍历。而是从 5 这个节点继续往下找下一个节点。如下,我们遍历了 level 3 后,记录下当前处在 5 这个节点,那接下来遍历是 5 往后走,发现 100 大于目标 66,所以还是继续下沉。
第二层遍历
当到 level 1 时,发现 5 的下一个节点恰恰好是 66 ,就将结果直接返回。
遍历第一层

跳跃表删除
跳跃表的删除和查找类似,都是一级一级找到相对应的节点,然后将 next 对象指向下下个节点,完全和链表类似。

现在我们来删除 66 这个节点,查找 66 节点和上述类似。
找到 66 节点
接下来是断掉 5 节点 next 的 66 节点,然后将它指向 100 节点。
指向 100 节点
如上就是跳跃表的删除操作了,和我们平时接触的链表是一致的。当然,跳跃表的修改,也是和删除查找类似,只不过是将值修改罢了,就不继续介绍了。

跳跃表实现

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
 * @author Gentle
 * 实现跳跃表
 */
public class SkipList<T> {
    public static void main(String[] args) {
        SkipList<String> list = new SkipList<>();
        list.put(1, "1");
        list.put(2, "2");
        list.put(3, "3");
        list.put(5, "5");
        list.put(100, "100");
        list.put(66, "66");

        System.out.println(list);
        System.out.println("查找66 : " + list.get(66));
        list.delete(66);
    }

    /**
     * 跳跃表的节点的构成
     *
     * @param <E>
     */
    private static class SkipNode<E> {
        /**
         * 存储的数据
         */
        E val;
        /**
         * 跳跃表按照这个分数值进行从小到大排序。
         */
        Integer score;
        /**
         * next指针,指向下一层的指针
         */
        SkipNode<E> next, down;

        SkipNode(E val, int score) {
            this.val = val;
            this.score = score;
        }
    }
    /** 节点的高度,这里限制最高 4 层 */
    private static final int MAX_LEVEL = 4;

    /**
     * 跳跃表数据结构
     */
    private SkipNode<T> head;
    /** 节点级别 */
    private int level = 0;
    /**
     * 用于产生随机数的Random对象
     **/
    private Random random = new Random();

    public SkipList() {
        //创建默认初始高度的跳跃表
        this(4);
    }

    /**
     * 跳跃表的初始化
     */
    public SkipList(int level) {
        this.level = level;
        int i = level;
        SkipNode<T> temp = null;
        SkipNode<T> prev = null;
        while (i-- != 0) {
            temp = new SkipNode<T>(null, Integer.MAX_VALUE);
            temp.down = prev;
            prev = temp;
        }
        //头节点
        head = temp;
    }

    /**
     * 产生节点的高度。使用抛硬币
     *
     * @return
     */
    private int getRandomLevel() {
        int lev = 1;
        while (random.nextInt() % 2 == 0) {
            lev++;
        }
        return lev > MAX_LEVEL ? MAX_LEVEL : lev;
    }

    /**
     * 查找跳跃表中的一个值
     *
     * @param score 分数
     * @return
     */
    public T get(Integer score) {
        SkipNode<T> t = head;
        while (t != null) {
            if (t.score.equals(score)) {
                return t.val;
            }
            if (t.next == null) {
                if (t.down != null) {
                    t = t.down;
                    continue;
                } else {
                    return null;
                }
            }
            if (t.next.score > score) {
                t = t.down;
            } else {
                t = t.next;
            }
        }
        return null;
    }

    /**
     *
     * @param score 分数键(排序)
     * @param val  值
     */
    public void put(Integer score, T val) {

        SkipNode<T> t = head, cur = null;
        //记录每一层当前节点的前驱节点
        List<SkipNode<T>> path = new ArrayList<>();
        while (t != null) {
            if (t.score .equals(score)) {
                //表示存在该值的点,表示需要更新该节点
                cur = t;
                break;
            }
            if (t.next == null) {
                //需要向下查找,先记录该节点
                path.add(t);
                if (t.down != null) {
                    //进入下一个层级
                    t = t.down;
                    continue;
                } else {
                    break;
                }
            }
            if (t.next.score > score) {
                //需要向下查找,先记录该节点
                path.add(t);
                if (t.down == null) {
                    break;
                }
                t = t.down;
            } else {
                t = t.next;
            }
        }
        if (cur != null) {
            while (cur != null) {
                cur.val = val;
                cur = cur.down;
            }
        } else {
            //随机层级
            int lev = getRandomLevel();

            if (lev > level) {
                SkipNode<T> temp = null;
                //前驱节点现在是top了
                SkipNode<T> prev = head;
                while (level++ != lev) {
                    temp = new SkipNode<T>(null, Integer.MIN_VALUE);
                    //加到path的首部
                    path.add(0, temp);
                    temp.down = prev;
                    prev = temp;
                }
                //头节点
                head = temp;
                //level长度增加到新的长度
                level = lev;
            }
            //从后向前遍历path中的每一个节点,在其后面增加一个新的节点
            SkipNode<T> downTemp = null, temp, prev;
            for (int i = level - 1; i >= level - lev; i--) {
                temp = new SkipNode<T>(val, score);
                prev = path.get(i);
                temp.next = prev.next;
                prev.next = temp;
                temp.down = downTemp;
                downTemp = temp;
            }
        }
    }

    /**
     * 根据score的值来删除节点。
     *
     * @param score 分数键(排序)
     */
    public void delete(Integer score) {
        //1,查找到节点列的第一个节点的前驱
        SkipNode<T> t = head;
        while (t != null) {
            if (t.next == null) {
                t = t.down;
                continue;
            }
            // 在这里说明找到了该删除的节点
            if (t.next.score.equals(score)) {
                t.next = t.next.next;
                t = t.down;
                //删除当前节点后,还需要继续查找之后需要删除的节点
                continue;
            }

            if (t.next.score > score) {
                t = t.down;
            } else {
                t = t.next;
            }
        }
    }

    /**
     * 输出结果,一层一层输出
     * @return
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        SkipNode<T> t = head, next;
        while (t != null) {
            next = t;
            while (next != null) {
                sb.append(next.score).append(" ");
                next = next.next;
            }
            sb.append("\n");
            t = t.down;
        }
        return sb.toString().replace(Integer.MAX_VALUE+"","").replace("-2147483648","");
    }
}

结果测试
在跳跃表结果
总结
本章,我们学习了跳跃表的原理,知道在 Redis 中使用地方。 最后使用 java 代码实现了一个跳跃表(代码参考自网络)。本质上跳跃表就是一个链表,但这链表是有序的。意味着我们就可以冗余节点,来达到快速查询的效率。如果是无序的,那就没必要了。

 

 

转载:https://blog.csdn.net/weixin_41622183/article/details/91126155?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-2.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-2.control

posted @ 2020-12-25 10:21  彼岸-花已开  阅读(121)  评论(0编辑  收藏  举报