链表-跳表【node6】

【1024程序员节征文】以 1024 之名,写我与代码的「双向奔赴」 10w+人浏览 1.2k人参与

什么是跳表

跳表(Skip List)是一种随机化的数据结构,基于并联的有序链表,其效率可比拟于二叉搜索树(如红黑树、AVL树等)。跳表的平均查找和插入时间复杂度都是O(log n),但与平衡树相比,跳表的实现更为简单,且常数因子更小。

跳表是对有序链表的一种扩展,通过维护一系列分层的链表,并在每一层中跳过部分元素,从而加快查找速度。跳表上层的链表作为下层链表的"快速通道",使得在查找时可以先在高层链表中进行大步跳跃,再在底层链表中进行精确定位。

为什么需要跳表?

传统的有序链表虽然插入和删除操作比较方便(只需要修改指针),但查找操作需要从头开始遍历,时间复杂度为 O(n)。如果链表很长,查找效率会非常低。

平衡树(如 AVL 树、红黑树)可以实现 O(logn) 的查找、插入和删除,但它们的实现比较复杂,需要进行旋转等操作来维护树的平衡。

跳表提供了一种折衷方案:它在实现复杂度上接近链表,但在性能上接近平衡树。

基本原理

基本结构

跳表由多层链表组成,每一层都是一个有序链表,底层包含所有元素,而上层则是下层的子集。具体来说:

  • 最底层(Level 0)是一个普通的有序链表,包含所有元素
  • 第一层(Level 1)大约包含每两个元素中的一个
  • 第二层(Level 2)大约包含每四个元素中的一个
  • 依此类推,第i层大约包含每2^i个元素中的一个

节点结构

每个跳表节点包含:

  • 值(key):节点存储的实际数据
  • 多个前向指针:指向同层的下一个节点
  • 层数(level):决定节点在哪些层出现
/**
* 跳表节点类
*/
class SkipListNode {
  Integer key;
  SkipListNode[] forward;

  public SkipListNode(Integer key, int level) {
      this.key = key;
      this.forward = new SkipListNode[level + 1];
  }

  @Override
  public String toString() {
      return "SkipListNode(key=" + key + ")";
  }
}

跳表核心操作

随机层数的生成

跳跳表使用类似于抛硬币的方式来决定一个新节点的层数:

  1. 新节点默认在最底层(Level 0)出现
  2. 使用概率参数p(通常为0.5或0.25)决定是否上升到更高层
  3. 生成一个随机数r在[0,1]范围内
  4. 如果r < p,则层数加1,节点会出现在Level 1
  5. 继续生成随机数和比较,直到某次r >= p或达到最大层数

这种随机层数生成机制的特点是:

  • 每个节点至少在Level 0层出现
  • 每一层上的节点数量大约是下一层的一半(当p=0.5时)
  • 最底层包含所有节点,向上每层节点数量递减
  • 平均来说,大约有1/2的节点会出现在Level 1
  • 大约有1/4的节点会出现在Level 2
  • 大约有1/8的节点会出现在Level 3
  • 以此类推…
private int randomLevel() {
    int level = 0;
    while (random.nextDouble() < P && level < MAX_LEVEL) {
        level++;
    }
    return level;
}

搜索操作

示例代码

跳表的搜索从最高层开始,然后逐层向下:

  1. 从头节点的最高层开始
  2. 如果当前节点的前向指针指向的节点值小于目标值,则向前移动
  3. 否则,降低一层继续搜索
  4. 重复上述过程,直到达到最底层并找到目标值或确定其不存在
public SkipListNode search(int key) {
    SkipListNode current = this.header;
    //从最高层开始,逐层向下查找
    for (int i = this.level; i >= 0; i--) {
        //在当前层水平移动,知道找到小于或等于目标值的最大节点
        while (current.forward[i] != null && current.forward[i].key < key) {
            current = current.forward[i];
        }
    }
    //现在在底层,检查下一个节点是否是目标值
    current = current.forward[0];
    //如果下个节点存在并且key相等,则找到目标
    if (current != null && current.key == key) {
        return current;
    }
    return null;//未找到目标
}

