3

foreword

The jump table can achieve the same time complexity of O(logN) as the red-black tree, and the implementation is simple. The underlying data structure of the ordered set object in Redis uses the jump table. This article will learn the implementation of the skip table.

text

1. The basic concept of skip table

The skip list, that is, the skip list ( Skip List ), is based on the parallel linked list data structure, and the operation efficiency can reach O(logN) , which is friendly to concurrency. The schematic diagram of the skip list is shown below.

The characteristics of the skip table can be summarized as follows.

  • The skip list is a multi-level ( level ) linked list structure;
  • Each level in the skip list is an ordered linked list and is arranged in ascending order of elements (default);
  • The level in which the element in the skip list will appear is randomly determined, but as long as the element appears in the kth level, the element will also appear in the linked list below the k level;
  • The underlying linked list of the skip list contains all elements;
  • The head node and tail node of the jump table do not store elements, and the number of layers of the head node and the tail node is the maximum number of layers of the jump table;
  • The node in the jump table contains two pointers, one pointer points to the next node of the same-level linked list, and the other pointer points to the same element node of the lower-level linked list.

Take the skip table in the above figure as an example, if you want to find element 71, the search process is as shown in the following figure.

The search starts from the head node of the top-level linked list. When the node of element 71 is found, a total of 4 nodes are traversed. However, if the traditional linked list method is used (that is, the search starts from the head node of the bottom linked list of the skip list), then 7 nodes need to be traversed, so the skip table trades space for time, which shortens the time it takes to operate the skip table.

2. Nodes of the skip list

It is known that the nodes in the skip list need to have a pointer to the next node of the current layer linked list, and a pointer to the same element node of the lower linked list, so the nodes in the skip list are defined as follows.

 public class SkiplistNode {

    public int data;
    public SkiplistNode next;
    public SkiplistNode down;
    public int level;

    public SkiplistNode(int data, int level) {
        this.data = data;
        this.level = level;
    }

}

The above is the simplest way to define the nodes in the jump table. The stored element data is an integer, and the size of the element data is directly compared when comparing between nodes.

3. Initialization of the skip table

When the skip list is initialized, the head and tail nodes of each layer of linked list are created and the set is used to store the head and tail nodes. The number of layers of the head and tail nodes is randomly specified, and the number of layers of the head and tail nodes represents the current number of layers of the skip list. . After initialization, the skip table structure is shown below.

The relevant code for jump table initialization is shown below.

 public LinkedList<SkiplistNode> headNodes;
public LinkedList<SkiplistNode> tailNodes;

public int curLevel;

public Random random;

public Skiplist() {
    random = new Random();

    //headNodes用于存储每一层的头节点
    headNodes = new LinkedList<>();
    //tailNodes用于存储每一层的尾节点
    tailNodes = new LinkedList<>();

    //初始化跳表时,跳表的层数随机指定
    curLevel = getRandomLevel();
    //指定了跳表的初始的随机层数后,就需要将每一层的头节点和尾节点创建出来并构建好关系
    SkiplistNode head = new SkiplistNode(Integer.MIN_VALUE, 0);
    SkiplistNode tail = new SkiplistNode(Integer.MAX_VALUE, 0);
    for (int i = 0; i <= curLevel; i++) {
        head.next = tail;
        headNodes.addFirst(head);
        tailNodes.addFirst(tail);

        SkiplistNode headNew = new SkiplistNode(Integer.MIN_VALUE, head.level + 1);
        SkiplistNode tailNew = new SkiplistNode(Integer.MAX_VALUE, tail.level + 1);
        headNew.down = head;
        tailNew.down = tail;

        head = headNew;
        tail = tailNew;
    }
}

4. Add method of skip table

When adding each element to the skip list, it is first necessary to randomly specify the number of layers in the skip list for this element. It is necessary to expand the number of layers of the skip list, and expanding the number of layers of the skip list is to expand the number of layers of the head and tail nodes. The following is an add-on process that needs to expand the number of jump table layers.

