SkipList跳表:高效查找的利器

引言

在计算机科学中,数据结构的选择对于程序的性能至关重要。对于需要高效查找、插入和删除操作的场景,跳跃表(SkipList)是一种非常实用的数据结构。本文将详细介绍跳跃表的原理、实现和应用场景。

什么是跳表

下图是一个简单的有序单链表,假设我们需要查找10这个元素,只能从头部开始逐个遍历,无法随机访问,查找路径:1、2、3、4、5、6、7、8、9、10。这样的查找效率很低,平均时间复杂度很高O(n)。那有没有办法提高链表的查找速度呢?

如下图,我们从每5个元素抽出一个元素构建一个新的链表,新链表指向了原链表。这样我们查找数据的时候先从上层链表开始查找,那么查找10的路径为1、6、7、8、9、10。查询范围一下子缩小了一半

两层链表就把查询范围减少了一半,如果加多几层,岂不更快 ?

增加多一层链表后,查询路径为1、6、9、10,比两层更快。可以看出跳表是通过空间换时间的方式加快查询性能

跳表是一种基于有序链表 的随机化数据结构,通过多级索引 实现类似二分查找的效率。其核心思想是:​用空间换时间,允许快速查询、插入和删除操作。跳表的平均时间复杂度为 O(log n),与平衡二叉树相当,但实现更简单

跳表的实现

插入操作

如下图,如果我们想把4这个元素插入到跳表中,那应该怎么操作呢?

插入元素和查找元素一样,先从最上层开始查找,找到底层原始链表应该插入的位置,跳表的原始链表需要保持有序。整个过程类似查找4原始一样,查找路径1、1、3、3。因为4大于3,小于5,4应该插入再3和5之间。

如果一直往原始链表插入数据,不更新索引(往上抽多层链表),就可能出现两个索引节点之间数据非常多,极端情况下,跳表就退化为单链表了。这种情况怎么解 ?那就要我们插入数据的时候,索引节点也要相对应的增加或者重建。

那怎么去维护这个索引呢 ?相对容易理解的方法就是每次新增节点把之前的索引删掉,再全部重新构建。这种效率是极低的,有没有更加高效的方法 ?

假设我们希望跳表每一层抽取50%元素作为上一层索引,理想的情况下第二层索引就是每隔一个元素抽取一个,第三层在第二层的基础上每隔一个元素抽取一个,以此类推。如下图所示

为了提高索引的维护效率,如果我们在原始链表中随机选择n/2 个元素作为一级索引,以此类推是否也可以呢? 虽然随机选择的元素作为索引可能是不均匀的,但是如果元素基数足够大,且抽取是随机的,那么索引整体就是均匀的,对于查询效率影响不大。

所以我们可以维护这么一个索引:随机选择n/2元素作为一级索引、n/4作为二级索引、n/8作为三级索引,以此类推,直到最顶层索引。下面就用代码来展示怎么实现

在每次插入新元素的时候,随机让元素有1/2概率在一级索引、1/4概率在二级索引、1/8概率在三级索引... 这样就能满足以上的条件。我们先实现一个随机选择索引层的方法

Java 复制代码
//最大层数
private static final int MAX_LEVEL = 16;
//随机概率
private static final double P = 0.5;

private Random random = new Random();

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

假设我们规定最大索引层为16,有1/2的概率作为一层索引。因为 random.nextDouble() < P 每次有1/2的概率,所以level=1就是1/2, level=2 就是 1/2*1/2=1/4,以此类推。这方法符合我们的要求

Java 复制代码
private static class Node<K, V> {
  K key;
  V value;
  Node<K, V>[] forward; //指向不同层级的下一个节点

  public Node(K key, V value, int level) {
    this.key = key;
    this.value = value;
    this.forward = new Node[level];
  }
}
Java 复制代码
//插入

