跳表

单向链表
双向链表
循环链表
跳表
异或链表

示例练习>>>

我们知道如果一个数组是有序的,查询的时候可以使用二分法进行查询,时间复杂度可以降到 O(logn) ,但如果链表是有序的,我们仍然是从前往后一个个查找,这样显然很慢,这个时候我们可以使用跳表(Skip list),跳表就是多层链表,每一层链表都是有序的,最下面一层是原始链表,包含所有数据,从下往上节点个数逐渐减少,如下图所示。

跳表的特性:

  • 一个跳表有若干层链表组成;
  • 每一层链表都是有序的;
  • 跳表最下面一层的链表包含所有数据;
  • 如果一个元素出现在某一次层,那么该层下面的所有层都必须包含该元素;
  • 上一层的元素指向下层的元素必须是相同的;
  • 头指针 head 指向最上面一层的第一个元素;

跳表节点的插入

如果是单链表只需要找出待插入节点的前一个节点即可,但在跳表中不光要插入到原始链表中,在他上面的某些层也有可能需要插入,实现方式有随机性和确定性两种选择,其中随机性一般比较常见。比如要在跳表中插入一个元素,可以随机生成一个数字 level ,从 level 层往下每层都要插入。所以跳表中节点不光有 next 属性,还有 down 属性,他指向下一个节点的引用,先来看下跳表的节点类。

// 跳表的节点类。
public class SkipListNode {
    // 跳表节点的值,在实际应用中节点类可以加个泛型,这里为了方便介绍,直接使用 int 类型。
    public int val;
    public SkipListNode next;// 指向后面一个节点。
    public SkipListNode down;// 指向下面一层的相同节点。

    public SkipListNode(int val, SkipListNode next) {
        this.val = val;
        this.next = next;
    }
}

在跳表中我们还需要定义一个最大值 MAX_LEVEL ,就是跳表的最大层级数不能超过这个值。我们再来看下索引层级的随机函数,他主要用于在插入节点的时候从第几层开始插入,他是随机的,越往上机率越小,这也符合跳表的特性,越往上节点越少,最大值不能超过 MAX_LEVEL 。

// 索引层级随机函数。
private int randLevel() {
    int level = 1;// 1 的概率是0.5,2的概率是0.25,3的概率是0.125,4的概率是0.0625,……
    // Math.random()每次会生成一个 0 到 1 之间的随机数
    while (Math.random() < 0.5f && level < MAX_LEVEL)
        level++;
    return level;
}

在跳表中每一层都有一个头节点,头节点不存储任何数据,其中 head 是最上面一层的头节点,跳表的插入,删除以及查找都是从他开始的,如果想要获取下面一层的头节点,可以通过 head.down 获取。跳表的插入总共分为 3 步:

第一步:在跳表节点插入之前先判断上面的层级有没有创建,如果没有创建,需要先创建,如下图所示。

第二步:如果创建了层级或者插入的层级小于跳表的层数,需要找到每一层待插入节点的前一个节点,如下图所示。

第三步:从上往下插入节点,链表的插入可以参考单向链表,插入的节点除了连接 next 指针以外,还要连接 down 指针。

来看下代码:

public void add(int num) {
    int level = randLevel();// 从第几层开始插入,随机数。
    // 记录待插入节点的前一个节点。
    SkipListNode[] preNodes = new SkipListNode[level];

    // 第一步:如果跳表层数比较少,在上面添加,层数至少为 level 。
    if (curLevelCount < level) {
        SkipListNode beforeHead = head;
        head = new SkipListNode(-1, null);// 更新 head 节点。
        SkipListNode curHead = head;
        // 在上面添加每层的头节点。
        for (int i = curLevelCount; i < level - 1; i++) {
            SkipListNode node = new SkipListNode(-1, null);
            curHead.down = node;
            curHead = node;
        }
        // 最后创建的链表头节点和之前的头节点连在一起。
        curHead.down = beforeHead;
    }

    // 第二步:从上往下查找每层待插入节点的前一个节点。
    SkipListNode pre = head;
    // 上层不需要插入的跳过。
    for (int i = curLevelCount - 1; i >= level; i--)
        pre = pre.down;
    // 从当前层往下每层都要插入该节点,找出每层待插入节点的前一个节点。
    for (int i = level - 1; i >= 0; i--) {
        while (pre.next != null && pre.next.val < num)
            pre = pre.next;
        preNodes[i] = pre;// 记录前一个节点。
        pre = pre.down;
    }

    // 第三步:节点插入,插入的时候不光有 next 指针,而且还有 down 指针。
    SkipListNode topNode = null;
    // 把新建节点 node 插到该层下面的每一层。
    for (int i = level - 1; i >= 0; i--) {
        // 创建新节点。
        SkipListNode node = new SkipListNode(num, preNodes[i].next);
        // 链表的插入,参见单向链表的插入。
        preNodes[i].next = node;
        // 上下也要连接。
        if (topNode != null)
            topNode.down = node;
        topNode = node;
    }
    if (level > curLevelCount)// 更新跳表的层级,用来记录当前跳表的层级。
        curLevelCount = level;
}