In the initial state, the number of layers of the skip table is 2, as shown in the following figure.

Now we want to add element 120 to the skip list, and the randomly specified layer number is 3, which is greater than the current skip list level of 2. At this time, we need to expand the number of layers in the skip list, as shown in the following figure.

When inserting element 120 into the skip list, start from the top level and insert it down layer by layer, as shown in the following figure.

The code of the add method of the skip table is shown below.

 public void add(int num) {
    //获取本次添加的值的层数
    int level = getRandomLevel();
    //如果本次添加的值的层数大于当前跳表的层数
    //则需要在添加当前值前先将跳表层数扩充
    if (level > curLevel) {
        expanLevel(level - curLevel);
    }

    //curNode表示num值在当前层对应的节点
    SkiplistNode curNode = new SkiplistNode(num, level);
    //preNode表示curNode在当前层的前一个节点
    SkiplistNode preNode = headNodes.get(curLevel - level);
    for (int i = 0; i <= level; i++) {
        //从当前层的head节点开始向后遍历,直到找到一个preNode
        //使得preNode.data < num <= preNode.next.data
        while (preNode.next.data < num) {
            preNode = preNode.next;
        }

        //将curNode插入到preNode和preNode.next中间
        curNode.next = preNode.next;
        preNode.next = curNode;

        //如果当前并不是0层,则继续向下层添加节点
        if (curNode.level > 0) {
            SkiplistNode downNode = new SkiplistNode(num, curNode.level - 1);
            //curNode指向下一层的节点
            curNode.down = downNode;
            //curNode向下移动一层
            curNode = downNode;
        }
        //preNode向下移动一层
        preNode = preNode.down;
    }
}

private void expanLevel(int expanCount) {
    SkiplistNode head = headNodes.getFirst();
    SkiplistNode tail = tailNodes.getFirst();
    for (int i = 0; i < expanCount; i++) {
        SkiplistNode headNew = new SkiplistNode(Integer.MIN_VALUE, head.level + 1);
        SkiplistNode tailNew = new SkiplistNode(Integer.MAX_VALUE, tail.level + 1);
        headNew.down = head;
        tailNew.down = tail;

        head = headNew;
        tail = tailNew;

        headNodes.addFirst(head);
        tailNodes.addFirst(tail);
    }
}

5. Search method of skip list

When searching for an element in the skip list, you need to start from the top level and search down layer by layer. Follow the rules below when searching.

  • When the target value is greater than the value of the next node of the current node, continue to search backward on the linked list of this layer;
  • When the target value is greater than the current node value and less than the next node value of the current node, move down one layer and search backward from the same node position in the lower linked list;
  • The target value is equal to the current node value, and the search ends.

The following figure is a schematic diagram of a search process.

The code for the skip table search is shown below.

 public boolean search(int target) {
    //从顶层开始寻找,curNode表示当前遍历到的节点
    SkiplistNode curNode = headNodes.getFirst();
    while (curNode != null) {
        if (curNode.next.data == target) {
            //找到了目标值对应的节点,此时返回true
            return true;
        } else if (curNode.next.data > target) {
            //curNode的后一节点值大于target
            //说明目标节点在curNode和curNode.next之间
            //此时需要向下层寻找
            curNode = curNode.down;
        } else {
            //curNode的后一节点值小于target
            //说明目标节点在curNode的后一节点的后面
            //此时在本层继续向后寻找
            curNode = curNode.next;
        }
    }
    return false;
}

6. How to delete skip list

When a certain element needs to be deleted in the skip list, it is necessary to delete the node of this element in all layers. The specific deletion rules are as follows.

  • First, search for the node to be deleted according to the search method of the skip table. If it can be searched, the node to be deleted is located at the highest level of the node layer number;
  • From the top layer of the node to be deleted down, delete the node to be deleted at each layer. The deletion method is to make the node before the node to be deleted directly point to the node after the node to be deleted.

