数据结构——配对堆

引入

配对堆是一个支持插入查询/删除最小值合并修改元素等操作的数据结构,是一种可并堆。有速度快和结构简单的优势,但由于其为基于势能分析的均摊复杂度,无法可持久化。

定义

配对堆是一棵满足堆性质的带权多叉树(如下图),即每个节点的权值都小于或等于他的所有儿子(以小根堆为例,下同)。
在这里插入图片描述

通常我们使用儿子 - 兄弟表示法储存一个配对堆(如下图),一个节点的所有儿子节点形成一个单向链表。每个节点储存第一个儿子的指针,即链表的头节点;和他的右兄弟的指针。

这种方式便于实现配对堆,也将方便复杂度分析。

在这里插入图片描述

struct Node {T v;  // T为权值类型Node *child, *sibling;// child 指向该节点第一个儿子,sibling 指向该节点的下一个兄弟。// 若该节点没有儿子/下个兄弟则指针指向 nullptr。
};

从定义可以发现,和其他常见的堆结构相比,配对堆不维护任何额外的树大小,深度,排名等信息(二叉堆也不维护额外信息,但它是通过维持一个严格的完全二叉树结构来保证操作的复杂度),且任何一个满足堆性质的树都是一个合法的配对堆,这样简单又高度灵活的数据结构奠定了配对堆在实践中优秀效率的基础;作为对比,斐波那契堆糟糕的常数就是因为它需要维护很多额外的信息。

配对堆通过一套精心设计的操作顺序来保证它的总复杂度,原论文1将其称为「一种自调整的堆(Self Adjusting Heap)」。在这方面和 Splay 树(在原论文中被称作「Self Adjusting Binary Tree」)颇有相似之处。

过程

查询最小值

从配对堆的定义可看出,配对堆的根节点的权值一定最小,直接返回根节点即可。

合并

合并两个配对堆的操作很简单,首先令两个根节点较小的一个为新的根节点,然后将较大的根节点作为它的儿子插入进去。(见下图)

在这里插入图片描述
需要注意的是,一个节点的儿子链表是按插入时间排序的,即最右边的节点最早成为父节点的儿子,最左边的节点最近成为父节点的儿子。

实现

Node* meld(Node* x, Node* y) {// 若有一个为空则直接返回另一个if (x == nullptr) return y;if (y == nullptr) return x;if (x->v > y->v) std::swap(x, y);  // swap后x为权值小的堆,y为权值大的堆// 将y设为x的儿子y->sibling = x->child;x->child = y;return x;  // 新的根节点为 x
}

插入

合并都有了,插入就直接把新元素视为一个新的配对堆和原堆合并就行了。

删除最小值

首先要提及的一点是,上文的几个操作都十分偷懒,完全没有对数据结构进行维护,所以我们需要小心设计删除最小值的操作,来保证总复杂度不出问题。

根节点即为最小值,所以要删除的是根节点。考虑拿掉根节点之后会发生什么:根节点原来的所有儿子构成了一片森林;而配对堆应当是一棵树,所以我们需要通过某种顺序把这些儿子全部合并起来。

一个很自然的想法是使用 m e l d meld meld 函数把儿子们从左到右挨个并在一起,这样做的话正确性是显然的,但是会导致单次操作复杂度退化到 O ( n ) O(n) O(n)

为了保证总的均摊复杂度,需要使用一个「两步走」的合并方法:

1.把儿子们两两配成一对,用 meld 操作把被配成同一对的两个儿子合并到一起(见下图 1),
2.将新产生的堆** 从右往左**(即老的儿子到新的儿子的方向)挨个合并在一起(见下图 2)。

在这里插入图片描述
在这里插入图片描述

先实现一个辅助函数merges,作用是合并一个节点的所有兄弟。

实现

Node* merges(Node* x) {if (x == nullptr || x->sibling == nullptr)return x;  // 如果该树为空或他没有下一个兄弟,就不需要合并了,return。Node* y = x->sibling;                // y 为 x 的下一个兄弟Node* c = y->sibling;                // c 是再下一个兄弟x->sibling = y->sibling = nullptr;   // 拆散return meld(merges(c), meld(x, y));  // 核心部分
}

