banner
RustyNail

RustyNail

coder. 【blog】https://rustynail.me 【nostr】wss://ts.relays.world/ wss://relays.world/nostr

SkipList(跳表)

在看《Redis 设计与实现》的时候看到有涉及跳表(SkipList)的相关应用,就想了解一下。作记录如下。

跳表其实就是一个同时维护多条链表的数据结构。换句话说,就是为了加快查询速度,而占用更多的空间来储存数据,算是空间换时间了。

结构看起来是这样的:

image

结构#

在一般的链表里,会有 next 或这 forward/backward 这类的指针来指向其他节点,跳表
就厉害了

它有一排(level,redis 中跳表节点的 level 的数量是 1~32)这种指针,而且这些指针指向的节点的位置一般也不同(跨度不一样)。

节点定义#

一个最基本的节点的属性是这样的。Key 和 Value,以及前向指针的数量和保存他们的数组

public class Node<T> {
    private Long key;
    private T data;
    private int level;
    private Node<T>[] forwards;
}

数据结构#

SkipList 的结构包括头尾节点、最大的 level 高度,添加节点时,节点的高度由 getRandomLevel()方法提供

public class SkipList<T> {
    private int MAX_LEVEL;
    private Node<T> head;
    private Node<T> tail;

    /**
     * 构造函数
     *
     * @param level
     */
    public SkipList(int level) {
        
    }

    /**
     * 获取新节点的层数,
     * 不大于最大节点
     *
     * @return
     */
    public int getRandomLevel() {
        int count = 0;
        Random random = new Random(System.currentTimeMillis());
        while (Math.abs(random.nextLong()) % 2 == 1) {
            if (count < MAX_LEVEL - 1) {
                count += 1;
            } else {
                break;
            }
        }
        if (count > MAX_LEVEL - 1) {
            count = MAX_LEVEL - 1;
        }
        return count;
    }
}

添加数据#

添加数据的关键在于,找到添加的位置的前置节点,而且是,同时维护的这么多层的前置节点。

Node<T>[] update = new Node[MAX_LEVEL];
Node<T> tempHead = head;

一个用来保存前置节点的数组,和临时变量用于遍历

for (int i = MAX_LEVEL - 1; i >= 0; i--) {
   if (tempHead.getForwards()[i] == tail || tempHead.getForwards()[i].getKey() > key) {
     update[i] = tempHead;
   } else {
     while (tempHead.getForwards()[i] != tail && tempHead.getForwards()[i].getKey() < key) {
       tempHead = tempHead.getForwards()[i];
     }
     if (tempHead.getForwards()[i] != tail && tempHead.getForwards()[i].getKey() == key) {
       tempHead.getForwards()[i].setData(data);
       return;
     }
     update[i] = tempHead;
    }
}

从最高层开始遍历,最高层的跨越较快。当 当前节点的下一个节点是尾部或者 key 大于要插入的 key 时,将当前节点保存为这一层的结果。

保存完后进入下一层。

如果不符合上面的条件,就接着在那一层横向遍历 tempHead = tempHead.getForwards()[i];直到尾部或 key 大于目标 key,key 相等就覆盖然后提前返回。

然后保存结果 update[i] = tempHead;

最后更新链表

创建链表节点时,要由前向层数(level),
level 的大小不超过 Head 的大小,而且 level 的数值时按概率来的,这里是

  • 1 :1/2
  • 2 :1/4
  • 3 :1/8
  • ..

level 加一的概率是 1/2

redis 取的是 level: 1/4 和 MAX : 32

Node<T> node = new Node<>();
node.setKey(key);
node.setData(data);
node.setForwards(new Node[level + 1]);
int level = getRandomLevel();
node.setLevel(level);
/**
* 链接新节点
*/
for (int i = 0; i <= level; i++) {
    if (update[i] != null) {
        node.getForwards()[i] = update[i].getForwards()[i];
        update[i].getForwards()[i] = node;
    }
}

查找#

查找和插入差不多

横向和纵向分别遍历,找到就返回

public T getData(Long key) {
    Node<T> tempHead = head;
    for (int i = MAX_LEVEL - 1; i >= 0; i--) {
        if (tempHead.getForwards()[i] == tail || tempHead.getForwards()[i].getKey() > key) {
            continue;
        } else {
            while (tempHead.getForwards()[i] != tail && tempHead.getForwards()[i].getKey() < key) {
                tempHead = tempHead.getForwards()[i];
            }
            if (tempHead.getForwards()[i] != null) {
                if (tempHead.getForwards()[i].getKey() != null) {
                    if (tempHead.getForwards()[i].getKey().equals(key)) {
                        return tempHead.getForwards()[i].getData();
                    }
                }
            }
        }
    }
    return null;
}

复杂度#

由于每个节点的前向高度都是随机的,所以比较难算。

时间复杂度#
  • 最差是 O (n)
  • 查找 O (lgn) 的概率较高
空间复杂度#
  • S < 2n

被吊打#

和 java 的 HashMap 比了一下(膨胀), 被吊打了

image

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。