| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 数据结构与算法 -> 霍罗维兹数据结构:堆 -> 正文阅读 |
|
[数据结构与算法]霍罗维兹数据结构:堆 |
Ⅰ. 堆的概念与性质0x00 堆的概念📚 若有一个关键码的集合???, 堆分为大堆和小堆。将其所有元素按完全二叉树的顺序存储在一个数组中: ① 如满足??? 且??? ??则称为小堆。 ② 如满足??且?? ?? ,则称为大堆。 我们将根节点最大的堆称作最大堆(即大根堆),根节点最小的堆称作最小堆(即小根堆)。 🔺 综上所述: ① 大堆:树中任何一棵树及子树中,父亲的值都大于等于孩子 (??) ② 小堆:树中任何一颗树及子树中,父亲的值都小于等于孩子?(? ) 0x01 堆的性质① 堆总是一棵完全二叉树。 ② 堆中的某个节点的值总是不大于或不小于其父节点的值。 0x02 堆的作用① 堆排序 ② 解决??问题,在??个数中找出最大的前??个或找出最小的??个 …… Ⅱ. 堆的定义
0x00 数组堆
0x01 接口函数📚 这是需要实现几个接口函数:
Ⅲ. 堆的实现0x00 堆的初始化(HeapInit)💬 Heap.h
🔑 解读:堆的实际结构就是一个数组,我们用数组去实现,想要让它支持增删查改,我们就不能使用静态数组,要用动态数组。这样我们就可以扩容,有 size 和 capacity,这里和顺序表差不多,这是一个非常通用的结构。 💬 Heap.c
0x01 堆的销毁(HeapDestroy)💬 Heap.h
💬 Heap.c
0x02 堆的打印(HeapPrint)💬 Heap.h
💬 Heap.c
0x03?判断堆是否为空(HeapIsEmpty)💬 Heap.h
💬 Heap.c
🔑 解读:这里巧妙利用布尔特性,直接 return 数组大小即可。?size?== 0 则为真,反之返回假。 0x04?大堆的插入(HeapPush)?📌 插入的核心思路:? ① 先将元素插入到堆的末尾,即最后一个孩子之后。 ② 插入之后如果堆的性质遭到破坏,就将新插入的节点顺着其的父亲往上调整到合适位置。直到调到符合堆的性质为止。
检查是否需要增容(HeapCheckCapacity)
🔑 解读:参考顺序表章节的讲解,这里不再赘述。
💭 举例: ① 比如下面的情况:新插入的为 60,子大于父(60 > 56 ),这时就需要交换。 ② 还有更特殊的情况:比如新插入的为 100,100 和 56 交换完之后还要和 70 交换。
向上调整(AdjustUp)
🔑 解读: ① 设计 AdjustUp 接口时需要先想好要接收什么值,因为我们想要对数组内容进行调整,所以我们需要传入数组,并用 int* 接收。另外用 child 接收我们刚插入的值的下标,作为调整位置的起始位置。 ②?首先要防止传入的数组为空。 ③ 我们首先要确定孩子和父亲的下标,已知孩子求父亲: ? (该公式在上一章已经学过了) 孩子下标我们已经有了,因为我们把调整位置的起始位置,也就是刚插入的数据传到了该函数中并以 child 接收了。这时我们定义 parent 变量,根据公式就可以推导出父亲的下标了。 ④ 我们已经得到了孩子和父亲的下标,我们就可以开始进行操作了。首先分析一下最坏的情况,最坏的情况就是调到根位置,child = parent,因为根节点永远是 0,child 为 0 时那么 parent 就该小于 0 了,所以当 child 作为根节点时就说明已经调完了,这里我们 while 的判断条件为 child > 0,至于为什么这里要这么别扭地以 child > 0 作为条件而不是 parent >= 0,我们下面会细说。 ⑤ 进入循环后先 if 判断,如果数组中 child 的值大于 parent 的值,说明孩子大于父亲,这意味着不符合大堆的性质。我们就要进行置换的操作。这里我们创建一个 HPDataType 类型的 tmp 临时变量进行交换即可。 ⑥ 交换完毕后,他们的值已经互相交换好了,这时我们要改变 parent 和 child 的指向,让它们继续往上走,以便于继续进行判断。child = parent,parent 再次根据公式算出新的 parent 即可。 ⑦?设计下 if 的 else 部分,如果数组的 child 的值大于 parent 的值,说明符合大堆的性质了,?break 跳出循环即可。 ? 写循环条件时为什么要写成:while(child?>?0)??我们要看是否调到根了,按照正常思路,不应该是看 parent 是否小于0吗??while(parent?>=?0) 💡 这么写是不行的!这么写实际上是存在问题的我们带入一个值看看就知道了:
🔺 所以我们要使用?while(child?>?0) ,既不会带来问题效果也和 parent>=0 一样。因为当 child =?0 时就说明已经触及到根了。 ? 这里我们用到了交换,因为后面写删除接口也是需要用到交换的,我们不妨把它封装成一个接口,方便我们后续可以多次调用: 交换(Swap)
💬 Heap.h
💬 Heap.c
🔑 解读: ① 首先检查 php 是否为空。 ② 检查是否需要扩容调用我们刚才实现好的 HeapCheckCapacity 接口即可。 ③ 随后插入数据即可,这里和顺序表的尾插的一样。 ④ 最后调用 AdjustUp 接口,传入目标数组,和插入的数据(即?size - 1)。 0x05 堆的删除(HeapPop)
📌 删除的核心思路:删除堆,删除的是堆顶的数据。就是删除这个树的根。 📚 将堆顶的数据跟最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。 ① 将堆顶元素与堆中最后一个元素进行交换。 ② 删除堆中的最后一个元素。 ③ 将堆顶元素向下调整到满足堆特性为止。
小堆向下调整(SmallAdjustDown)
🔑 解读: ① 因为要考虑左孩子还是右孩子,我们可以定义两个变量,分别记录左孩子和有孩子。但是我们这里可以用更好的方法,只需要定义一个孩子即可。具体实现方法如下:首先创建 child,我们先默认它是左孩子,利用传进来的 parent 根据公式计算出 child 的大小: 因为我们暂且默认为左孩子,我们随后进入循环后要进行检查,看看是左孩子大还是右孩子大,这里我们只需要根据数组的性质,将 child + 1 即可得到右孩子的下标,从而方便我们进行比较。比较完毕后将 child 重新赋值,拿个孩子小就将 child 给谁。 ② 一共有两个结束条件(出口),第一个结束条件是父亲小于孩子就停止,第二个结束条件是chi调整到叶子。所以,循环体判断部分根据我们分析的结束条件,如果调整到叶子就结束,我们接受了 n,也就是数组的大小,只要 child 超过数组的大小(child > n) 就结束,这是第一个出口。 ③ 进入循环后,对比左孩子和右孩子哪一个更小,child 就交付给谁。这里还要注意的是,要防止孩子不存在的情况,如果 child + 1 比 n 大,就说明孩子不存在。所以我们再写 if 判断条件的时候就要注意了,我们加一个 child + 1 < n 的条件限制一下:
④ 确认好较小的孩子后,我们就可以开始比较孩子和父亲的大小了。如果孩子小于父亲,就不符合小堆的性质,我们就要交换它们的值。这里我们直接调用我们刚才写的 Swap 接口即可,这就是为什么在写向上调整的时候要我们单独把交换部分的代码写成函数的原因。 ⑤ 交换完毕后,他们的值已经互相交换好了,这时我们要改变 parent 和 child 的指向,让它们往下走,parent = child ,child?再次根据公式算出新的?child 即可。 ⑥ 设计下 if 的 else 部分,如果数组的 child 的值大于 parent 的值,说明符合小堆的性质了,?break 跳出循环即可,这是第二个出口。
💬 Heap.h
💬 Heap.c
🔑 解读: ① 首先检查 php 是否为空。 ② 因为删除必须得有东西可删,这里肯定是要检查是否为空的,调用 HeapIsEmpty?,逻辑反操作一下,断言数组不为空。 ③ 因为删除堆,删除的是堆顶的数据,所以这里我们要在删除前让,删除数据根据我们的经验,直接 size - - 即可。 ④ 最后调用 AdjustDown?接口,传入目标数组,数组的大小,和起始位置(即 0)。 ? 如果我们想改成大堆呢? 💡 只需要改变一下判断条件即可: ① 选出左右孩子大的那一个。 ② 小堆是所有父亲都小于孩子,大堆则是所有父亲都要大于孩子,我们来改一下: 大根堆向下调整(BigAdjustDown)
🔑 解读:很简单,只需要改一下判断的条件即可。 💬 Test.c
0x06 返回堆顶数据(HeapTop)💬 Heap.h
💬 Heap.c
🔑 解读: ① 首先检查传入的?php 是否为空,还要防止堆内没有数据。 ② 返回堆顶,堆顶就是根,根的下标为 0,返回根即可。 0x07?统计堆的个数(HeapSize)💬 Heap.h
💬 Heap.c
🔑 解读: ① 首先检查传入的?php 是否为空。 ② 与其说统计堆的个数,不如说是返回堆的个数,因为根本就不需要统计,直接返回 size 就完事了。 Ⅳ. 【霍洛维兹数据结构笔记】The Heap Abstract Data Type0x00 大根堆定义A max tree is a tree in which the key value in each node is no smaller than the key values in its children (if any). A max heap is a complete binary tree that is also a max tree. 大根树中每个结点的关键字都不小于孩子结点(如果有的话)的关键字。大根堆即是完全二叉树又是大根树。 A min tree is a tree in which the key value in each node is no larger than the key values in its children (if any). A min heap is a complete binary tree that is also a min tree. 小根数中每个结点的关键字都不大于孩子节点(如果有的话)的关键字。大根堆即是完全二叉树又是小根树。 值得注意的是,我们将堆表示为一个数组,尽管我们没有使用位置0。 从堆的定义可以看出: ? ? ? ? ① 最小树的根包含树中最小的键。 ? ? ? ? ② 最大树的根包含树中的最大键。 对最大堆的基本操作:(1) 创建一个空堆? (2) 向堆中插入一个新元素? (3) 从堆中删除最大元素。 0x02?Priority Queues堆,常用来实现优先级队列。优先级队列的删除操作以队列元素的优先级高低为准。 譬如总是删除优先级最高的元素,或总是删除优先级最低的元素。 优先级队列的插入操作不限制元素的优先级,任何时刻任何优先级的元素都可以入列。 实现优先级队列:堆被用来作为优先级队列的有效实现。 0x03 大根堆插入操作?-?Insertion Into A Max Heap
我们可以通过以下步骤在一个有 ?个元素的堆中插入一个新元素。 ① 将元素放在新的节点(即第 ?个位置) ② 沿新节点到根的路径移动,如果当前节点的元素比其父节点的元素大,则将它们互换并重复。 [Program 5.13] Insertion into a max heap (大根堆插入操作)
对于?insert_max_heap 的分析: 该函数首先检查是否有一个完整的堆。 如果没有,则将 ?设置为新堆的大小()。 然后通过使用while循环确定项目在堆中的正确位置。 这个 while 循环被迭代了 ?次,因此时间复杂度为??。 0x04 大根堆删除操作 - Delete From A Max Heap当我们从一个最大堆中删除一个元素时,我们总是从堆的根部取走它。 如果堆有 ?个元素,在删除根部的元素后,堆必须成为一个完整的二叉树,少一个节点,即()个元素。。 我们把元素放在根节点的位置 ?,为了建立堆,我们向下移动堆,比较父节点和它的子节点,交换失序的元素,直到重新建立堆为止。 [Program 5.14] : Deletion from a max_heap - 大根堆删除操作
对于 delete_max_heap 的分析: 函数 delete_max_heap 的操作是向下移动堆,比较和交换父节点和子节点,直到重新建立堆的定义。由于一个有 ?个元素的堆的高度为?,while 循环了??次。因此时间复杂度为?. Ⅴ. 二叉搜索树 -?BINARY SEARCH TREES?0x00 介绍虽然堆很适合于需要优先级队列的应用,但它并不适合于我们删除和搜索任意元素的应用。 二叉搜索树在操作、插入、删除和搜索任意元素方面的性能都很不错。 0x01 定义二叉搜索树是一棵二叉树。它可能是空的。 如果它不是空的,它满足以下属性: ① 每个元素都有一个键,没有两个元素有相同的键,也就是说,键是唯一的。 ② 非空的左子树中的键必须比根中的键小。 ③ 非空的右子树中的键必须比根中的键大。 ④ 左和右子树也是二叉搜索树 思考:如果我们按顺序遍历二叉搜索树,并按访问的顺序打印节点的数据, 那么打印的数据顺序是什么? 0x02 搜索一颗二叉搜索树[Program 5.15] : Recursive search for a binary search tree
[Program 5.16] Iterative search for a binary search tree
分析:设??是二叉搜索树的高度,那么 search 和 search2 的时间复杂性都是 ?。 然而,搜索有一个额外的堆栈空间要求,是 ?。 0x03 二叉搜索树的插入要插入一个新的元素,键 。 首先,我们通过搜索树来验证该键是否与现有元素不同。 如果搜索不成功,我们就在搜索终止的地方插入该元素。 ?[Program 5.17] : Inserting an element into a binary search tree
modified_search 在二叉搜索树 *node 中搜索键 num。 如果树是空的,或者num被提出,它返回NULL。 否则,它返回一个指向搜索过程中遇到的树的最后一个节点的指针。 分析 insert_node: 设 ?为二叉搜索树的高度。 由于搜索需要 ?时间,算法的其余部分需要 Θ(1) 时间。 所以insert_node需要的总体时间是?. 0x04 二叉搜索树的删除删除一个叶子节点。 将其父级的相应子字段设置为NULL并释放该节点。 删除有单个子节点的非叶节点:erase 该节点,然后将单个子节点放在被 erase 节点的位置上。 删除有两个子节点的非叶节点:用其左子树中最大的元素或其右子树中最小的元素替换该节点。 然后从子树中删除这个被替换的元素。 注意,子树中最大和最小的元素总是在零度或一度的节点中。 不难看出,删除可以在 ?时间内进行,其中 ?是二叉搜索树的高度。 0x05 二叉搜索树的高度然而,当随机插入和删除时,二叉搜索树的高度平均为 ?。 最坏情况下高度为 ?的搜索树被称为平衡搜索树。 Ⅵ. 不相交集合的表示?-?SET REPRESENTATION0x00 引子我们研究树在集合表示中的使用。 为了简单起见,我们假设集合的元素是数字 ?。 我们同时还假设被表示的集合是成对且不相交的。 下图展示的是一个可能的表示方法: 请注意,对于每一个集合来说,节点都是从子集到父集的链接。 对这些集合进行的操作如下: ① 不相交集合的合并。如果我们希望得到两个不相交的集合 ?和 的合并,用 ?代替?和 。 ② Find(i). 找到包含元素 ?的集合 0x01 合并与查找操作 - Union and Find Operations假设我们希望得到 ?和 的合并。 我们只需让其中一棵树成为另一棵树的子树。?可以用下图中的任何一种表现形式: 为了实现集合的合并操作,我们只需将其中一个根的父域设置为另一个根。 下图展示了命名这些集合的方式: 为了简化对 union 和 find 算法的讨论,我们将忽略集合的名称,而用代表它们的树的根来识别集合。 由于树中的节点被编号为 0 到 n-1 ,我们可以用节点的编号作为一个索引。 注意,根节点的父节点为 -1 我们可以实现find(i),方法是从 ?开始跟踪指数,直到父的指数为负数为止。 [Program 5.18] : Initial attempt at union-find functions.
分析 union1 和 find1: 让我们处理以下的 union-find 操作序列: 这个序列产生了下图的蜕化树: 由于? 的时间是恒定的,所有的 ?个联合可以在时间 ?内处理完毕。 对于每个发现,如果该元素处于第i层,那么找到其根部所需的时间是 ?。 因此,处理 ?个发现所需的总时间为: 通过避免退化树的创建,我们可以获得更有效的 union 和 find 的操作实现。 0x02?定义?的加权规则。如果树 ?中的节点数少于树j中的节点数,则使 ?成为 ?的父节点;否则使???成为 ?的父节点。 为了实现加权规则,我们需要知道每棵树上有多少个节点。 也就是说,我们需要在每棵树的根部维护一个计数字段。 我们可以将根部的父字段中的计数保持为一个负数。 [Program 5.19] : Union operation incorporating the weighting rule.
引理:令 ?是一棵有 ?个节点的树,作为 ?的结果而创建,那么??的深度为: 参考资料 Fundamentals of Data Structures in C |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/30 18:43:49- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |