Treap 原理详解和实战

一 点睛

Treap 指 Tree + heap,又叫作树堆,同时满足二叉搜索树和堆两种性质。二叉搜索树满足中序有序性,输入序列不同,创建的二叉搜索树也不同,在最坏的情况下(只有左子树或只有右子树)会退化为线性。例如输入1 2 3 4 5,创建的二叉搜索树如下图所示。

Treap 原理详解和实战_第1张图片

二叉搜索树的插入、查找、删除等效率与树高成正比,因此在创建二叉搜索树时要尽可能通过调平衡压缩树高。平衡树有很多种,例如 AVL 树、伸展树、SBT、红黑树等,这些调平衡的方法相对复杂。

若一个二叉搜索树插入节点的顺序是随机的,则得到的二叉搜索树在大多数情况下是平衡的,即使存在一些极端情况,这种情况发生的概率也很小,因此以随机顺序创建的二叉搜索树,其期望高度为O(logn )。可以将输入数据随机打乱,再创建二叉搜索树,但我们有时

并不能事前得知所有待插入的节点,而 Treap 可以有效解决该问题。

Treap 是一种平衡二叉搜索树,它给每个节点都附加了一个随机数,使其满足堆的性质,而节点的值又满足二叉搜索树的有序性,其基本操作的期望时间复杂度为 O(logn)。相对于其他平衡二叉搜索树,Treap 的特点是实现简单,而且可以基本实现随机平衡。

在 Treap 的构建过程中,插入节点时会给每个节点都附加一个随机数作为优先级,该优先级满足堆的性质(最大堆或最小堆均可,这里以最大堆为例,根的优先级大于左右子节点),数值满足二叉搜索树性质(中序有序性,左子树大于根,右子树小于根)。

输入 6 4 9 7 2,构建 Treap。首先给每个节点都附加一个随机数作为优先级,根据输入数据和附加随机数,构建的 Treap 如下图所示。

Treap 原理详解和实战_第2张图片

二 右旋和左旋

Treap 需要两种旋转操作:右旋和左旋。

1 右旋(zig)

节点 p 右旋时,会携带自己的右子树,向右旋转到 q 的右子树位置,q 的右子树被抛弃,此时 p 右旋后左子树正好空闲,将 q 的右子树放在 p 的左子树位置,旋转后的树根为 q 。

Treap 原理详解和实战_第3张图片

2 左旋(zag)

节点 p 左旋时,携带自己的左子树,向左旋转到 q 的左子树位置,q 的左子树被抛弃,此时 p 左旋后右子树正好空闲,将 q 的左子树放在 p 的右子树位置,旋转后的树根为 q 。

Treap 原理详解和实战_第4张图片

总结:无论是右旋还是左旋,旋转后总有一棵子树被抛弃,一个指针空闲,正好配对。

三 插入

Treap 的插入操作和二叉搜索树一样,首先根据有序性找到插入的位置,然后创建新节点插入该位置。创建新节点时,会给该节点附加一个随机数作为优先级,自底向上检查该优先级是否满足堆性质,若不满足,则需要右旋或左旋,使其满足堆性质。

算法步骤如下。

(1)从根节点 p 开始,若 p 为空,则创建新节点,将待插入元素 val 存入新节点,并给新节点附加一个随机数作为优先级。

(2)若 val 等于 p 的值,则什么都不做,返回。

(3)若 val 小于 p 的值,则在 p 的左子树中递归插入。回溯时做旋转调整,若 p 的优先级小于其左子节点的优先级,则 p 右旋。

(4)若 val 大于 p 的值,则在 p 的右子树中递归插入。回溯时做旋转调整,若 p 的优先级小于其右子节点的优先级,则 p 左旋。

一个树堆如下图所示,在该树堆中插入元素 8,插入过程如下。

Treap 原理详解和实战_第5张图片

(1)根据二叉搜索树的插入操作,将 8 插入 9 的左子节点位置,假设 8 的随机数优先级为 25016。

