Treap 原理详解和实战

news2024/10/6 16:27:50

一 点睛

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

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

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

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

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

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

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

二 右旋和左旋

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

1 右旋(zig)

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

2 左旋(zag)

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

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

三 插入

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

算法步骤如下。

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

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

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

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

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

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

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

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

(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 左旋。

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

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

五 前驱

在 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/23205.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CMCT-FA修饰阿霉素纳米脂质体/ADR-HAS-MS单抗Hab18偶联阿霉素人血清白蛋白微球的制备方法

瑞禧生物这里整理的内容是CMCT-FA修饰阿霉素纳米脂质体/ADR-HAS-MS单抗Hab18偶联阿霉素人血清白蛋白微球的相关制备方法&#xff0c;来学习&#xff01; MCT-FA修饰阿霉素纳米脂质体的研究&#xff1a; 利用1-乙基-3-(3-二甲基丙基)-碳二亚胺(EDC)介导 反应合成了叶酸偶联的羧甲…

LeetCode刷题---160. 相交链表(双指针-对撞指针)

文章目录一、编程题&#xff1a;160. 相交链表&#xff08;双指针-对撞指针&#xff09;1.题目描述2.示例1&#xff1a;3.示例2&#xff1a;4.示例3&#xff1a;5.提示&#xff1a;6.提示&#xff1a;二、解题思路1.思路2.复杂度分析&#xff1a;3.算法图解三、代码实现总结一、…

CSS——图标字体

为什么需要图标字体&#xff1f; 在网页中经常会有需要使用一些图标的地方&#xff0c;比如&#x1f6d2; &#xff0c; &#x1f464;&#xff0c;⏫等等&#xff0c;虽然我们可以通过图片来引入图标&#xff0c;但是图片本身比较大&#xff0c;页面刷新加载图片耗时不说&…

[附源码]java毕业设计小区疫情防控系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

知乎乱码、b站首页乱码、蓝奏云网盘打不开都是DNS惹的祸!修改设备的DNS

知乎乱码、b站首页乱码、蓝奏云网盘打不开等问题&#xff0c;常常是连接WiFi的时候会出现&#xff0c;一旦将手机切换为蜂窝、或者给电脑用蜂窝热点这个问题就能解决&#xff1a; 接下来教一下大家如何修改自己设备的DNS,从而 修复知乎乱码、b站首页乱码、蓝奏云网盘打不开 0.…

八环氧环己基乙基笼状聚倍半硅氧|八苯胺丙基poss

八环氧环己基乙基笼状聚倍半硅氧 外观&#xff1a;透明&#xff0c;淡黄色半固体状物 黏度&#xff1a;50000cps&#xff08;60℃&#xff09; 密度&#xff1a;1.25g/ml 分子量&#xff1a;1418 环氧当量&#xff1a;177 折射率&#xff1a;1.52 溶解性&#xff1a;溶于…

所见即所得的3D打印建模设计

3D打印机安装好后&#xff0c;需要的软件环境&#xff1a; 1. Cura 这类切片软件&#xff1b; 用于将STL等模型文件转换成3D打印的执行指令集&#xff0c;其实就是G-CODE的组合&#xff0c;有些还支持在线调试。 Simplify3D https://download.csdn.net/download/pocean2012…

深度剖析数据在内存中的存储

个人主页&#xff1a;平行线也会相交 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【C/C】 目录1.数据类型详细介绍1.1数据类型介绍1.2类型的基本归类2.整型在内存中的存储3.大小端字节序介绍及判断什么是大端小端&#x…

[附源码]Python计算机毕业设计茶叶产品质量安全可追溯系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

尚医通 (二十八) --------- 医院管理相关 (引入注册中心、远程调用)

目录一、医院管理医院管理效果展示二、注册中心与服务调用1. Nacos 概述2. 注册服务三、医院管理实现1. 医院列表2. service-cmn 模块提供接口3. 封装 Feign 服务调用4. 医院接口远程调用数据字典5. 添加数据字典显示接口6. 医院列表前端一、医院管理 目前我们把医院、科室和排…

函数栈帧的创建和销毁

“总有人间一两风&#xff0c; 填我十万八千梦” &#x1f351;作者&#xff1a;小赛毛 &#x1f495;文章初次日期&#xff1a;2022/11/21 目录 函数栈帧解决了什么问题&#xff1f; 什么是栈&#xff1f; 什么是寄存器&#xff1f; 函数栈帧的创建和销毁 预热知识准备&a…

Flink DataStream API 介绍

Flink DataStream API 介绍 StreamExecutionEnvironment #mermaid-svg-JKeWa22W2vWA4zBS {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-JKeWa22W2vWA4zBS .error-icon{fill:#552222;}#mermaid-svg-JKeWa22W2vWA4z…

使用element-ui实现树形穿梭框

<template><div class"transferTreeBox"><!-- 左侧待选内容 --><div class"SelectBox"><div class"boxTitle" click"clickAllSelect">全选 ></div><div class"boxCenter"><…

【Hack The Box】Linux练习-- Frolic

HTB 学习笔记 【Hack The Box】Linux练习-- Frolic &#x1f525;系列专栏&#xff1a;Hack The Box &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年9月7日&#x1f334; &#x1f36d…

dumi 2,它来了它来了它来了

dumi 1.0 在 2020 年 3 月 2 日正式发布&#xff0c;到今天一共有 80 位 Contributor、提交 1100 Commit、为近 4000 个开源项目提供了组件库/站点的文档方案&#xff1b;dumi 作为一个 GitHub 数亿开源项目中的渺小一粒&#xff0c;能有这么多人共同参与、能为这么多项目提供价…

【JAVA程序设计】(C00097) 基于SSM的果树溯源可视化管理系统

基于SSM的果树溯源可视化管理系统项目简介项目获取开发环境项目技术运行截图项目简介 基于ssm框架的果树溯源可视化管理系统&#xff0c;本系统分为二种用户&#xff1a;管理员、农户 管理员角色包含以下功能&#xff1a; 登录、农户管理、商家管理、果树管理、地块管理、农资…

马上2023年了,终于发现一款颜值爆表的记账软件

不知道大家平时有没有记账的习惯&#xff0c;我是在疫情之后&#xff0c;才开始记账的。 记账之后&#xff0c;的确发现了很多问题。尤其是自己花钱大手大脚没有规划的毛病。 后来&#xff0c;在每个月第1周&#xff0c;我都会分析一下上一个月的账目&#xff0c;看看自己的收…

同花顺_代码解析_交易系统_J09_18

本文通过对同花顺中现成代码进行解析&#xff0c;用以了解同花顺相关策略设计的思想 目录 J_09 抛物线转向系统 J_10 均线系统 J_11 随机指标专家 J_12 顺势指标 J_15 动量线 J_16 心理线 J_17 变动速率 J_18 相对强弱指标 J_09 抛物线转向系统 指标标识由绿变红时为买…

LeetCode287之寻找重复数(相关话题:二分查找,快慢指针)

题目描述 给定一个包含 n 1 个整数的数组 nums &#xff0c;其数字都在 [1, n] 范围内&#xff08;包括 1 和 n&#xff09;&#xff0c;可知至少存在一个重复的整数。 假设 nums 只有 一个重复的整数 &#xff0c;返回 这个重复的数 。 你设计的解决方案必须 不修改 数组 …

【SIFT】超详详详解 - 实现细节记录

目录前言一、尺度空间的生成&#xff1a;高斯金字塔 Gaussian pyrmid1、图像的尺度空间 - 高斯金字塔 Gaussian pyramid2、高斯金字塔的组 与 组数 Octave1&#xff09;组2&#xff09;组数3&#xff09;升采样获得 base image3、高斯金字塔的层与层数 interval31&#xff09;层…