查找步骤

查找值为12的节点的过程。搜索从头节点的最高层开始,逐层向下进行。

步骤一从最高层开始搜索

  • 搜索从跳表的最高层(Level 2)的头节点开始。我们要查找值为12的节点。
  • 当前位置:Level 2的Head节点
  • 目标:找到值为12的节点

步骤二在最高层水平移动到合适位置

  • 在Level 2层,Head的next指向值为7的节点。因为7 < 12,所以我们移动到值为7的节点。
  • 当前位置:Level 2的节点7
  • 判断:7 < 12,可以继续向右移动

步骤三当前层无法继续前进,降低层级

  • 在Level 2层,节点7的next指向值为18的节点。因为18 > 12,所以我们无法继续向右移动。此时需要降到Level 1层继续搜索。
  • 当前位置:Level 1的节点7
  • 判断:在Level 2中,下一个节点值18 > 12,无法继续水平移动,需要降级

步骤四在中间层继续搜索

  • 在Level 1层,节点7的next指向值为12的节点。因为12 = 12,我们找到了目标值,但仍然需要降到最底层以确认节点是否存在于最底层。
  • 当前位置:Level 1的节点12
  • 判断:12 = 12,找到目标值,继续降级到最底层确认

步骤五在最底层确认结果

  • 在Level 0(最底层),我们确认值为12的节点确实存在。搜索成功完成。
  • 当前位置:Level 0的节点12
  • 结果:成功找到目标节点12

搜索路径总结:

  1. 从Level 2的Head节点开始
  2. 移动到Level 2的节点7
  3. 发现下一个节点18 > 12,降到Level 1
  4. 在Level 1找到节点12,等于目标值
  5. 降到Level 0确认节点12存在
  6. 搜索成功完成

搜索操作的时间复杂度为O(log n),因为我们利用了跳表的分层结构,每一层大约跳过了一半的节点。

插入操作

示例代码

跳表的插入过程包括:

  1. 搜索合适的插入位置,同时记录每一层需要更新的节点
  2. 为新节点随机生成一个层数
  3. 如果新节点的层数高于当前跳表的层数,更新跳表层数
  4. 更新所有受影响层的前向指针
public boolean insert(int key) {
    // 创建更新数组,用于存储需要更新前向指针的节点
    SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
    SkipListNode current = this.header;
    // 从最高层开始,查找适合的插入位置
    for (int i = this.level; i >= 0; i--) {
        while (current.forward[i] != null && current.forward[i].key < key) {
            current = current.forward[i];
        }
        // 记录每一层需要更新的节点
        update[i] = current;
    }
    // 移动到下一个节点
    current = current.forward[0];
    // 检查key是否已经存在
    if (current != null && current.key == key) {
        return false;// key已存在,则插入失败
    }
    // 为新节点生成随机层数
    int randomLevel = randomLevel();
    if (randomLevel > this.level) {
        // 更新跳表当前最大层数
        for (int i = this.level + 1; i <= randomLevel; i++) {
            update[i] = this.header;
        }
        this.level = randomLevel;
    }
    // 创建新的节点
    SkipListNode newNode = new SkipListNode(key, randomLevel);
    // 更新所有受影响的前向指针
    for (int i = 0; i <= randomLevel; i++) {
        newNode.forward[i] = update[i].forward[i];
        update[i].forward[i] = newNode;
    }
    return true;
}

插入步骤

如下在跳表中插入值为15的新节点的过程。插入操作包括查找位置、记录更新路径、生成随机层数和更新指针。

步骤一查找插入位置并记录更新路径

  • 我们要插入值为15的新节点。首先需要从最高层开始,找到合适的插入位置,并记录每层需要更新的节点。
  • 当前位置:Level 2的Head节点
  • 目标:找到值为15应该插入的位置

步骤二**Level 2层搜索**:

  • 在Level 2层,当前位置是节点7。其下一个节点是18,因为18 > 15,所以我们不能继续向右移动。记录节点7为Level 2层需要更新的节点。
  • 当前位置:Level 2的节点7
  • 判断:下一个节点18 > 15,无法继续右移,记录update[2] = 7
  • 降级到Level 1继续搜索