Treap 原理详解和实战_第6张图片

(2)回溯时,判断是否需要旋转,9 的优先级比其左子节点小,因此 9 节点右旋。

Treap 原理详解和实战_第7张图片

(3)继续向上判断,7 的优先级比 7 的右子节点小,因此7节点左旋。

Treap 原理详解和实战_第8张图片

(4)继续向上判断,6 的优先级不比 6 的左右子节点小,满足最大堆性质,无须调整,已向上判断到树根,算法停止。

四 删除

Treap 的删除操作非常简单:找到待删除的节点,将该节点向优先级大的子节点旋转,一直旋转到叶子,直接删除叶子即可。

算法步骤如下。

(1)从根节点 p 开始,若待删除元素 val 等于p 的值,则:若 p 只有左子树或只有右子树,则令其子树子承父业代替 p ,返回;若 p 的左子节点优先级大于右子节点的优先级,则 p 右旋,继续在 p 的右子树中递归删除;若 p 的左子节点的优先级小于右子节点的优先级,则 p 左旋,继续在 p 的左子树中递归删除。

(2)若 val 小于 p 的值,则在 p 的左子树中递归删除。

(3)若 val 大于 p 的值,则在 p 的右子树中递归删除。

在上面的树堆中删除元素 8,删除过程如下。

(1)根据二叉搜索树的删除操作,首先找到 8 的位置,8 的右子节点优先级大,8 左旋。

Treap 原理详解和实战_第9张图片

(2)接着判断,8 的左子节点优先级大,8 右旋。

Treap 原理详解和实战_第10张图片

(3)此时 8 只有一个左子树,左子树子承父业代替它。

Treap 原理详解和实战_第11张图片

五 前驱

在 Treap 中求一个节点 val 的前驱时,首先从树根开始,若当前节点的值小于 val,则用 res 暂存该节点的值,在当前节点的右子树中寻找,否则在当前节点的左子树中寻找,直到当前节点为空,返回 res,即为 val 的前驱。

六 后继

在 Treap 中求一个节点 val 的后继时,首先从树根开始,若当前节点的值大于 val,则用 res 暂存该节点的值,在当前节点的左子树中寻找,否则在当前节点的右子树中寻找,直到当前节点为空,返回 res,即为 val 的后继。

七 代码

package com.platform.modules.alg.alglib.p366;

import java.util.Random;

public class P366 {
    public String output = "";

    private int maxn = 100005;
    int n; // 结点数
    int cnt; // 结点存储下标累计
    int root; // 树根

    private node tr[] = new node[maxn];

    // 生成新结点
    int New(int val) {
        tr[++cnt].val = val;
        tr[cnt].pri = Math.abs(new Random().nextInt()) % 100;
        tr[cnt].num = tr[cnt].size = 1;
        tr[cnt].rc = tr[cnt].lc = 0;
        return cnt;
    }

    // 更新子树大小
    void Update(int p) {
        tr[p].size = tr[tr[p].lc].size + tr[tr[p].rc].size + tr[p].num;
    }

    // 右旋
    int zig(int p) {
        int q = tr[p].lc;
        tr[p].lc = tr[q].rc;
        tr[q].rc = p;
        tr[q].size = tr[p].size;
        Update(p);
        // 现在 q 为根
        p = q;
        return p;
    }

    // 左旋
    int zag(int p) {
        int q = tr[p].rc;
        tr[p].rc = tr[q].lc;
        tr[q].lc = p;
        tr[q].size = tr[p].size;
        Update(p);
        // 现在 q 为根
        p = q;
        return p;
    }

    int Insert(int p, int val) // 在 p 的子树插入值val
    {
        if (p == 0) {
            p = New(val);
            return p;
        }
        tr[p].size++;
        if (val == tr[p].val) {
            tr[p].num++;
            return p;
        }
        if (val < tr[p].val) {
            tr[p].lc = Insert(tr[p].lc, val);
            if (tr[p].pri < tr[tr[p].lc].pri)
                p = zig(p);
        } else {
            tr[p].rc = Insert(tr[p].rc, val);
            if (tr[p].pri < tr[tr[p].rc].pri)
                p = zag(p);
        }
        return p;
    }

