美文网首页
算法 1.6 跳表:Redis 中如何实现有序集合?【leetc

算法 1.6 跳表:Redis 中如何实现有序集合?【leetc

作者: 珺王不早朝 | 来源:发表于2021-01-08 22:55 被阅读0次

    题目描述

    不使用任何库函数,设计一个跳表
    跳表是在 O(log(n)) 时间内完成增加、删除、搜索操作的数据结构
    跳表相比于树堆与红黑树,其功能与性能相当,并且跳表的代码长度更短,其设计思想与链表相似
    例如,一个跳表包含 [30, 40, 50, 60, 70, 90],然后增加 80、45 到跳表中,以下图的方式操作:

    • 跳表中有很多层,每一层是一个短的链表
    • 你的设计应该要包含这些函数:
      • void add(int num):插入一个元素到跳表
      • bool search(int target):返回 target 是否存在于跳表中。
      • bool erase(int num):在跳表中删除一个值(num 不存在则返回 false,存在多个 num 则删除其中任意一个)

    约束条件:

    • 0 <= num, target <= 20000
    • 最多调用 50000 次 search, add, 以及 erase操作。

    样例:

    数据结构

    • 链表、跳表

    算法思维

    • 遍历

    解题要点

    • 类的设计(成员变量 与 成员方法)
    • 跳表的性质与特点

    关键知识点:跳表
    • Skip List,跳跃表,简称跳表
    • 实质是一种可以进行二分查找的链表
    • 在原有的有序链表上面增加了多级索引,通过索引来实现快速查找,以空间换时间


    跳表的特点
    • 多层结构,每一层随机概率产生
    • 每一层都是有序链表,默认升序,最底层包含所有元素,即原链表
    • 每个节点包含两个指针:向右(right,同级链表)、向下(down,下级链表)

    解题步骤

    一. Comprehend 理解题意
    题目主干要求
    • 不能使用任何库函数
    • 跳表由很多层有序的链表结构组成
    • 最底层(Level 1)称为原链表,包含所有元素
    • 上面每一层(Level i)链表都叫索引层,上级索引是下一级的子集
    • 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现
    • 每个节点包含两个指针,一个指向同一链表中的后一个元素,一个指向下面一层的元素
    • 跳表的每一个操作都是从最高层链表的头节点开始,从左往右,从上往下
    题目附加信息
    • 添加元素允许重复
    • 删除重复元素中的一个即可
    • 添加元素后,用扔硬币的方式决定是否创建索引
    • 删除元素时,应考虑索引的删除问题
    • 跳表中存储的数据范围是:[0, 20000]
    解题思路
    • 跳表(Skiplist)类的成员:
      • Node head:跳表的头节点指针
      • void add(int num):插入
      • boolean search(int target):查询
      • boolean erase(int num):删除
    • 节点(Node)类的成员:
      • int val:数据域
      • Node right,down:指针域,向右,向下
    二. Choose 选择数据结构与算法

    数据结构:链表(多层链表组成跳表)
    算法思维:遍历

    三. Code 编码实现基本解法
    步骤一:实现一个不含索引的跳表
    1. 创建跳表时初始化头节点和当前层级 head = HEAD; // -1right = nulldown = null
    2. 完成查找功能 :boolean search(int target) 找到返回 true
      从 head 开始,从左到右、从上到下依次查找
      如果 right 指向null,即尾节点,下移
      如果节点值等于 target,返回
      小于往右,大于往下
    3. 完成删除功能:boolean erase(int num) 删除成功返回 true
      查找到要删除的元素,方式与 search 相同
      执行删除操作:前面节点指向后面节点
    4. 完成插入功能:void add(int num) 插入原链表,支持重复
      定位原链表中要插入的位置,方式与 search 相同,新元素放到重复元素后
      新建节点,执行插入操作
    class Skiplist {
        final int HEAD_VALUE = -1; // 链表头节点的值 
        final Node HEAD = new Node(HEAD_VALUE);
        Node head; // 最左上角的头节点,所有操作的开始位置 
        int levels; // 当前层级,即 head 节点所在的最高层数 
    
        public Skiplist() {
            head = HEAD;
            levels = 1;
        }
    
        class Node {
            int val;
            Node right, down;
            Node(int val) {
                this(val, null, null);
            }
            Node(int val, Node right, Node down) {
                this.val = val;
                this.right = right;
                this.down = down;
            }
        }
    
        public void add(int num) {
            // 1.定位插入位置:原链表中 >= num 的最小节点前 
            Node node = head;
            while(node != null) { // node==null,到达原链表 
                while(node.right != null && node.right.val < num) {
                    node = node.right;
                }
                if(node.down == null) {
                    break;
                }
                // 继续查找下一层的位置 
                node = node.down;
            }
            // 2.插入新节点 
            Node newNode = new Node(num, node.right, null);
            node.right = newNode;
            // 3.TODO 根据扔硬币决定(是否)生成索引 
        }
    
        public boolean search(int target) {
            Node n = head;
            while(n != null) {
                // 1.在同一层级上向右查找,直到链表的结尾 
                while(n.right != null && n.right.val < target) {
                    n = n.right;
                }
                // 2.若找到,返回true 
                Node right = n.right; // 要查找的节点 
                if(right != null && right.val == target) {
                    return true;
                }
                // 3.若右侧数据较大,向下一层 
                n = n.down;
            }
            return false;
        }
    
        public boolean erase(int num) {
            boolean exist = false;
            Node n = head;
            while(n != null) {
                // 2.获取该指定数据节点的前一个节点 
                while(n.right != null && n.right.val < num) {
                    n = n.right;
                }
                // 2.与当前层链表断开 
                Node right = n.right; // 要删除的节点 
                if(right != null && right.val == num) {
                    n.right = right.right;
                    right.right = null; // help GC 
                    exist = true;
                }
                // 删除下一层 
                n = n.down;
            }
            return exist;
        }
    }
    
    步骤二:实现有索引的插入和删除
    1. 完成删除功能:
      下移,删除每一层
    2. 完成插入功能:
      抛硬币生成(多层)索引:
      正面朝上(随机数1),生成索引
      背面朝上(随机数0),返回
      索引层不存在,新建索引链表的头节点和索引节点
    四. Consider 思考更优解
    剔除无效代码,优化空间消耗
    • 查找和删除方法定位元素的方式是相同的,可以优化
    • 跳表的效率跟索引紧密相关,可以调整索引的生成策略来尝试优化
    寻找更好的算法思维
    • 借鉴其它算法
    五. Code 编码实现最优解
    class Skiplist {
    
        //声明所需的成员变量
        final int HEAD_VALUE = -1; //头节点值
        Node head;  //头节点
        int levels; //跳表当前包括几层链表
        int length; //跳表元链表的长度
    
        public Skiplist() {
            head = new Node(HEAD_VALUE);
            levels = 1;
            length = 1;
        }
    
        public void add(int num) {
            //添加条件:当前正在遍历元链表 && 下一个节点的值不小于 num
            Node curr = head;
            Node[] nodes = new Node[levels]; //节点数组,存放每一个拐点
            int i = 0; //计数器
    
            //逐行遍历跳表,直到进入最底层
            while (curr != null) {
    
                //遍历当前层的每一个节点
                while (curr.right != null && curr.right.val < num)
                    curr = curr.right;
    
                //将拐点存入数组中
                nodes[i++] = curr;
                curr = curr.down;
            }
    
            //在元链表中插入新节点
            curr = nodes[--i];
            Node newNode = new Node(num, curr.right, null);
            curr.right = newNode;
            length++;
    
            //添加索引,参数为:1.哪些节点上可能添加索引 2.当前为第几个节点 3.节点值
            addIndex(newNode, nodes, i);
        }
    
        private void addIndex(Node target, Node[] nodeArray, int indices) {
            Node downNode = target;
            int coin = new Random().nextInt(2);
    
            //添加索引的要求:硬币为1,且层数没有超过链表长度的 1/64
            while (coin == 1 && levels < (length >> 6)) {
                //没到达顶层前:在每一个拐点右侧添加新的索引节点
                if (indices > 0) {
                    Node prev = nodeArray[--indices];
                    Node newIndex = new Node(target.val, prev.right, downNode);
                    prev.right = newIndex;
                    downNode = newIndex;
                } else { //到达顶层后:添加新的一层,包括新的索引节点和一个新的头节点
                    Node newIndex = new Node(target.val, null, downNode);
                    head = new Node(HEAD_VALUE, newIndex, head);
                    levels++;
                }
            }
        }
    
        public boolean erase(int num) {
            Node curr = get(num, head);
            if (curr == null) return false;
            //如果跳表中有当前元素,则逐行删除此节点
            while (curr != null) {
                Node right = curr.right;
                curr.right = right.right;
                right.right = null;
                right.down = null;
    
                //注意:这里只能使用 curr.down 而不能使用 curr,后者会报错
                curr = get(num, curr.down);
            }
            length--;
            return true;
        }
    
        public boolean search(int target) {
            return get(target, head) != null;
        }
    
        private Node get(int target) {
            return get(target, head);
        }
    
        private Node get(int target, Node from) {
            Node curr = from;
            
            //逐行遍历跳表
            while (curr != null) {
                
                //遍历当前行中的每一个节点,直到当前节点的下一个节点不小于 target
                while (curr.right != null && curr.right.val < target) 
                    curr = curr.right;
                
                //循环结束后三种情况:right == target; right > target; right == null
                if (curr.right != null && curr.right.val == target) 
                    return curr;
                
                curr = curr.down;
            }
            return null;
        }
    
        class Node {
            int val;
            Node right, down;
    
            Node(int val) {
                this(val, null, null);
            }
    
            Node(int val, Node right, Node down) {
                this.val = val;
                this.right = right;
                this.down = down;
            }
        }
    }
    

    时间复杂度:O(log(n)) -- 二分查找 O(log(n)),查找+删除 O(log(n)) + O(k),查找+插入 O(log(n)) + O(k)
    空间复杂度:O(n) -- 元链表 O(n),索引 O(n)
    执行耗时:46 ms,击败了 35.10% 的Java用户
    内存消耗:46.5 MB,击败了 21.30% 的Java用户

    六. Change 变形与延伸
    题目变形
    • ConcurrentSkipListMap:TreeMap 的并发版本
    • ConcurrentSkipListSet:TreeSet 的并发版本
    • 给定一个链表,如何转成跳表结构?
    延伸扩展
    • Redis,LevelDB,Lucene

    相关文章

      网友评论

          本文标题:算法 1.6 跳表:Redis 中如何实现有序集合?【leetc

          本文链接:https://www.haomeiwen.com/subject/knbboktx.html