最后一句话是该函数的核心,这句话分三部分:
1.meld(x,y)「配对」了 x 和 y。
2.merges( c ) 递归合并 c 和他的兄弟们。
3.将上面 2 个操作产生的 2 个新树合并。

需要注意到的是,上文提到了第二步时的合并方向是有要求的(从右往左合并),该递归函数的实现已保证了这个顺序,如果读者需要自行实现迭代版本的话请务必注意保证该顺序,否则复杂度将失去保证。

有了 merges 函数,delete-min 操作就显然了。

Node* delete_min(Node* x) {Node* t = merges(x->child);delete x;  // 如果需要内存回收return t;
}

减小一个元素的值

要实现这个操作,需要给节点添加一个「父」指针,当节点有左兄弟时,其指向左兄弟而非实际的父节点;否则,指向其父节点。

首先节点的定义修改为:

struct Node {LL v;int id;Node *child, *sibling;Node *father;  // 新增:父指针,若该节点为根节点则指向空节点 nullptr
};

meld 操作修改为:

Node* meld(Node* x, Node* y) {if (x == nullptr) return y;if (y == nullptr) return x;if (x->v > y->v) std::swap(x, y);if (x->child != nullptr) {  // 新增:维护父指针x->child->father = y;}y->sibling = x->child;y->father = x;  // 新增:维护父指针x->child = y;return x;
}

merges操作修改为:

Node *merges(Node *x) {if (x == nullptr) return nullptr;x->father = nullptr;  // 新增:维护父指针if (x->sibling == nullptr) return x;Node *y = x->sibling, *c = y->sibling;y->father = nullptr;  // 新增:维护父指针x->sibling = y->sibling = nullptr;return meld(merges(c), meld(x, y));
}

现在我们来考虑如何实现 decrease-key 操作。
首先我们发现,当我们减少节点 x 的权值之后,以 x 为根的子树仍然满足配对堆性质,但 x 的父亲和 x 之间可能不再满足堆性质。
因此我们把整棵以 x 为根的子树剖出来,现在两棵树都符合配对堆性质了,然后把他们合并起来,就完成了全部操作。

// root为堆的根,x为要操作的节点,v为新的权值,调用时需保证 v <= x->v
// 返回值为新的根节点
Node *decrease_key(Node *root, Node *x, LL v) {x->v = v;                 // 更新权值if (x == root) return x;  // 如果 x 为根,则直接返回// 把x从fa的子节点中剖出去,这里要分x的位置讨论一下。if (x->father->child == x) {x->father->child = x->sibling;} else {x->father->sibling = x->sibling;}if (x->sibling != nullptr) {x->sibling->father = x->father;}x->sibling = nullptr;x->father = nullptr;return meld(root, x);  // 重新合并 x 和根节点
}

复杂度分析

配对堆结构与实现简单,但时间复杂度分析并不容易。

原论文1仅将复杂度分析到 melddelete-min 操作均为均摊 O ( log ⁡ n ) O(\log n) O(logn),但提出猜想认为其各操作都有和斐波那契堆相同的复杂度。

遗憾的是,后续发现,不维护额外信息的配对堆,在特定的操作序列下,decrease-key 操作的均摊复杂度下界至少为 Ω ( log ⁡ log ⁡ n ) 2 \Omega (\log \log n)2 Ω(loglogn)2

目前对复杂度上界比较好的估计有,Iacono 的 O ( 1 ) O(1) O(1) meld,$O(\log n) $decrease;Pettie 的 O ( 2 2 log ⁡ log ⁡ n ) O(2^{2 \sqrt{\log \log n}}) O(22loglogn ) meld 和 decrease。需要注意的是,前述复杂度均为均摊复杂度,因此不能对各结果分别取最小值。

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

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

相关文章

C语言暑假刷题冲刺篇——day1

目录 一、选择题 二、编程题 &#x1f388;个人主页&#xff1a;库库的里昂 &#x1f390;CSDN新晋作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏✨收录专栏&#xff1a;C语言每日一练 ✨其他专栏&#xff1a;代码小游戏C语言初阶&#x1f91d;希望作者的文章能对你…

问道管理:网上如何打新股?

随着资本市场的不断敞开&#xff0c;越来越多的人开始重视股票市场&#xff0c;并想经过网上打新股来取得更大的出资收益。但是&#xff0c;网上打新股的办法并不简略&#xff0c;怎样才能成功地打新股呢&#xff1f;本文将从多个角度剖析&#xff0c;协助广阔出资者处理这一问…

海信聚好看将携新品DBdoctor,亮相中国数据库技术大会(DTCC2023)

海信聚好看将携新品DBdoctor&#xff0c;亮相中国数据库技术大会 8月16日—18日&#xff0c;第14届中国数据库技术大会&#xff08;DTCC-2023&#xff09;将在北京国际会议中心隆重召开。作为国内数据库领域规模最大的技术交流盛会&#xff0c;吸引了众多业内知名企业和数百名…

[谦实思纪 01]整理自2023雷军年度演讲——《成长》(上篇)武大回忆(梦想与成长)

文章目录 [谦实思纪]整理自2023雷军年度演讲 ——《成长》&#xff08;上篇&#xff09;武大回忆&#xff08;梦想与成长&#xff09;0. 写在前面1. 梦开始的地方1.1 要有梦想&#xff0c;要用目标量化梦想 2. 在两年内修完所有的学分。2.1 别老自己琢磨&#xff0c;找个懂的人…

【LeetCode 算法】Matrix Diagonal Sum 矩阵对角线元素的和

文章目录 Matrix Diagonal Sum 矩阵对角线元素的和问题描述&#xff1a;分析代码Math Tag Matrix Diagonal Sum 矩阵对角线元素的和 问题描述&#xff1a; 给你一个正方形矩阵 mat&#xff0c;请你返回矩阵对角线元素的和。 请你返回在矩阵主对角线上的元素和副对角线上且不…

Python爬虫IP代理池的建立和使用

写在前面 建立Python爬虫IP代理池可以提高爬虫的稳定性和效率&#xff0c;可以有效避免IP被封锁或限制访问等问题。 下面是建立Python爬虫IP代理池的详细步骤和代码实现&#xff1a; 1. 获取代理IP 我们可以从一些代理IP网站上获取免费或付费的代理IP&#xff0c;或者自己租…

【深度学习所有损失函数】在 NumPy、TensorFlow 和 PyTorch 中实现(1/2)

一、说明 在本文中&#xff0c;讨论了深度学习中使用的所有常见损失函数&#xff0c;并在NumPy&#xff0c;PyTorch和TensorFlow中实现了它们。 二、内容提要 我们本文所谈的代价函数如下所列&#xff1a; 均方误差 &#xff08;MSE&#xff09; 损失二进制交叉熵损失加权二进…

“深入解析JVM内部机制:探索Java虚拟机的奥秘“

标题&#xff1a;深入解析JVM内部机制&#xff1a;探索Java虚拟机的奥秘 JVM&#xff08;Java虚拟机&#xff09;是Java程序的核心执行环境&#xff0c;它负责将Java字节码转换为机器码并执行。了解JVM的内部机制对于理解Java程序的执行过程和性能优化至关重要。本文将深入解析…

开启想象翅膀:轻松实现文本生成模型的创作应用,支持LLaMA、ChatGLM、UDA、GPT2、Seq2Seq、BART、T5、SongNet等模型,开箱即用

开启想象翅膀&#xff1a;轻松实现文本生成模型的创作应用&#xff0c;支持LLaMA、ChatGLM、UDA、GPT2、Seq2Seq、BART、T5、SongNet等模型&#xff0c;开箱即用 TextGen: Implementation of Text Generation models 1.介绍 TextGen实现了多种文本生成模型&#xff0c;包括&a…

c++——::作用域、命名空间、using(声明和编译指令)

c 作用域和名字控制 一、::(双冒号) 作用域 <::>运算符是一个作用域如果<::>前面什么都没有加 代表是全局作用域 二、命名空间&#xff08;namespace) 1、namespace 本质是作用域,可以更好的控制标识符的作用域命名空间 就可以存放 变量 函数 类 结构体 … 2…

【kubernetes】在k8s集群环境上,部署kubesphere

部署kubesphere 学习于尚硅谷kubesphere课程 前置环境配置-部署默认存储类型 这里使用nfs #所有节点安装 yum install -y nfs-utils# 在master节点执行以下命令 echo "/nfs/data/ *(insecure,rw,sync,no_root_squash)" > /etc/exports # 执行以下命令&#xff…

QML与C++交互

目录 1 QML获取C的变量值 2 QML获取C创建的自定义对象 3 QML发送信号绑定C端的槽 4 C端发送信号绑定qml端槽 5 C调用QML端函数 1 QML获取C的变量值 QQmlApplicationEngine engine; 全局对象 上下文属性 QQmlApplicationEngine engine; QQmlContext *context1 engine.…

flowable流程移植新项目前端问题汇总

flowable流程移植到新项目时&#xff0c;出现一些前端问题&#xff0c;汇总如下&#xff1a; PS F:\khxm\NBCIO_VUE> yarn run serve yarn run v1.21.1 $ vue-cli-service serve INFO Starting development server... ERROR Error: Vue packages version mismatch: -…

25 | 葡萄酒质量数据分析

基于kaggle提供的公开数据集,对全球葡萄酒分布情况和质量情况进行数据探索和分析 from kaggle: https://www.kaggle.com/zynicide/wine-reviews 分析思路: 0、数据准备 1、葡萄酒的种类 2、葡萄酒质量 3、葡萄酒价格 4、葡萄酒描述词库 5、品鉴师信息 6、总结 0、数据准备 …

学习Vue:组件的概念和优势

在现代的前端开发中&#xff0c;组件化开发是一种重要的方法&#xff0c;它可以将复杂的应用程序拆分成多个独立的、可复用的组件。Vue.js 是一个流行的前端框架&#xff0c;它支持组件化开发&#xff0c;让开发者能够更轻松地构建和维护复杂的用户界面。在本文中&#xff0c;我…

计算机组成部分

计算机的五大部件是什么&#xff1f;答案&#xff1a;计算机的五大部件是运算器&#xff0c;控制器&#xff0c;存储器&#xff0c;输入设备和输出设备。 其中运算器和控制器合称中央处理器&#xff0c;是计算机的核心部件&#xff1b; 存储器是用来存储程序指令和数据用的&am…

修改第三方组件默认样式

深度选择器 修改el-input的样式&#xff1a; <el-input class"input-area"></el-input>查看DOM结构&#xff1a; 原本使用 /deep/ 但是可能不兼容 使用 :deep .input-area {:deep(.el-input__inner){background-color: blue;} }将 input 框背景色改为…

【Kubernetes】Kubernetes的Pod进阶

Pod进阶 一、资源限制和重启策略1. 资源限制2. 资源单位2.1 CPU 资源单位2.2 内存 资源单位 3. 重启策略&#xff08;restartPolicy&#xff09; 二、健康检查的概念1. 健康检查1.1 探针的三种规则1.2 Probe 支持三种检查方法 2. 示例2.1 exec 方式2.2 httpGet 方式2.3 tcpSock…

临床试验三原则-对照、重复、随机

临床试验必须遵循三个基本原则&#xff1a;对照、重复、随机。 一、对照原则和对照的设置 核心观点&#xff1a;有比较才有鉴别。 对照组和试验组同质可比。 三臂试验 安慰剂&#xff1a;试验组&#xff1a;阳性对照组1&#xff1a;n&#xff1a;m&#xff08;n≥m&#xff…

FFmpeg常见命令行(五):FFmpeg滤镜使用

前言 在Android音视频开发中&#xff0c;网上知识点过于零碎&#xff0c;自学起来难度非常大&#xff0c;不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》&#xff0c;结合我自己的工作学习经历&#xff0c;我准备写一个音视频系列blog。本文是音视频系…