步骤三**Level 1层搜索**:

  • 在Level 1层,我们移动到值为12的节点。下一个节点是18,因为18 > 15,所以记录节点12为Level 1层需要更新的节点。
  • 当前位置:Level 1的节点12
  • 判断:下一个节点18 > 15,无法继续右移,记录update[1] = 12
  • 降级到Level 0继续搜索

步骤四**Level 0层搜索**:

  • 在Level 0层,我们当前在节点12。下一个节点是18,因为18 > 15,所以记录节点12为Level 0层需要更新的节点。
  • 当前位置:Level 0的节点12
  • 判断:下一个节点18 > 15,无法继续右移,记录update[0] = 12
  • 此时我们已经找到了每一层需要更新的节点:
    • Level 2: 节点7
    • Level 1: 节点12
    • Level 0: 节点12

步骤五**为新节点生成随机层数**:

  • 接下来,我们需要随机生成新节点的层数。假设随机生成的层数为1,那么新节点将出现在Level 0和Level 1层中。
  • 随机生成层数:1
  • 这意味着新节点15将出现在Level 0和Level 1层中

步骤六**创建新节点并在Level 0层插入**:

  • 我们创建一个值为15的新节点,并在Level 0层插入。
  • 更新Level 0层的指针:
    • 新节点15的next指向原来12节点指向的18节点
    • 12节点的next指向新的15节点

步骤七**在Level 1层插入**:

  • 接下来,我们在Level 1层插入新节点。
  • 更新Level 1层的指针:
    • 新节点15的next指向原来12节点指向的18节点
    • 12节点的next指向新的15节点

步骤八**插入完成**:

  • 插入操作完成,新节点15已成功插入到跳表中的Level 0和Level 1层。
  • 结果:
    • 新节点15出现在Level 0和Level 1层
    • 所有相关的指针都已更新
    • 跳表的结构保持有序

插入操作总结:

  1. 首先从最高层开始,查找合适的插入位置,并记录每一层需要更新的节点
  2. 检查目标值是否已存在于跳表中,如果存在则插入失败
  3. 为新节点随机生成一个层数
  4. 创建新节点,并更新所有受影响层的前向指针
  5. 如果新节点的层数高于当前跳表的层数,则更新跳表的层数

插入操作的平均时间复杂度为O(log n),因为查找位置的时间复杂度是O(log n),而更新指针的操作是O(L),其中L是新节点的层数,平均为常数。

删除操作

示例代码

跳表的删除过程与插入类似:

  1. 搜索目标节点,同时记录每一层需要更新的节点
  2. 找到目标节点后,更新所有受影响的前向指针
  3. 如果删除后某层变为空,则减少跳表的层数
public boolean delete(int key) {
    // 创建更新数组
    SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
    SkipListNode current = this.header;
    // 从最高层开始,查找目标节点
    for (int i = this.level; i >= 0; i--) {
        while (current.forward[i] != null && current.forward[i].key < key) {
            current = current.forward[i];
        }
        update[i] = current;
    }
    // 移动到下一节点
    current = current.forward[0];
    // 如果找到目标节点
    if (current == null || current.key != key) {
        return false;
    }
    //从最基层开始,更新所有收到影响层的前向指针
    for (int i = 0; i <= this.level; i++) {
        if (update[i].forward[i] != current) {
            break;
        }
        update[i].forward[i] = current.forward[i];
    }
    // 删除没有元素的层
    while (this.level > 0 && this.header.forward[this.level] == null) {
        this.level--;
    }
    return true;//没有找到目标节点
}

删除步骤

如下是删除值为12的节点的过程。删除操作包括查找目标节点、记录更新路径和更新指针。

步骤一 查找目标节点并记录更新路径

  • 我们要删除值为12的节点。首先需要从最高层开始,找到目标节点,并记录每层需要更新的节点(前驱节点)。
  • 当前位置:Level 2的Head节点
  • 目标:找到值为12的节点并记录每层的前驱节点

