跳表的实现以及应用

前言

最近在看Redis相关资料发现redis的存储类型中有一个是zset,zset很有意思,分为两种实现一种是基于压缩列表,另一种是基于跳表实现。

压缩列表

“Redis 为了节约内存空间使用,zset 和 hash 容器对象在元素个数较少的时候,采用压 缩列表 (ziplist) 进行存储。压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有任 何冗余空隙。” 我理解这句话的意思是创建一个比较小的数组,虽然需要频繁扩容但是这样可以避免占用不必要的内存空间。

跳表

着重说一下跳表,跳表也是一种数据结构,但是这种数据结构并不是常见的。它是一种在链表上添加索引来达到高效查找。传统的链表在查找的时候只能从头节点开始一个一个向后遍历,如果链表足够长,那就意味着要将链表从头遍历一次。如果在链表的每个节点上添加一系列索引,如图,那么效率就会大大提升。redis中的Zset在数据量少的情况下使用的就是跳表的实现。

image.png

image.png
以上是一个比较直观的例子,结合最上面的图,每一个竖列为一个节点,最下面保存的是真实的数据,上面的为索引。这样的数据结果就是跳表(SkipList).

跳表的应用

在java中LinkedHashSet是一个有序的不重复的集合。LinkedHashSet是基于链表来实现的集合,下面使用java来实现一个基于跳表的set集合。

package com.cz.map;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @program: Reids
 * @description: 使用跳表实现set有序集合
 * @author: Cheng Zhi
 * @create: 2023-04-24 10:45
 **/
public class JefSkipSet<T> implements Set {

    /**
     * 计数器
     */
    private AtomicInteger count;
    
    /**
     * 链表节点,每个节点提供向右向下指针
     */
    private static class Node<T>{
        Node rightNext;
        Node downNext;
        int key;
        T value;
        public Node(int key, T value) {
            this.key = key;
            this.value = value;
        }
    }

    private Node head;
    private int currentHightLevel = 0;
    private int MAX_INDEX_LEVEL = 32;
    private Random random = new Random();

    public JefSkipSet() {
        head = new Node(Integer.MIN_VALUE, null);
        count = new AtomicInteger();
    }

    /**
     * 查找节点
     * @param key
     * @return
     */
    private Node find(int key) {

        Node node = head;
        while(node != null) {

            if (node.rightNext == null ) {
                node = node.downNext;
            } else if (node.rightNext.key > key) { // 如果当前节点的右侧节点大于key,则需要下沉。
                node = node.downNext;
            } else if (node.rightNext.key < key) { // 如果当前节点的右侧节点小于key,则右移。
                node = node.rightNext;
            } else if (node.rightNext.key == key){ // 找到了直接返回
                return node.rightNext;
            }
        }

        return null;
    }

    /**
     * 添加节点
     * @param node
     */
    private void add(Node node) {

        if (node == null) {
            return;
        }
        Node findNode = find(node.key);
        if (findNode != null) { // 如果找到直接替换值
            findNode.value = node.value;
            return;
        }
        Node tmp = head;
        Stack<Node> stack = new Stack<Node>();  // 保存所有下沉的节点,其中栈顶节点就是目标节点要插入的位置。目标节点插入到栈顶节点的后面。
        while (tmp != null) {

            if (tmp.rightNext == null) {
                stack.add(tmp);
                tmp = tmp.downNext;
            } else if (tmp.rightNext.key > node.key) {
                stack.add(tmp);
                tmp = tmp.downNext;
            } else if (tmp.rightNext.key < node.key) {
                tmp = tmp.rightNext;
            }
        }

        Node indexNode = null;
        int level = 1;
        while (!stack.isEmpty()) {

            Node pop = stack.pop();
            Node newNode = new Node(node.key, node.value);
            newNode.downNext = indexNode;
            indexNode = newNode; // 复制一份做为索引节点
            if (pop.rightNext == null) {
                pop.rightNext = newNode;
            } else {
                node.rightNext = pop.rightNext;
                pop.rightNext = newNode;
            }

            if (level > MAX_INDEX_LEVEL) {
                break;
            }

            // 随机产生索引节点,50%的概率会产生索引
            double v = random.nextDouble();
            if (v > 0.5) {
                break;
            }

            level ++;

            if (level > currentHightLevel) {
                currentHightLevel = level;
                // 新建head节点,保证head的索引一定是最高的
                Node newHeadNode = new Node(Integer.MIN_VALUE, null);
                newHeadNode.downNext = head;
                head = newHeadNode;
                stack.add(head);
            }

        }

        count.getAndIncrement();
    }

