每日算法 — 使用java实现Treap:随机优先级与Split-Merge平衡二叉搜索树

引言

平衡二叉搜索树是算法世界中的基石结构。从AVL树到红黑树,各种平衡策略层出不穷,但它们的实现往往伴随着复杂的旋转逻辑和大量的边界条件处理。今天我们要介绍一种优雅得多的方案——Treap(树堆),它通过为每个节点赋予一个随机优先级,将二叉搜索树(BST)与堆(Heap)两种结构巧妙地融合在一起,以极高的概率维持树的平衡。

本文采用非旋转式Treap(Split-Merge Treap)的实现方式。与传统依靠左旋、右旋来维持平衡的树不同,非旋转式Treap仅通过两个核心操作 splitmerge 就能完成插入、删除和区间操作,代码简洁而强大。

Treap的核心思想

Treap这个名字本身即是 Tree + Heap 的组合。它要求每个节点同时满足两个性质:

  1. 二叉搜索树性质:节点的左子树所有值小于该节点,右子树所有值大于该节点。
  2. 堆性质:每个节点的优先级小于(或大于)其子节点的优先级。

关键洞察在于:只要优先级是随机分配的,Treap的期望高度就是 O(log n)。这就好比随机快速排序的期望复杂度是 O(n log n) 一样——随机性为我们省却了繁琐的平衡维护。

非旋转式Treap的两大核心操作

Split 操作

split(root, key) 将一棵树拆分为两棵子树 LR,使得 L 中所有节点的值小于等于 keyR 中所有节点的值大于 key

拆分逻辑十分直观:
– 若 root.val <= key,则根节点及其左子树属于 L,继续对右子树递归拆分。
– 若 root.val > key,则根节点及其右子树属于 R,继续对左子树递归拆分。

Merge 操作

merge(L, R) 将两棵子树合并为一棵,前提是 L 中所有值小于 R 中所有值。

合并逻辑同样简洁:
– 比较两棵树的根节点优先级,优先级更高的作为新根。
– 若 L 的根优先级更高,L 的右子树与 R 递归合并。
– 若 R 的根优先级更高,R 的左子树与 L 递归合并。

完整Java实现

下面给出非旋转式Treap的完整Java实现,包含插入、删除、查询、前驱、后继以及中序遍历。代码经过精心设计,可以直接运行。

import java.util.Random;

/**
 * 非旋转式Treap(Split-Merge Treap)实现
 * 通过随机优先级维持期望 O(log n) 的高度
 */
public class Treap {

    private static final Random RAND = new Random();
    private Node root;

    /**
     * Treap节点定义
     * val: 节点值(BST比较依据)
     * priority: 随机优先级(Heap比较依据)
     * size: 子树节点数(用于隐式Treap的索引查询)
     */
    private static class Node {
        int val;
        int priority;
        Node left, right;
        int size;

        Node(int val) {
            this.val = val;
            this.priority = RAND.nextInt();
            this.size = 1;
        }
    }

    /** 获取子树大小,空节点为0 */
    private int getSize(Node node) {
        return node == null ? 0 : node.size;
    }

    /** 更新节点size字段 */
    private void update(Node node) {
        if (node != null) {
            node.size = 1 + getSize(node.left) + getSize(node.right);
        }
    }

    /**
     * 按值拆分:将树拆分为 <= key 和 > key 两部分
     * @return 包含左右子树根节点的数组
     */
    private Node[] split(Node node, int key) {
        if (node == null) {
            return new Node[]{null, null};
        }
        if (node.val <= key) {
            // 当前节点属于左半部分,递归拆分右子树
            Node[] splitRight = split(node.right, key);
            node.right = splitRight[0];
            update(node);
            return new Node[]{node, splitRight[1]};
        } else {
            // 当前节点属于右半部分,递归拆分左子树
            Node[] splitLeft = split(node.left, key);
            node.left = splitLeft[1];
            update(node);
            return new Node[]{splitLeft[0], node};
        }
    }

    /**
     * 合并两棵子树,要求 leftTree 中所有值 < rightTree 中所有值
     */
    private Node merge(Node leftTree, Node rightTree) {
        if (leftTree == null) return rightTree;
        if (rightTree == null) return leftTree;

        // 优先级高的作为根,保持堆性质
        if (leftTree.priority < rightTree.priority) {
            leftTree.right = merge(leftTree.right, rightTree);
            update(leftTree);
            return leftTree;
        } else {
            rightTree.left = merge(leftTree, rightTree.left);
            update(rightTree);
            return rightTree;
        }
    }

    /** 插入一个值 */
    public void insert(int val) {
        // 先拆分为 < val 和 >= val 两部分
        Node[] split = split(root, val - 1);
        Node newNode = new Node(val);
        // 顺序合并:左部 + 新节点 + 右部
        root = merge(merge(split[0], newNode), split[1]);
    }

    /** 删除一个值(删除一个出现即可) */
    public void delete(int val) {
        // 拆分为 < val, = val, > val 三部分
        Node[] split1 = split(root, val - 1);
        Node[] split2 = split(split1[1], val);
        // 丢弃中间部分(等于val的子树),将其左右子树合并后接回
        Node mid = split2[0];
        Node mergedMid = merge(mid == null ? null : mid.left, mid == null ? null : mid.right);
        root = merge(split1[0], merge(mergedMid, split2[1]));
    }