    int Delete(int p, int val) // 在p的子树删除值val
    {
        if (p == 0)
            return p;
        tr[p].size--;
        if (val == tr[p].val) {
            if (tr[p].num > 1) {
                tr[p].num--;
                return p;
            }
            if (tr[p].lc == 0 || tr[p].rc == 0)
                p = tr[p].lc + tr[p].rc; // 有一个儿子为空,直接用儿子代替
            else if (tr[tr[p].lc].pri > tr[tr[p].rc].pri) {
                p = zig(p);
                tr[p].rc = Delete(tr[p].rc, val);
            } else {
                p = zag(p);
                tr[p].lc = Delete(tr[p].lc, val);
            }
            return p;
        }
        if (val < tr[p].val)
            tr[p].lc = Delete(tr[p].lc, val);
        else
            tr[p].rc = Delete(tr[p].rc, val);
        return p;
    }

    // 找前驱
    int GetPre(int val) {
        int p = root;
        int res = -1;
        while (p > 0) {
            if (tr[p].val < val) {
                res = tr[p].val;
                p = tr[p].rc;
            } else
                p = tr[p].lc;
        }
        return res;
    }

    // 找后继
    int GetNext(int val) {
        int p = root;
        int res = -1;
        while (p > 0) {
            if (tr[p].val > val) {
                res = tr[p].val;
                p = tr[p].lc;
            } else
                p = tr[p].rc;
        }
        return res;
    }

    public P366() {
        for (int i = 0; i < tr.length; i++) {
            tr[i] = new node();
        }
    }

    void print(int p) {
        if (p > 0) {
            print(tr[p].lc);
            output += tr[p].val + " " + tr[p].pri + " " + tr[p].num + " " + tr[p].size + " " + tr[tr[p].lc].val + " " + tr[tr[p].rc].val + " " + "\n";
            print(tr[p].rc);
        }
    }

    public String cal(String input) {
        String[] line = input.split("\n");
        // 初始化节点
        n = Integer.parseInt(line[0]);
        String[] nums = line[1].split(" ");

        for (int i = 1; i <= n; i++) {
            root = Insert(root, Integer.parseInt(nums[i - 1]));
        }
        print(root);
        int opt, x;

        int count = 2;
        while (true) {
            String[] command = line[count++].split(" ");
            opt = Integer.parseInt(command[0]);
            switch (opt) {
                case 0:
                    return output;
                case 1:
                    x = Integer.parseInt(command[1]);
                    root = Insert(root, x);
                    print(root);
                    break;
                case 2:
                    x = Integer.parseInt(command[1]);
                    root = Delete(root, x);
                    print(root);
                    break;
                case 3:
                    x = Integer.parseInt(command[1]);
                    output += GetPre(x) + "\n";
                    break;
                case 4:
                    x = Integer.parseInt(command[1]);
                    output += GetNext(x) + "\n";
                    break;
            }
        }
    }
}

class node {
    int lc, rc; // 左右孩子
    int val, pri; // 值,优先级
    int num, size; // 重复个数,根的子树的大小
}

八 测试

1 输入

5

6 2 7 4 9

1 8

2 8

3 7

4 2

0

2 输出

2 77 1 3 0 4

4 18 1 2 0 6

6 1 1 1 0 0

7 88 1 5 2 9

9 61 1 1 0 0

2 77 1 3 0 4

4 18 1 2 0 6

6 1 1 1 0 0

7 88 1 6 2 8

8 77 1 2 0 9

9 61 1 1 0 0

2 77 1 3 0 4

4 18 1 2 0 6

6 1 1 1 0 0

7 88 1 5 2 9

9 61 1 1 0 0

6

4

你可能感兴趣的:(数据结构与算法,数据结构,算法)