    private void delete(int key) {
        Node node = head;
        while (node != null) {
            if (node.rightNext == null) {
                node = node.rightNext;
            } else if (node.rightNext.key < key) {
                node = node.rightNext;
            } else if (node.rightNext.key > key) {
                node = node.downNext;
            } else if (node.rightNext.key == key) {
                // 这里要做删除逻辑
                node.rightNext = node.rightNext.rightNext;
                node = node.downNext;
            }
        }

        count.getAndDecrement();
    }

    @Override
    public int size() {
        return count.get();
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

    @Override
    public boolean contains(Object o) {
        return false;
    }

    @Override
    public Iterator iterator() {
        return null;
    }

    @Override
    public Object[] toArray() {
        return new Object[0];
    }

    /**
     * 保证顺序的自增
     * @param o
     * @return
     */
    @Override
    public boolean add(Object o) {

        // 为o计算hash
        int key = o.hashCode();
        Node node = new Node(key, o);
        try {
            add(node);
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    @Override
    public boolean remove(Object o) {
        int key = o.hashCode();
        try {
            delete(key);
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    @Override
    public boolean addAll(Collection c) {
        return false;
    }

    @Override
    public void clear() {

    }

    @Override
    public boolean removeAll(Collection c) {
        return false;
    }

    @Override
    public boolean retainAll(Collection c) {
        return false;
    }

    @Override
    public boolean containsAll(Collection c) {
        return false;
    }

    @Override
    public Object[] toArray(Object[] a) {
        return new Object[0];
    }

    public void printStruct() {
        Node teamNode=head;
        int index=1;
        Node last=teamNode;
        while (last.downNext!=null){
            last=last.downNext;
        }
        while (teamNode!=null) {
            Node enumNode=teamNode.rightNext;
            Node enumLast=last.rightNext;
            System.out.printf("%-8s","head->");
            while (enumLast!=null&&enumNode!=null) {
                if(enumLast.key==enumNode.key)
                {
                    System.out.printf("%-5s",enumLast.key+"->");
                    enumLast=enumLast.rightNext;
                    enumNode=enumNode.rightNext;
                }
                else{
                    enumLast=enumLast.rightNext;
                    System.out.printf("%-5s","");
                }

            }
            teamNode=teamNode.downNext;
            index++;
            System.out.println();
        }
    }

    public static void main(String[] args) {
        JefSkipSet<Integer> jefSkipSet = new JefSkipSet<Integer>();
        for (int i= 0; i< 50; i++) {
            jefSkipSet.add("a");
        }
        jefSkipSet.printStruct();
        System.out.println(jefSkipSet.size());
    }
}

跳表的实现主要利用Node的两个指针,一个指向右边节点,一个指向下面节点。代码中使用的是随机生成索引的方式。跳表在java中的应用还有比如ConcurrentSkipListSet和ConcurrentSkipListMap。因为相对于树形结构的数据结构而言,跳表的实现还是相对简单的。这也是redis使用跳表而不使用红黑树的原因。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis中的跳表(Skip List)是一种有序数据结构,用于实现有序集合(Sorted Set)的数据存储和查询。跳表通过添加多级索引来加速查询操作。 跳表实现主要包含以下几个步骤: 1. 跳表节点的定义:定义一个节点结构,包含键值对以及多个指向下一个节点的指针,其中指针的数量由一个随机函数决定,通常设置为1到32之间。 2. 跳表层级的定义:定义一个层级结构,包含多个指向不同层级的节点的指针。每个层级都是一个链表,其中最底层是原始数据链表,每个节点按照键值进行排序。 3. 插入操作:在插入新节点时,需要选择节点要插入的层级。从最高层级开始,逐层向下遍历,直到找到插入位置。在遍历过程中,如果遇到相同键值的节点,则更新节点的值;如果没有找到相同键值的节点,则将新节点插入到对应位置,并将相关指针进行更新。 4. 删除操作:在删除节点时,需要找到对应键值的节点,并将相关指针进行更新。如果删除后某个层级中没有节点了,则需要将该层级删除。 5. 查询操作:在查询某个键值对应的节点时,从最高层级开始,逐层向下遍历,直到找到节点或者遍历到最底层。在遍历过程中,根据节点的键值与目标键值的大小关系,决定向右还是向下移动。 跳表的优点是查询效率高,时间复杂度为O(log N),与平衡二叉树相当。同时,跳表实现相对简单,不需要进行平衡操作,适用于实际应用中有序集合的存储和查询需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值