步骤二**Level 2层搜索**:

  • 在Level 2层,当前位置是节点7。其下一个节点是18,因为18 > 12,所以我们不能继续向右移动。记录节点7为Level 2层需要更新的节点。
  • 当前位置:Level 2的节点7
  • 判断:下一个节点18 > 12,无法继续右移,记录update[2] = 7
  • 降级到Level 1继续搜索

步骤三**Level 1层搜索**:

  • 在Level 1层,当前位置是节点7。下一个节点是12,因为12 = 12,我们找到了目标节点的前驱节点。记录节点7为Level 1层需要更新的节点。
  • 当前位置:Level 1的节点7
  • 判断:下一个节点12 = 12(是目标节点),记录update[1] = 7
  • 降级到Level 0继续搜索

步骤四Level 0层搜索

  • 在Level 0层,我们移动到节点9。下一个节点是12,因为12 = 12,我们找到了目标节点的前驱节点。记录节点9为Level 0层需要更新的节点。
  • 当前位置:Level 0的节点9
  • 判断:下一个节点12 = 12(是目标节点),记录update[0] = 9
  • 此时我们已经找到了每一层需要更新的节点:
    • Level 2: 节点7(虽然Level 2没有节点12,但为了完整性记录前驱)
    • Level 1: 节点7
    • Level 0: 节点9

步骤五**更新Level 0层的指针**:

  • 找到目标节点12后,我们开始更新指针。首先更新Level 0层的指针:
    • 节点9的next指向原来12节点指向的15节点

步骤六**更新Level 1层的指针**:

  • 继续更新Level 1层的指针:
    • 节点7的next指向原来12节点指向的15节点
  • Level 2层没有节点12,所以无需更新。

步骤七**删除完成**:

  • 删除操作完成,节点12已从跳表中移除。所有相关的指针都已更新,跳表的结构保持有序。
  • 结果:
    • 节点12已从Level 0和Level 1层移除
    • 所有相关的指针都已更新
    • 跳表的结构保持完整和有序

删除操作总结:

  1. 首先从最高层开始,查找目标节点,并记录每一层需要更新的节点(前驱节点)
  2. 检查目标节点是否存在于跳表中,如果不存在则删除失败
  3. 如果找到目标节点,从底层开始,更新所有受影响层的前向指针
  4. 检查并删除没有元素的最高层

删除操作的平均时间复杂度为O(log n),因为查找节点的时间复杂度是O(log n),而更新指针的操作与节点的层数相关,平均为常数。

复杂度分析

时间复杂度

跳表的主要操作时间复杂度如下:

操作平均时间复杂度最坏时间复杂度
查找O(log n)O(n)
插入O(log n)O(n)
删除O(log n)O(n)

跳表的平均时间复杂度为O(log n)的原因:

  • 在查找过程中,每一层都大约跳过一半的节点
  • 跳表的层数平均为O(log n)
  • 在每一层最多访问O(1)个节点

空间复杂度

跳表的空间复杂度为O(n),其中n是元素个数。虽然跳表有多层,但总的额外空间消耗仍然是有界的:

  • 第0层:n个节点
  • 第1层:约n/2个节点
  • 第2层:约n/4个节点
  • 总节点数约为:n + n/2 + n/4 + … ≈ 2n

因此,额外空间消耗的常数因子通常小于2。

完整代码

package lianbiao;

import java.util.*;

/**
 * @Author Stringzhua
 * @Date 2025/10/23 17:35
 * description:跳表
 */

/**
 * 跳表节点类
 */
class SkipListNode {
    Integer key;
    SkipListNode[] forward;

    public SkipListNode(Integer key, int level) {
        this.key = key;
        this.forward = new SkipListNode[level + 1];
    }

    @Override
    public String toString() {
        return "SkipListNode(key=" + key + ")";
    }
}

/**
 * 跳表实现类
 */
class SkipList implements Iterable<Integer> { // 实现Iterable接口
    private static final int MAX_LEVEL = 16;
    private static final double P = 0.5;
    private int level;
    private SkipListNode header;
    private Random random;

