每日算法 — 使用java实现跳表:概率性分层索引与O(log n)高效查找

跳表(Skip List)是一种基于概率的数据结构,由William Pugh于1990年提出。它通过在有序链表上构建多层索引,实现了与平衡树相当的O(log n)平均时间复杂度,同时代码实现远比红黑树、AVL树简单直观。跳表被广泛应用于Redis的有序集合(ZSet)、LevelDB等高性能系统中。

核心思想

跳表的核心思想是在有序链表上随机建立”快速通道”。底层是一个完整的有序链表,向上每一层都是下一层的子集,节点以一定概率(通常为1/2或1/4)晋升到更高层。查询时从顶层开始,利用索引快速跳过大量节点,逐层下沉至底层定位目标。

算法设计

节点结构

每个节点包含值、多层前向指针数组。层数通过随机函数决定,期望层数为O(log n)。

class SkipListNode {
    int value;
    SkipListNode[] forward; // 前向指针数组,forward[i]表示第i层的下一个节点

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

随机层数生成

使用几何分布决定节点层数,确保高层节点稀疏、低层节点密集。

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

查找算法

从最高层开始,比较当前节点与目标值,若小于目标则向右移动,否则下沉一层。

public boolean search(int target) {
    SkipListNode current = head;
    for (int i = level; i >= 0; i--) {
        while (current.forward[i] != null && current.forward[i].value < target) {
            current = current.forward[i];
        }
    }
    current = current.forward[0];
    return current != null && current.value == target;
}

插入算法

先找到每一层的前驱节点,再生成随机层数,最后逐层插入并更新前驱指针。

public void insert(int value) {
    SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
    SkipListNode current = head;

    // 从顶层向下查找每一层的前驱节点
    for (int i = level; i >= 0; i--) {
        while (current.forward[i] != null && current.forward[i].value < value) {
            current = current.forward[i];
        }
        update[i] = current;
    }

    current = current.forward[0];

    // 若值已存在,可选择更新或忽略
    if (current == null || current.value != value) {
        int newLevel = randomLevel();

        // 若新节点层数超过当前最大层数,更新头节点指针
        if (newLevel > level) {
            for (int i = level + 1; i <= newLevel; i++) {
                update[i] = head;
            }
            level = newLevel;
        }

        SkipListNode newNode = new SkipListNode(value, newLevel);

        // 逐层插入新节点
        for (int i = 0; i <= newLevel; i++) {
            newNode.forward[i] = update[i].forward[i];
            update[i].forward[i] = newNode;
        }
    }
}

删除算法

类似插入,先找到各层前驱,再逐层断开指针连接。

public boolean delete(int value) {
    SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
    SkipListNode current = head;

    for (int i = level; i >= 0; i--) {
        while (current.forward[i] != null && current.forward[i].value < value) {
            current = current.forward[i];
        }
        update[i] = current;
    }

    current = current.forward[0];

    if (current != null && current.value == value) {
        for (int i = 0; i <= level; i++) {
            if (update[i].forward[i] != current) break;
            update[i].forward[i] = current.forward[i];
        }

        // 清理空层
        while (level > 0 && head.forward[level] == null) {
            level--;
        }
        return true;
    }
    return false;
}

完整可运行代码

import java.util.Random;

/**
 * 跳表实现 - 支持查找、插入、删除操作
 * 平均时间复杂度: O(log n)
 * 空间复杂度: O(n)
 */
public class SkipList {
    private static final double P = 0.5; // 晋升概率
    private static final int MAX_LEVEL = 16; // 最大层数

    private final SkipListNode head;
    private int level;
    private final Random random;

    private static class SkipListNode {
        int value;
        SkipListNode[] forward;

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

    public SkipList() {
        this.head = new SkipListNode(-1, MAX_LEVEL);
        this.level = 0;
        this.random = new Random();
    }

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

    public boolean search(int target) {
        SkipListNode current = head;
        for (int i = level; i >= 0; i--) {
            while (current.forward[i] != null && current.forward[i].value < target) {
                current = current.forward[i];
            }
        }
        current = current.forward[0];
        return current != null && current.value == target;
    }

    public void insert(int value) {
        SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
        SkipListNode current = head;

        for (int i = level; i >= 0; i--) {
            while (current.forward[i] != null && current.forward[i].value < value) {
                current = current.forward[i];
            }
            update[i] = current;
        }

        current = current.forward[0];

        if (current == null || current.value != value) {
            int newLevel = randomLevel();
            if (newLevel > level) {
                for (int i = level + 1; i <= newLevel; i++) {
                    update[i] = head;
                }
                level = newLevel;
            }

            SkipListNode newNode = new SkipListNode(value, newLevel);
            for (int i = 0; i <= newLevel; i++) {
                newNode.forward[i] = update[i].forward[i];
                update[i].forward[i] = newNode;
            }
        }
    }

    public boolean delete(int value) {
        SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
        SkipListNode current = head;

        for (int i = level; i >= 0; i--) {
            while (current.forward[i] != null && current.forward[i].value < value) {
                current = current.forward[i];
            }
            update[i] = current;
        }

        current = current.forward[0];

        if (current != null && current.value == value) {
            for (int i = 0; i <= level; i++) {
                if (update[i].forward[i] != current) break;
                update[i].forward[i] = current.forward[i];
            }
            while (level > 0 && head.forward[level] == null) {
                level--;
            }
            return true;
        }
        return false;
    }

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

    public static void main(String[] args) {
        SkipList skipList = new SkipList();
        int[] values = {3, 6, 7, 9, 12, 17, 19, 21, 25, 26};

        for (int v : values) {
            skipList.insert(v);
        }

        System.out.println("跳表结构:");
        skipList.printList();

        System.out.println("\n查找 19: " + skipList.search(19));
        System.out.println("查找 100: " + skipList.search(100));

        System.out.println("\n删除 19");
        skipList.delete(19);
        System.out.println("查找 19: " + skipList.search(19));
    }
}

复杂度分析

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

跳表通过概率保证平均性能,实际工程中可通过调整晋升概率P来平衡时间与空间:P越小,层数越少,空间越省但查询稍慢;P越大,索引越密集,查询越快但空间开销增加。

应用场景

  • Redis ZSet有序集合的底层实现
  • LevelDB、RocksDB等LSM-Tree存储引擎的MemTable
  • 需要高效范围查询的有序数据场景
  • 作为平衡树的简化替代方案