public void add(K key, V value) {
    // 创建一个数组来记录每一层插入节点的前驱节点
    Node<K, V>[] update = new Node[MAX_LEVEL];
    // 从头节点开始
    Node<K, V> current = head;
    // 从最高层开始向下逐层查找插入位置
    for (int i = level - 1; i >= 0; i--) {
        // 沿着当前层的链表移动,直到找到最后一个小于目标键的节点
        while (current.forward[i] != null && current.forward[i].key.compareTo(key) < 0) {
            current = current.forward[i];
        }
        // 记录当前层的前驱节点
        update[i] = current;
    }
    
    // 检查目标键是否已存在
    if (current.forward[0] != null && current.forward[0].key.compareTo(key) == 0) {
        // 如果存在,更新其值
        current.forward[0].value = value;
        return;
    }
    
    // 随机确定新节点的层级
    int randomLevel = randomLevel();
    
    // 如果新节点的层级超过了当前最高层级,则更新前驱节点数组
    if (randomLevel > level) {
        for (int i = level; i < randomLevel; i++) {
            update[i] = head;
        }
        // 更新当前最高层级
        level = randomLevel;
    }
    
    // 创建新节点
    Node<K, V> newNode = new Node<>(key, value, randomLevel);
    
    // 更新每一层的指针,将新节点插入到跳跃表中
    for (int i = 0; i < randomLevel; i++) {
        // 新节点的指针指向下一个节点
        newNode.forward[i] = update[i].forward[i];
        // 前驱节点的指针指向新节点
        update[i].forward[i] = newNode;
    }
}

查找

Java 复制代码
public V get(K key) {
    // 从头节点开始
    Node<K, V> current = head;
    
    // 从最高层开始向下逐层查找
    for (int i = level - 1; i >= 0; i--) {
        // 沿着当前层的链表移动,直到找到最后一个小于目标键的节点
        while (current.forward[i] != null && current.forward[i].key.compareTo(key) < 0) {
            current = current.forward[i];
        }
    }
    
    // 这时current已经在最底层原始链表,检查目标键是否存在于最底层
    if (current.forward[0] != null && current.forward[0].key.compareTo(key) == 0) {
        // 如果存在,返回对应的值
        return current.forward[0].value;
    }
    
    // 如果不存在,返回 null
    return null;
}

删除

Java 复制代码
public void remove(K key) {
    // 创建一个数组来记录每一层的前驱节点
    Node<K, V>[] update = new Node[MAX_LEVEL];
    // 从头节点开始
    Node<K, V> current = head;
    
    // 从最高层开始向下逐层查找目标键的前驱节点
    for (int i = level; i >= 0; i--) {
        // 沿着当前层的链表移动,直到找到最后一个小于目标键的节点
        while (current.forward[i] != null && current.forward[i].key.compareTo(key) < 0) {
            current = current.forward[i];
        }
        // 记录当前层的前驱节点
        update[i] = current;
    }
    
    // 遍历每一层,检查目标键是否存在并更新指针
    for (int i = 0; i < level; i++) {
        // 检查当前层的前驱节点的下一个节点是否为目标键
        if (update[i].forward[i] != null && update[i].forward[i].key.compareTo(key) == 0) {
            // 获取要删除的节点
            Node<K, V> deleteNode = update[i].forward[i];
            // 更新前驱节点的指针,跳过删除的节点
            update[i].forward[i] = deleteNode.forward[i];
            // 断开删除节点的指针
            deleteNode.forward[i] = null;
        }
    }
    
    // 更新当前最高层级
    // 如果最高层的指针为空,则减少层级
    while (level > 0 && head.forward[level] == null) {
        level--;
    }
}

完整代码

Java 复制代码
package com.test.lsmtree;

import java.util.Random;

/**
 * @author Yang WenJie
 * @date 2025/4/7 下午5:23
 */
public class SkipList<K extends Comparable,V> {

    //最大层数
    private static final int MAX_LEVEL = 16;

    //随机概率
    private static final double P = 0.5;

    private Random random;

    private Node head;

    private int level;

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

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

    public void add(K key, V value) {
        Node<K,V>[] update = new Node[MAX_LEVEL];
        Node<K,V> current = head;
        for (int i = level - 1; i >= 0; i--) {
            while (current.forward[i] != null && current.forward[i].key.compareTo(key) < 0) {
                current = current.forward[i];
            }
            update[i] = current;
        }
        if (current.forward[0] != null && current.forward[0].key.compareTo(key) == 0) {
            current.forward[0].value = value;
            return;
        }

        int randomLevel = randomLevel();

        if (randomLevel > level) {
            for (int i = level; i < randomLevel; i++) {
                update[i] = head;
            }
            level = randomLevel;
        }

        Node<K,V> newNode = new Node<>(key, value, randomLevel);

        for (int i = 0; i < randomLevel; i++) {
            newNode.forward[i] = update[i].forward[i];
            update[i].forward[i] = newNode;
        }

    }

    public V get(K key) {

        Node<K,V> current = head;
        for (int i = level - 1; i >= 0; i--) {
            while (current.forward[i] != null && current.forward[i].key.compareTo(key) < 0) {
                current = current.forward[i];
            }
        }
        if (current.forward[0] != null && current.forward[0].key.compareTo(key) == 0) {
            return current.forward[0].value;
        }
        return null;
    }

    public void remove(K key) {
        Node<K,V>[] update = new Node[MAX_LEVEL];
        Node<K,V> current = head;
        for (int i = level; i >=0; i--) {
            while (current.forward[i] != null && current.forward[i].key.compareTo(key) < 0) {
                current = current.forward[i];
            }
            update[i] = current;
        }

        for (int i = 0; i < level; i++) {
            if (update[i].forward[i] != null && update[i].forward[i].key.compareTo(key) == 0) {
                Node<K, V> deleteNode = update[i].forward[i];
                update[i].forward[i] = deleteNode.forward[i];
                deleteNode.forward[i] = null; // 断开引用
            }
        }

        while (level > 0 && head.forward[level] == null) {
            level--;
        }

    }


    public void printAllLevels() {
        System.out.println("Skip List Levels:"+ level);
        for (int i = level - 1; i >=0 ; i--) {
            Node<K,V> current = head.forward[i];
            System.out.print("Level " + i + ": ");
            while (current != null) {
                System.out.print(current.key + " ");
                current = current.forward[i];
            }
            System.out.println();

        }
    }


    private static class Node<K,V> {
        K key;
        V value;
        Node<K,V>[] forward;

        public Node(K key, V value, int level) {
            this.key = key;
            this.value = value;
            this.forward = new Node[level];
        }
    }

    public static void main(String[] args) {
        SkipList<Integer, String> skipList = new SkipList<>();
        skipList.add(1, "one");
        skipList.add(2, "two");
        skipList.add(3, "three");
        skipList.add(4, "four");
        skipList.add(5, "five");
        skipList.add(6, "six");
        skipList.add(7, "seven");
        skipList.add(8, "eight");
        skipList.add(9, "nine");

        skipList.add(2, "two updated"); // 更新键为2的值

        skipList.printAllLevels(); // 打印每一层的内容
        System.out.println(skipList.get(1)); // 输出: one
        System.out.println(skipList.get(2)); // 输出: two
        System.out.println(skipList.get(3)); // 输出: three
        skipList.remove(4);
        System.out.println(skipList.get(4)); // 输出: null
    }
}

跳表的应用

跳跃表在数据库和缓存系统中被广泛使用,特别是在需要高效查找和动态更新的场景中。例如:

  • LevelDB:Google 的 LevelDB 使用跳跃表作为内存表(MemTable)的实现,用于存储最近插入的数据。跳跃表的高效插入和查找能力使其成为理想的内存存储结构。
  • Redis:Redis 的有序集合(Sorted Set)使用跳跃表作为底层数据结构,支持高效的范围查询和动态更新。

本文由博客一文多发平台 OpenWrite 发布!

相关推荐
杰克逊的日记40 分钟前
es的告警信息
大数据·elasticsearch·搜索引擎
Alpha汇股志3 小时前
英国股票实时API 对比:iTick的差异化优势解析
大数据·人工智能·开源·业界资讯
IT成长日记3 小时前
【Hadoop入门】Hadoop生态之Yarn简介
大数据·hadoop·分布式
煤烦恼4 小时前
spark core编程之行动算子、累加器、广播变量
大数据·分布式·spark
爱编程的王小美4 小时前
大数据专业学习路线
大数据·学习
对许5 小时前
org.apache.spark.SparkException: Kryo serialization failed: Buffer overflow...
大数据·spark
苏小夕夕5 小时前
spark(三)
大数据·windows·spark
高冷小伙6 小时前
大数据开发之数据仓库
大数据·数据仓库
SOFAStack7 小时前
蚂蚁 Flink 实时计算编译任务 Koupleless 架构改造
大数据·架构·flink