    public SkipList() {
        this.level = 0;
        this.header = new SkipListNode(null, MAX_LEVEL);
        this.random = new Random();
    }

    // 新增:获取当前跳表最大层数(供外部范围查询使用)
    public int getLevel() {
        return this.level;
    }

    // 新增:获取头节点(供外部范围查询使用)
    public SkipListNode getHeader() {
        return this.header;
    }

    private int randomLevel() {
        int level = 0;
        while (random.nextDouble() < P && level < MAX_LEVEL) {
            level++;
        }
        return level;
    }

    public SkipListNode search(int key) {
        SkipListNode current = this.header;
        //从最高层开始,逐层向下查找
        for (int i = this.level; i >= 0; i--) {
            //在当前层水平移动,知道找到小于或等于目标值的最大节点
            while (current.forward[i] != null && current.forward[i].key < key) {
                current = current.forward[i];
            }
        }
        //现在在底层,检查下一个节点是否是目标值
        current = current.forward[0];
        //如果下个节点存在并且key相等,则找到目标
        if (current != null && current.key == key) {
            return current;
        }
        return null;//未找到目标
    }

    public boolean insert(int key) {
        // 创建更新数组,用于存储需要更新前向指针的节点
        SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
        SkipListNode current = this.header;
        // 从最高层开始,查找适合的插入位置
        for (int i = this.level; i >= 0; i--) {
            while (current.forward[i] != null && current.forward[i].key < key) {
                current = current.forward[i];
            }
            // 记录每一层需要更新的节点
            update[i] = current;
        }
        // 移动到下一个节点
        current = current.forward[0];
        // 检查key是否已经存在
        if (current != null && current.key == key) {
            return false;// key已存在,则插入失败
        }
        // 为新节点生成随机层数
        int randomLevel = randomLevel();
        if (randomLevel > this.level) {
            // 更新跳表当前最大层数
            for (int i = this.level + 1; i <= randomLevel; i++) {
                update[i] = this.header;
            }
            this.level = randomLevel;
        }
        // 创建新的节点
        SkipListNode newNode = new SkipListNode(key, randomLevel);
        // 更新所有受影响的前向指针
        for (int i = 0; i <= randomLevel; i++) {
            newNode.forward[i] = update[i].forward[i];
            update[i].forward[i] = newNode;
        }
        return true;
    }

    public boolean delete(int key) {
        // 创建更新数组
        SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
        SkipListNode current = this.header;
        // 从最高层开始,查找目标节点
        for (int i = this.level; i >= 0; i--) {
            while (current.forward[i] != null && current.forward[i].key < key) {
                current = current.forward[i];
            }
            update[i] = current;
        }
        // 移动到下一节点
        current = current.forward[0];
        // 如果找到目标节点
        if (current == null || current.key != key) {
            return false;
        }
        //从最基层开始,更新所有收到影响层的前向指针
        for (int i = 0; i <= this.level; i++) {
            if (update[i].forward[i] != current) {
                break;
            }
            update[i].forward[i] = current.forward[i];
        }
        // 删除没有元素的层
        while (this.level > 0 && this.header.forward[this.level] == null) {
            this.level--;
        }
        return true;//没有找到目标节点
    }

    public void display() {
        for (int i = this.level; i >= 0; i--) {
            System.out.print("Level " + i + ": ");
            SkipListNode node = this.header.forward[i];
            while (node != null) {
                System.out.print(node.key + " -> ");
                node = node.forward[i];
            }
            System.out.println("null");
        }
    }

    /**
     * 实现Iterable接口的iterator()方法
     */
    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<Integer>() {
            private SkipListNode current = header.forward[0];

            @Override
            public boolean hasNext() {
                return current != null;
            }

            @Override
            public Integer next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                }
                int key = current.key;
                current = current.forward[0];
                return key;
            }
        };
    }

    public boolean contains(int key) {
        return search(key) != null;
    }

    public int size() {
        int count = 0;
        SkipListNode current = header.forward[0];
        while (current != null) {
            count++;
            current = current.forward[0];
        }
        return count;
    }
}

/**
 * 跳表功能测试类
 */