The following figure is a schematic diagram of the deletion process.

The code for the deletion of the skip table is shown below.

 public boolean erase(int num) {
    //删除节点的遍历过程与寻找节点的遍历过程是相同的
    //不过在删除节点时如果找到目标节点,则需要执行节点删除的操作
    SkiplistNode curNode = headNodes.getFirst();
    while (curNode != null) {
        if (curNode.next.data == num) {
            //preDeleteNode表示待删除节点的前一节点
            SkiplistNode preDeleteNode = curNode;
            while (true) {
                //删除当前层的待删除节点,就是让待删除节点的前一节点指向待删除节点的后一节点
                preDeleteNode.next = curNode.next.next;
                //当前层删除完后,需要继续删除下一层的待删除节点
                //这里让preDeleteNode向下移动一层
                //向下移动一层后,preDeleteNode就不一定是待删除节点的前一节点了
                preDeleteNode = preDeleteNode.down;

                //如果preDeleteNode为null,说明已经将底层的待删除节点删除了
                //此时就结束删除流程,并返回true
                if (preDeleteNode == null) {
                    return true;
                }

                //preDeleteNode向下移动一层后,需要继续从当前位置向后遍历
                //直到找到一个preDeleteNode,使得preDeleteNode.next的值等于目标值
                //此时preDeleteNode就又变成了待删除节点的前一节点
                while (preDeleteNode.next.data != num) {
                    preDeleteNode = preDeleteNode.next;
                }
            }
        } else if (curNode.next.data > num) {
            curNode = curNode.down;
        } else {
            curNode = curNode.next;
        }
    }
    return false;
}

7. The complete code of the skip table