跳表的查询

跳表的查询从最上面一层开始,每层往后查找,如果后面没有节点了或者后面的节点值比要查找的大,就往下面查找,如果找到返回 true ,如果没找到返回 false ,如下图所示。

来看下代码:

// 查找值为 target 的节点。
public boolean search(int target) {
    SkipListNode pre = head;
    while (pre != null) {
        // 如果当前节点值小于 target ,需要到右边查找,如果右边没有节点就到下边查找。
        if (pre.val < target) {
            if (pre.next == null)// 右边没有节点,到下边查找
                pre = pre.down;
            else
                pre = pre.next.val > target ? pre.down : pre.next;
        } else if (pre.val == target) {// 如果找到直接返回。
            return true;
        } else {
            return false;// 如果当前节点值大于 target ,说明没有,直接返回 false 。
        }
    }
    return false;
}

跳表节点的删除

节点的删除和添加类似,也是先从上往下找到待删除节点的前一个节点,因为单链表中节点的添加和删除都需要前一个节点。找到之后从当前层往下每一层都要删除,如果上面一层的节点都被删除完了,还需要把上层的链表清空,如下图所示。

来看下代码:

public boolean remove(int num) {
    // 删除链表和插入链表类似,都是需要先找到插入或删除链表的前一个节点。
    int topIndex = -1;// 从当前层开始往下每层都要删除。
    // 查找待删除节点的前一个节点,从上面一层开始查找。
    SkipListNode pre = head;
    for (int i = curLevelCount - 1; i >= 0; i--) {
        while (pre.next != null && pre.next.val < num)
            pre = pre.next;
        // 如果找到就终止查找,表示在当前层以及他下面的所有层都要删除该节点。
        if (pre.next != null && pre.next.val == num && topIndex == -1) {
            topIndex = i;
            break;
        }
        if (pre.down == null)// 如果跳表中没有要删除的节点,返回 false 。
            return false;
        pre = pre.down;// 当前层没找到就往下一层继续查找。
    }

    if (topIndex == -1)// 如果跳表中没找到要删除的节点,返回 false 。
        return false;

    // 从 topIndex 层开始,他下面的每一层都要删除。
    for (int i = topIndex; i >= 0; i--) {
        if (pre.next != null)// 节点删除,参考单向链表删除。
            pre.next = pre.next.next;
        pre = pre.down;// 继续下一层的删除。
        if (pre != null)// 找到待删除节点的前一个节点。
            while (pre.next != null && pre.next.val != num)
                pre = pre.next;
    }
    // 如果上面一层的节点被删除完了,要更新 curLevelCount 的值 ,还要更新 head节点。
    SkipListNode cur = head;
    while (curLevelCount > 1 && cur.next == null) {
        cur = cur.down;
        head = cur;
        curLevelCount--;
    }
    return true;
}

最后我们再来看下完整代码:

public class SkipList1 {

    // 打印跳表。
    private static void printLinked(SkipList1 skipList) {
        SkipListNode cur = skipList.head;
        while (cur != null) {
            SkipListNode tmp = cur.next;
            while (tmp != null) {
                System.out.print(tmp.val + ",");
                tmp = tmp.next;
            }
            System.out.println();
            cur = cur.down;
        }
    }

    // 跳表的最大层数。
    private int MAX_LEVEL = 16;

    // 当前跳表的最大层级。
    private int curLevelCount = 1;

    // 初始化定义一个头节点,指向最上面一层的第一个元素,头节点不存储任何数据。
    private SkipListNode head;

    public SkipList1() {// 构造函数。
        head = new SkipListNode(-1, null);
    }

    // 查找值为 target 的节点。
    public boolean search(int target) {
        SkipListNode pre = head;
        while (pre != null) {
            // 如果当前节点值小于 target ,需要到右边查找,如果右边没有节点就到下边查找。
            if (pre.val < target) {
                if (pre.next == null)// 右边没有节点,到下边查找
                    pre = pre.down;
                else
                    pre = pre.next.val > target ? pre.down : pre.next;
            } else if (pre.val == target) {// 如果找到直接返回。
                return true;
            } else {
                return false;// 如果当前节点值大于 target ,说明没有,直接返回 false 。
            }
        }
        return false;
    }