public class SkipListDemo {
    public static void basicOperationsDemo() {
        System.out.println("=== 基本操作演示 ===");
        SkipList skipList = new SkipList();

        System.out.println("\n插入元素:");
        int[] elements = {3, 6, 7, 9, 12, 19, 17, 26, 21, 25};
        for (int elem : elements) {
            skipList.insert(elem);
            System.out.println("插入 " + elem);
        }

        System.out.println("\n跳表结构:");
        skipList.display();

        System.out.println("\n搜索元素:");
        int[] searchElements = {7, 12, 19, 25, 30};
        for (int elem : searchElements) {
            boolean exists = skipList.contains(elem);
            System.out.println("搜索 " + elem + ": " + (exists ? "存在" : "不存在"));
        }

        System.out.println("\n删除元素:");
        int[] deleteElements = {7, 12, 30};
        for (int elem : deleteElements) {
            boolean success = skipList.delete(elem);
            System.out.println("删除 " + elem + ": " + (success ? "成功" : "失败"));
        }

        System.out.println("\n删除后的跳表结构:");
        skipList.display();

        // 修复2:现在支持for-each遍历(因SkipList实现了Iterable接口)
        System.out.println("\n遍历所有元素:");
        List<Integer> allElements = new ArrayList<>();
        for (int key : skipList) {
            allElements.add(key);
        }
        System.out.println(allElements);
    }

    public static void performanceTest(int n) {
        System.out.println("\n=== 性能测试 ===");
        SkipList skipList = new SkipList();

        List<Integer> data = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            data.add(i);
        }
        Collections.shuffle(data);
        List<Integer> testSample = data.subList(0, 100);

        System.out.println("\n插入 " + n + " 个元素:");
        long startTime = System.nanoTime();
        for (int key : data) {
            skipList.insert(key);
        }
        long endTime = System.nanoTime();
        double cost = (endTime - startTime) / 1e6;
        System.out.printf("耗时: %.6f 秒%n", cost / 1000);

        System.out.println("\n搜索 100 个元素:");
        startTime = System.nanoTime();
        for (int key : testSample) {
            skipList.search(key);
        }
        endTime = System.nanoTime();
        cost = (endTime - startTime) / 1e6;
        System.out.printf("耗时: %.6f 秒%n", cost / 1000);

        System.out.println("\n删除 100 个元素:");
        startTime = System.nanoTime();
        for (int key : testSample) {
            skipList.delete(key);
        }
        endTime = System.nanoTime();
        cost = (endTime - startTime) / 1e6;
        System.out.printf("耗时: %.6f 秒%n", cost / 1000);
    }

    public static void rangeQueryExample() {
        System.out.println("\n=== 范围查询示例 ===");
        SkipList skipList = new SkipList();
        for (int i = 1; i <= 100; i++) {
            skipList.insert(i);
        }

        class RangeQuery {
            List<Integer> query(SkipList skipList, int start, int end) {
                List<Integer> result = new ArrayList<>();
                // 修复3:通过getter获取private成员(header和level)
                SkipListNode header = skipList.getHeader();
                int currentLevel = skipList.getLevel();

                // 从最高层向下查找起始位置
                SkipListNode current = header;
                for (int i = currentLevel; i >= 0; i--) {
                    while (current.forward[i] != null && current.forward[i].key < start) {
                        current = current.forward[i];
                    }
                }

                // 收集范围内的元素
                current = current.forward[0];
                while (current != null && current.key <= end) {
                    result.add(current.key);
                    current = current.forward[0];
                }

                return result;
            }
        }

        RangeQuery rangeQuery = new RangeQuery();
        int[][] ranges = {{10, 20}, {50, 60}, {95, 105}};
        for (int[] range : ranges) {
            int start = range[0];
            int end = range[1];
            List<Integer> result = rangeQuery.query(skipList, start, end);
            System.out.println("范围 [" + start + ", " + end + "] 内的元素: " + result);
        }
    }

    public static void main(String[] args) {
        basicOperationsDemo();
        performanceTest(10000);
        rangeQueryExample();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值