The complete code for the skip table is shown below.

 public class Skiplist {

    public LinkedList<SkiplistNode> headNodes;
    public LinkedList<SkiplistNode> tailNodes;

    public int curLevel;

    public Random random;

    public Skiplist() {
        random = new Random();

        //headNodes用于存储每一层的头节点
        headNodes = new LinkedList<>();
        //tailNodes用于存储每一层的尾节点
        tailNodes = new LinkedList<>();

        //初始化跳表时,跳表的层数随机指定
        curLevel = getRandomLevel();
        //指定了跳表的初始的随机层数后,就需要将每一层的头节点和尾节点创建出来并构建好关系
        SkiplistNode head = new SkiplistNode(Integer.MIN_VALUE, 0);
        SkiplistNode tail = new SkiplistNode(Integer.MAX_VALUE, 0);
        for (int i = 0; i <= curLevel; i++) {
            head.next = tail;
            headNodes.addFirst(head);
            tailNodes.addFirst(tail);

            SkiplistNode headNew = new SkiplistNode(Integer.MIN_VALUE, head.level + 1);
            SkiplistNode tailNew = new SkiplistNode(Integer.MAX_VALUE, tail.level + 1);
            headNew.down = head;
            tailNew.down = tail;

            head = headNew;
            tail = tailNew;
        }
    }

    public boolean search(int target) {
        //从顶层开始寻找,curNode表示当前遍历到的节点
        SkiplistNode curNode = headNodes.getFirst();
        while (curNode != null) {
            if (curNode.next.data == target) {
                //找到了目标值对应的节点,此时返回true
                return true;
            } else if (curNode.next.data > target) {
                //curNode的后一节点值大于target
                //说明目标节点在curNode和curNode.next之间
                //此时需要向下层寻找
                curNode = curNode.down;
            } else {
                //curNode的后一节点值小于target
                //说明目标节点在curNode的后一节点的后面
                //此时在本层继续向后寻找
                curNode = curNode.next;
            }
        }
        return false;
    }

    public void add(int num) {
        //获取本次添加的值的层数
        int level = getRandomLevel();
        //如果本次添加的值的层数大于当前跳表的层数
        //则需要在添加当前值前先将跳表层数扩充
        if (level > curLevel) {
            expanLevel(level - curLevel);
        }

        //curNode表示num值在当前层对应的节点
        SkiplistNode curNode = new SkiplistNode(num, level);
        //preNode表示curNode在当前层的前一个节点
        SkiplistNode preNode = headNodes.get(curLevel - level);
        for (int i = 0; i <= level; i++) {
            //从当前层的head节点开始向后遍历,直到找到一个preNode
            //使得preNode.data < num <= preNode.next.data
            while (preNode.next.data < num) {
                preNode = preNode.next;
            }

            //将curNode插入到preNode和preNode.next中间
            curNode.next = preNode.next;
            preNode.next = curNode;

            //如果当前并不是0层,则继续向下层添加节点
            if (curNode.level > 0) {
                SkiplistNode downNode = new SkiplistNode(num, curNode.level - 1);
                //curNode指向下一层的节点
                curNode.down = downNode;
                //curNode向下移动一层
                curNode = downNode;
            }
            //preNode向下移动一层
            preNode = preNode.down;
        }
    }

    public boolean erase(int num) {
        //删除节点的遍历过程与寻找节点的遍历过程是相同的
        //不过在删除节点时如果找到目标节点,则需要执行节点删除的操作
        SkiplistNode curNode = headNodes.getFirst();
        while (curNode != null) {
            if (curNode.next.data == num) {
                //preDeleteNode表示待删除节点的前一节点
                SkiplistNode preDeleteNode = curNode;
                while (true) {
                    //删除当前层的待删除节点,就是让待删除节点的前一节点指向待删除节点的后一节点
                    preDeleteNode.next = curNode.next.next;
                    //当前层删除完后,需要继续删除下一层的待删除节点
                    //这里让preDeleteNode向下移动一层
                    //向下移动一层后,preDeleteNode就不一定是待删除节点的前一节点了
                    preDeleteNode = preDeleteNode.down;

                    //如果preDeleteNode为null,说明已经将底层的待删除节点删除了
                    //此时就结束删除流程,并返回true
                    if (preDeleteNode == null) {
                        return true;
                    }

                    //preDeleteNode向下移动一层后,需要继续从当前位置向后遍历
                    //直到找到一个preDeleteNode,使得preDeleteNode.next的值等于目标值
                    //此时preDeleteNode就又变成了待删除节点的前一节点
                    while (preDeleteNode.next.data != num) {
                        preDeleteNode = preDeleteNode.next;
                    }
                }
            } else if (curNode.next.data > num) {
                curNode = curNode.down;
            } else {
                curNode = curNode.next;
            }
        }
        return false;
    }

    private void expanLevel(int expanCount) {
        SkiplistNode head = headNodes.getFirst();
        SkiplistNode tail = tailNodes.getFirst();
        for (int i = 0; i < expanCount; i++) {
            SkiplistNode headNew = new SkiplistNode(Integer.MIN_VALUE, head.level + 1);
            SkiplistNode tailNew = new SkiplistNode(Integer.MAX_VALUE, tail.level + 1);
            headNew.down = head;
            tailNew.down = tail;

            head = headNew;
            tail = tailNew;

            headNodes.addFirst(head);
            tailNodes.addFirst(tail);
        }
    }

    private int getRandomLevel() {
        int level = 0;
        while (random.nextInt(2) > 1) {
            level++;
        }
        return level;
    }

}

Summarize

The time complexity of the skip table is the same as that of the AVL tree and the red-black tree, which can reach O(logN) , but the AVL tree needs to maintain a height balance, and the red-black tree needs to maintain an approximate height balance, which will cause the insertion or deletion of nodes. Some time overhead, so compared with AVL tree and red-black tree, skip table saves the time overhead of maintaining height balance, but also pays more space to store nodes of multiple layers, so skip table A table is a data structure that trades space for time.


半夏之沫
65 声望32 粉丝

引用和评论

0 条评论