    /** 查询值是否存在 */
    public boolean contains(int val) {
        Node cur = root;
        while (cur != null) {
            if (cur.val == val) return true;
            cur = val < cur.val ? cur.left : cur.right;
        }
        return false;
    }

    /** 查找前驱(小于val的最大值) */
    public Integer predecessor(int val) {
        Node cur = root;
        Integer res = null;
        while (cur != null) {
            if (cur.val < val) {
                res = cur.val;
                cur = cur.right;
            } else {
                cur = cur.left;
            }
        }
        return res;
    }

    /** 查找后继(大于val的最小值) */
    public Integer successor(int val) {
        Node cur = root;
        Integer res = null;
        while (cur != null) {
            if (cur.val > val) {
                res = cur.val;
                cur = cur.left;
            } else {
                cur = cur.right;
            }
        }
        return res;
    }

    /** 中序遍历输出(BST性质保证有序) */
    public void inorder() {
        inorder(root);
        System.out.println();
    }

    private void inorder(Node node) {
        if (node == null) return;
        inorder(node.left);
        System.out.print(node.val + " ");
        inorder(node.right);
    }

    /** 获取树中第 k 小的元素(1-based),利用size字段 */
    public Integer kth(int k) {
        if (k < 1 || k > getSize(root)) return null;
        Node cur = root;
        while (cur != null) {
            int leftSize = getSize(cur.left);
            if (k <= leftSize) {
                cur = cur.left;
            } else if (k == leftSize + 1) {
                return cur.val;
            } else {
                k -= leftSize + 1;
                cur = cur.right;
            }
        }
        return null;
    }

    // ========== 测试入口 ==========
    public static void main(String[] args) {
        Treap treap = new Treap();

        int[] values = {50, 30, 70, 20, 40, 60, 80, 25, 35, 75};
        for (int v : values) {
            treap.insert(v);
        }

        System.out.print("中序遍历: ");
        treap.inorder(); // 输出: 20 25 30 35 40 50 60 70 75 80

        System.out.println("包含 40? " + treap.contains(40)); // true
        System.out.println("包含 100? " + treap.contains(100)); // false

        System.out.println("40 的前驱: " + treap.predecessor(40)); // 35
        System.out.println("40 的后继: " + treap.successor(40)); // 50

        System.out.println("第 5 小: " + treap.kth(5)); // 40

        treap.delete(40);
        System.out.print("删除 40 后: ");
        treap.inorder(); // 输出不含 40
        System.out.println("包含 40? " + treap.contains(40)); // false
    }
}

隐式Treap:超越键值的区间操作

非旋转式Treap有一个极其强大的扩展——隐式Treap(Implicit Treap)。在隐式Treap中,节点不再按键值排序,而是按位置(索引)排序。每个节点的排名由左子树的大小决定。

通过 splitBySize(root, k) 将树拆分为前 k 个元素和剩余元素,我们可以轻松实现以下操作:

  • 区间翻转:将 [l, r] 区间拆分出来,打上懒标记交换左右子树。
  • 区间插入:将一段新序列整体插入到指定位置。
  • 区间删除:将指定区间拆分出来并丢弃。
  • 区间查询:查询子树和、最大值等聚合信息。

隐式Treap的代码与显式Treap几乎一致,仅需将 split 的比较条件从 val <= key 改为 leftSize <= k,是数据结构领域中代码复用率的典范。

复杂度分析

操作 期望时间复杂度 最坏时间复杂度
插入 O(log n) O(n)
删除 O(log n) O(n)
查询 O(log n) O(n)
前驱/后继 O(log n) O(n)
第 k 小 O(log n) O(n)
Split / Merge O(log n) O(n)

期望复杂度为 O(log n) 的数学依据是:随机优先级等价于随机插入顺序,而随机BST的期望高度为 O(log n)。最坏情况仅在极小的概率下发生(优先级全序排列的概率为 1/n!)。

空间复杂度为 O(n),每个节点仅需存储值、优先级、左右指针和子树大小。

应用场景

Treap 及隐式Treap 在竞赛编程和实际工程中都有广泛应用:

  • 动态集合维护:需要频繁插入、删除且要求有序遍历的场景。
  • 区间操作问题:如序列翻转、区间最值查询、区间和更新等,隐式Treap是经典解法。
  • 笛卡尔树构建:将数组值作为BST键、下标作为堆优先级,可在线性时间内构建笛卡尔树。
  • 可持久化数据结构:非旋转式Treap的Split-Merge语义天然适合函数式编程风格,便于实现可持久化版本。

总结

Treap 用一种近乎作弊的方式解决了平衡树的难题——不依赖复杂的旋转规则,而是将问题交给随机性。非旋转式实现更是将代码优雅度提升到了新的高度:仅需 splitmerge 两个递归函数,就能支撑起整套平衡树操作。掌握 Treap,不仅能让你在面对平衡树问题时多一把利器,更能加深对随机化算法本质的理解:有时,让概率为我们工作,比让确定性规则束缚我们更加高效。