LeetCode 1206. 设计跳表

136 阅读5分钟

今天来挑战一道超有意思的 LeetCode 题目 ——1206. 设计跳表。这道题可不简单,但是别怕,有我在,带你一步步吃透它💪

相关文档

什么是跳表

跳表(Skip List)是一种在 (O(log(n))) 时间内完成增加、删除、搜索操作的数据结构。它就像是链表的超级进化版👾 相比于树堆与红黑树,功能和性能都差不多,关键是代码还更短,主打一个短小精悍!它的设计思想和链表很相似,但是却有着链表没有的高效。

代码实现

直接上代码,看看我是怎么实现这个跳表的。

class Skiplist {
    static int MAX_LEVEL = 32;
    Node head;
    int level;
    double p = 0.25;
    Random r = new Random();
    public Skiplist() {
        this.head = new Node(-1, MAX_LEVEL);
        this.level = 0;
    }
    public boolean search(int target) {
        Node curr = this.head;
        for (int i = level - 1; i >= 0; i--) {
            while (curr.forward[i]!= null && curr.forward[i].val < target) {
                curr = curr.forward[i];
            }
        }
        return curr.forward[0]!= null && curr.forward[0].val == target;
    }
    public void add(int num) {
        Node[] update = new Node[MAX_LEVEL];
        Arrays.fill(update, this.head);
        Node curr = this.head;
        for (int i = level - 1; i >= 0; i--) {
            while (curr.forward[i]!= null && curr.forward[i].val < num) {
                curr = curr.forward[i];
            }
            update[i] = curr;
        }
        int lvl = randomLvl();
        this.level = Math.max(this.level, lvl);
        Node newNode = new Node(num, lvl);
        for (int i = lvl - 1; i >= 0; i--) {
            newNode.forward[i] = update[i].forward[i];
            update[i].forward[i] = newNode;
        }
    }
    public boolean erase(int num) {
        Node[] update = new Node[MAX_LEVEL];
        Arrays.fill(update, this.head);
        Node curr = this.head;
        for (int i = level - 1; i >= 0; i--) {
            while (curr.forward[i]!= null && curr.forward[i].val < num) {
                curr = curr.forward[i];
            }
            update[i] = curr;
        }
        if (curr.forward[0] == null || curr.forward[0].val!= num) {
            return false;
        }
        Node target = curr.forward[0];
        for (int i = 0; i < MAX_LEVEL; i++) {
            if (update[i].forward[i] == null || update[i].forward[i].val!= target.val) {
                continue;
            }
            update[i].forward[i] = update[i].forward[i].forward[i];
        }
        while (this.level > 1 && this.head.forward[this.level - 1] == null) {
            this.level--;
        }
        return true;
    }
    private int randomLvl() {
        int lvl = 1;
        while (r.nextDouble() <= p && lvl < MAX_LEVEL) {
            lvl++;
        }
        return lvl;
    }
    static class Node {
        int val;
        Node[] forward;
        Node(int val, int level) {
            this.val = val;
            this.forward = new Node[level];
        }
    }
}

代码思路解析

初始化部分

static int MAX_LEVEL = 32;
Node head;
int level;
double p = 0.25;
Random r = new Random();
public Skiplist() {
    this.head = new Node(-1, MAX_LEVEL);
    this.level = 0;
}

这里定义了一些常量和变量。MAX_LEVEL 表示跳表的最大层数,head 是跳表的头节点,level 表示当前跳表的实际层数,p 是一个概率值,用于决定新节点的层数,r 是一个随机数生成器。在构造函数中,初始化了头节点,值为 -1,层数为 MAX_LEVEL,当前实际层数为 0。这就像是搭房子,先把地基(头节点)打好🧱

搜索方法 search(int target)

public boolean search(int target) {
    Node curr = this.head;
    for (int i = level - 1; i >= 0; i--) {
        while (curr.forward[i]!= null && curr.forward[i].val < target) {
            curr = curr.forward[i];
        }
    }
    return curr.forward[0]!= null && curr.forward[0].val == target;
}

从最高层开始,一层一层向下搜索。在每一层,只要当前节点的下一个节点存在且值小于目标值,就继续向前移动。这就像是在多层的高速公路上找出口,先在最上面的快车道找,如果没找到就去下一层慢车道找🚗 最后判断最底层的下一个节点是否是目标节点。

添加方法 add(int num)

public void add(int num) {
    Node[] update = new Node[MAX_LEVEL];
    Arrays.fill(update, this.head);
    Node curr = this.head;
    for (int i = level - 1; i >= 0; i--) {
        while (curr.forward[i]!= null && curr.forward[i].val < num) {
            curr = curr.forward[i];
        }
        update[i] = curr;
    }
    int lvl = randomLvl();
    this.level = Math.max(this.level, lvl);
    Node newNode = new Node(num, lvl);
    for (int i = lvl - 1; i >= 0; i--) {
        newNode.forward[i] = update[i].forward[i];
        update[i].forward[i] = newNode;
    }
}

首先,和搜索一样,从最高层开始找到每一层应该插入的位置,并记录下来(用 update 数组)。然后,通过 randomLvl 方法随机生成新节点的层数。接着,创建新节点,再从新节点的最高层开始,将新节点插入到记录的位置中。这就像是在多层书架上插书,先找到每层应该插的位置,然后把新书插进去📚

删除方法 erase(int num)

public boolean erase(int num) {
    Node[] update = new Node[MAX_LEVEL];
    Arrays.fill(update, this.head);
    Node curr = this.head;
    for (int i = level - 1; i >= 0; i--) {
        while (curr.forward[i]!= null && curr.forward[i].val < num) {
            curr = curr.forward[i];
        }
        update[i] = curr;
    }
    if (curr.forward[0] == null || curr.forward[0].val!= num) {
        return false;
    }
    Node target = curr.forward[0];
    for (int i = 0; i < MAX_LEVEL; i++) {
        if (update[i].forward[i] == null || update[i].forward[i].val!= target.val) {
            continue;
        }
        update[i].forward[i] = update[i].forward[i].forward[i];
    }
    while (this.level > 1 && this.head.forward[this.level - 1] == null) {
        this.level--;
    }
    return true;
}

还是先从最高层开始找到每一层要删除节点的前一个节点(用 update 数组记录)。如果最底层的下一个节点不是要删除的节点,就返回 false。然后,从最低层开始,将所有指向要删除节点的指针跳过该节点。最后,如果删除节点后,最高层没有节点了,就降低跳表的实际层数。这就像是从多层停车场里开走一辆车,先找到每层对应的停车位,然后把车开走,再看看停车场最高层是不是空了,如果空了就把最高层拆掉🚗

随机层数方法 randomLvl()

private int randomLvl() {
    int lvl = 1;
    while (r.nextDouble() <= p && lvl < MAX_LEVEL) {
        lvl++;
    }
    return lvl;
}

这个方法用于随机生成新节点的层数。每次生成一个 0 到 1 之间的随机数,如果小于概率 p 且当前层数小于 MAX_LEVEL,就增加层数。这就像是抽奖,每次都有一定概率(p)抽到升级卡,抽到了就升级,直到抽不到或者达到最大等级🎰

总结

通过这道题,我们深入了解了跳表这种神奇的数据结构,并且亲手实现了它。虽然过程有点烧脑,但是掌握之后是不是感觉自己又变强了呢😎 希望这篇文章能帮助大家更好地理解跳表,一起在算法的世界里乘风破浪!如果有任何问题,欢迎在评论区留言哦~