    public void add(int num) {
        int level = randLevel();// 从第几层开始插入,随机数。
        // 记录待插入节点的前一个节点。
        SkipListNode[] preNodes = new SkipListNode[level];

        // 第一步:如果跳表层数比较少,在上面添加,层数至少为 level 。
        if (curLevelCount < level) {
            SkipListNode beforeHead = head;
            head = new SkipListNode(-1, null);// 更新 head 节点。
            SkipListNode curHead = head;
            // 在上面添加每层的头节点。
            for (int i = curLevelCount; i < level - 1; i++) {
                SkipListNode node = new SkipListNode(-1, null);
                curHead.down = node;
                curHead = node;
            }
            // 最后创建的链表头节点和之前的头节点连在一起。
            curHead.down = beforeHead;
        }

        // 第二步:从上往下查找每层待插入节点的前一个节点。
        SkipListNode pre = head;
        // 上层不需要插入的跳过。
        for (int i = curLevelCount - 1; i >= level; i--)
            pre = pre.down;
        // 从当前层往下每层都要插入该节点,找出每层待插入节点的前一个节点。
        for (int i = level - 1; i >= 0; i--) {
            while (pre.next != null && pre.next.val < num)
                pre = pre.next;
            preNodes[i] = pre;// 记录前一个节点。
            pre = pre.down;
        }

        // 第三步:节点插入,插入的时候不光有 next 指针,而且还有 down 指针。
        SkipListNode topNode = null;
        // 把新建节点 node 插到该层下面的每一层。
        for (int i = level - 1; i >= 0; i--) {
            // 创建新节点。
            SkipListNode node = new SkipListNode(num, preNodes[i].next);
            // 链表的插入,参见单向链表的插入。
            preNodes[i].next = node;
            // 上下也要连接。
            if (topNode != null)
                topNode.down = node;
            topNode = node;
        }
        if (level > curLevelCount)// 更新跳表的层级,用来记录当前跳表的层级。
            curLevelCount = level;
    }

    public boolean remove(int num) {
        // 删除链表和插入链表类似,都是需要先找到插入或删除链表的前一个节点。
        int topIndex = -1;// 从当前层开始往下每层都要删除。
        // 查找待删除节点的前一个节点,从上面一层开始查找。
        SkipListNode pre = head;
        for (int i = curLevelCount - 1; i >= 0; i--) {
            while (pre.next != null && pre.next.val < num)
                pre = pre.next;
            // 如果找到就终止查找,表示在当前层以及他下面的所有层都要删除该节点。
            if (pre.next != null && pre.next.val == num && topIndex == -1) {
                topIndex = i;
                break;
            }
            if (pre.down == null)// 如果跳表中没有要删除的节点,返回 false 。
                return false;
            pre = pre.down;// 当前层没找到就往下一层继续查找。
        }

        if (topIndex == -1)// 如果跳表中没找到要删除的节点,返回 false 。
            return false;

        // 从 topIndex 层开始,他下面的每一层都要删除。
        for (int i = topIndex; i >= 0; i--) {
            if (pre.next != null)// 节点删除,参考单向链表删除。
                pre.next = pre.next.next;
            pre = pre.down;// 继续下一层的删除。
            if (pre != null)// 找到待删除节点的前一个节点。
                while (pre.next != null && pre.next.val != num)
                    pre = pre.next;
        }
        // 如果上面一层的节点被删除完了,要更新 curLevelCount 的值 ,还要更新 head节点。
        SkipListNode cur = head;
        while (curLevelCount > 1 && cur.next == null) {
            cur = cur.down;
            head = cur;
            curLevelCount--;
        }
        return true;
    }

    // 索引层级随机函数。
    private int randLevel() {
        int level = 1;
        // Math.random()每次会生成一个 0 到 1 之间的随机数
        while (Math.random() < 0.5f && level < MAX_LEVEL)
            level++;
        return level;
    }


    // 跳表的节点类。
    public class SkipListNode {
        // 跳表节点的值,在实际应用中节点类可以加个泛型,这里为了方便介绍,直接使用 int 类型。
        public int val;
        public SkipListNode next;// 指向后面一个节点。
        public SkipListNode down;// 指向下面一层的相同节点。

        public SkipListNode(int val, SkipListNode next) {
            this.val = val;
            this.next = next;
        }
    }
}

上面代码中虽然上下两个节点的值是一样的,但我们还是创建了不同的对象,实际上我们只需要创建一个对象即可,上下关系不在是 down ,而是一个数组。

来看下代码:

public class SkipList2 {

// 打印跳表的值,由于level是随机生成的,所以同样的数据每次打印都会不一样,
// 如果想调试,可以让level变成一个固定值,这样同样的数据每次打印结果都会一样了。
private static void printLinked(SkipList2 skipList) {
    SkipListNode cur = skipList.head;
    for (int i = skipList.curLevelCount - 1; i >= 0; i--) {
        // 这里是横向查找,就是当前层往后面查找,这里是第 i 层。
        SkipListNode pre = cur;
        while (pre.next[i] != null) {
            System.out.print(pre.next[i].val + ",");
            pre = pre.next[i];
        }
        System.out.println();
    }

}

// 跳表的最大层数。
private int MAX_LEVEL = 16;

// 当前跳表的最大层级。
private int curLevelCount = 1;

// 初始化定义一个头节点,指向最上面一层的第一个元素,头节点不存储任何数据。
private SkipListNode head;

public SkipList2() {// 构造函数。
    head = new SkipListNode(-1);
}

// 查找值为 target 的节点。
public boolean search(int target) {
    SkipListNode pre = head;
    // 这里for循环是逆序的,他是从最上面一层开始查找,for 循环是纵向查找,里面的while循环是横向查找。
    for (int i = curLevelCount - 1; i >= 0; i--) {
        // 这里是横向查找,就是当前层往后面查找,这里是第 i 层。
        while (pre.next[i] != null && pre.next[i].val < target)
            pre = pre.next[i];
        // 如果在当前层查找到,直接返回true。
        if (pre.next[i] != null && pre.next[i].val == target)
            return true;
    }
    return false;// 没查找到,返回false。
}

// 添加数据
public void add(int num) {
    int level = randLevel();// 需要插入第几层。
    // 每一层初始化默认为head节点。
    SkipListNode[] preNodes = new SkipListNode[level];
    for (int i = 0; i < level; i++)
        preNodes[i] = head;

    // 找到每一层待插节点的前一个节点。
    SkipListNode pre = head;
    // 从当前层往下每层都要插入该节点。
    for (int i = level - 1; i >= 0; i--) {
        while (pre.next[i] != null && pre.next[i].val < num)
            pre = pre.next[i];
        preNodes[i] = pre;
    }

    // 创建新节点。
    SkipListNode node = new SkipListNode(num);
    // 把新建节点 node 插到该层以及下面的所有层。
    for (int i = level - 1; i >= 0; i--) {
        // 链表的插入,参见单向链表的插入。
        node.next[i] = preNodes[i].next[i];
        preNodes[i].next[i] = node;
    }
    if (level > curLevelCount)// 更新跳表的层级。
        curLevelCount = level;
}

public boolean remove(int num) {
    // 删除链表和插入链表类似,都是需要先找到插入或删除链表的前一个节点。
    SkipListNode[] preNodes = new SkipListNode[curLevelCount];
    int topIndex = -1;// 从当前层往下都要移除。
    // 查找待删除节点的前一个节点,从最上面一层开始查找。
    SkipListNode pre = head;
    for (int i = curLevelCount - 1; i >= 0; i--) {
        while (pre.next[i] != null && pre.next[i].val < num)
            pre = pre.next[i];
        if (pre.next[i] != null && pre.next[i].val == num && topIndex == -1)
            topIndex = i;
        preNodes[i] = pre;// 记录每层待删除节点的前一个节点。
    }
    if (topIndex == -1)// 如果没找到,也就是待删除的节点在跳表中不存在,直接返回。
        return false;

    // 删除操作。
    for (int i = topIndex; i >= 0; i--)
        preNodes[i].next[i] = preNodes[i].next[i].next[i];
    // 更新索引层数,如果当前层消失了,curLevelCount要减 1 。
    while (curLevelCount > 1 && head.next[curLevelCount - 1] == null)
        curLevelCount--;
    return true;
}

// 索引层级随机函数。
private int randLevel() {
    int level = 1;// 1 的概率是0.5,2的概率是0.25,3的概率是0.125,4的概率是0.0625,……
    // Math.random()每次会生成一个 0 到 1 之间的随机数
    while (Math.random() < 0.5f && level < MAX_LEVEL)
        level++;
    return level;
}


// 跳表的节点。
public class SkipListNode {
    // 跳表节点的值,在实际应用中节点类可以加个泛型,这里为了方便介绍,直接使用 int 类型。
    public int val;

    public SkipListNode(int val) {
        this.val = val;
    }

    // 普通的链表这里是 next 节点,而跳表这里需要记录每一层的节点,所以是数组。
    public SkipListNode[] next = new SkipListNode[MAX_LEVEL];
}

参考:

ConcurrentSkipListMap

ConcurrentSkipListSet

测试用例:

1206. Design Skiplist

1206. 设计跳表

相关链接

数组
数组滚动数组差分数组树状数组
链表
链单向表双向链表循环链表跳表异或链表
队列
队列循环队列双端队列
散列表
散列表
二叉树二叉搜索树AVL树红黑树字典树哈夫曼树线段树笛卡尔树
图的介绍图的遍历Dijkstra算法Bellman-Ford算法SPFA算法Floyd算法Prim算法Kruskal算法Boruvka算法拓扑排序

信奥赛编程(刷题请进)>>>

经过两年的打磨,我的新作《算法秘籍》已经出版,有需要的可以点击购买。也可以点击 内容介绍 查看详情。