数据结构——被优先队列玩起来的“树“

2024-02-01 04:30

本文主要是介绍数据结构——被优先队列玩起来的“树“,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

树——最终篇

    • ⭐⭐⭐
    • 什么是堆🌊🌊
      • ✅什么是优先队列
      • ✅优先队列的重要性
      • ✅优先队列和堆的暧昧关系
      • ✅回忆完全二叉树
      • ✅二叉堆
      • ✅举点栗子
      • ✅二叉堆的特性
    • 堆的抽象数据类型描述(最大堆演示带入)🌊🌊
      • ✅堆的主要操作
      • ✅最大堆的创建
      • ✅最大堆的插入
      • ✅最大堆的删除
    • 手动模拟堆⭐⭐⭐
    • 参考代码(C++版本)
      • ✅模块化剖析
        • heap_swap ✨ ✨ ✨
        • down(int u)
        • up(int u)
    • 树和森林与二叉树的转换🌊🌊
      • ✅树转换为二叉树
      • ✅森林转换为二叉树
      • ✅二叉树转换为树
      • ✅二叉树转换为森林
    • 哈夫曼树和哈夫曼编码🌊🌊
      • ✅日常瞎叭叭叭点闲话
      • ✅哈夫曼树的定义
    • 哈夫曼树的构造
      • 参考实现代码
      • ✅总结哈夫曼树的特点
    • 哈夫曼编码
      • ✅哈夫曼编码的生成方式
  • 写在最后🎈🎈🎈
    • 若有偏僻,欢迎各位小伙伴及时雅正( ^ - ^ )
    • 持续更新对数据结构的总结和理解喔💖~
      • 小伙伴们都应该也在备战期末了,笔者也在慢慢备考,更文就会慢一点点哈,原谅我。期末加油~ 🌹🌹🌹

小伙伴看完中篇的BST树和AVL树可能会觉得,树也太折腾人了,放过孩子吧。最终篇是不是要甩大招了喃,不😭,😭😭

最终章了,主要的设计是为了对付那人人得而诛之的期末考试,我下面所总结的内容,除了堆会在算法竞赛中用到,其他两个就让它们在期末考试的舞台上蹦跶吧,可恶的考试 💔

考试

⭐⭐⭐

什么是堆🌊🌊

✅什么是优先队列

优先队列(Priority Queue):一种特殊的队列,取出元素的顺序是依照元素的优先权(关键字)大小,而不是元素进入队列的先后顺序

经验出发,我们可以用数组实现、也可以用链表实现,下面我们通过一张图看看它们的实现效果
效果对比

通过对比可以看出来,要么时间复杂度过高,要么就只是在某一种操作上很突出,其他操作又相对难受了,整体都是不太尽人意。那么是否可以采用二叉树存储结构?假如二叉树的结点数是n,那么一棵完全二叉树的树高就是logN。利用这一性质,确实可以实现高速的管理数据

✅优先队列的重要性

优先队列

✅优先队列和堆的暧昧关系

优先队列

能够完成下列操作的数据结构叫做优先队列

  1. 插入一个数组
  2. 取出最值(能获得数值,能实现删除)

示意图

能够使用二叉树高效解决上述问题的数据结构被称为堆

✅回忆完全二叉树

没有满二叉树那么完美,最后可能会缺一些子树,但是一定一定要保证子树是按照层序编号排列的二叉树
完全二叉树

✅二叉堆

如果一棵完全二叉树各结点的键值与一个数组的各元素具备如图所示的对应关系,那么这个完全二叉树就是二叉堆。
二叉堆

二叉堆的逻辑结构是完全二叉树,但是实际却是一个用1作为起点的一维数组来表示的

若将表示二叉堆的数组命名为A,二叉堆的元素大小(元素个数)为H,那么二叉堆的元素就存储在A[1,…,H]中,其中根的下标为1.当给定一个结点的下标i时,就可以通过i/2、2 * i 、 2 * i +1轻松的算出来其父结点parent(i)、左子结点left(i)、右子结点right(i)。

✅举点栗子

二叉堆的一些栗子

✅二叉堆的特性

  1. 结构性:用数组表示的完全二叉树;
  2. 有序性:任一结点的关键字是其子树所有结点的最大值(或最小值)
    1. “最大堆(MaxHeap)”,也称“大顶堆”:最大值
    2. “最小堆(MinHeap)”,也称“小顶堆” :最小值

最大堆的根中存储着最大元素,回看二叉堆的图,可以看出来它就是一个最大堆

堆的抽象数据类型描述(最大堆演示带入)🌊🌊

✅堆的主要操作

最大堆 MaxHeap H,元素ElementType item

  1. MaxHeap Create( int MaxSize ):创建一个空的最大堆。
  2. Boolean IsFull( MaxHeap H ):判断最大堆H是否已满。
  3. Insert( MaxHeap H, ElementType item ):将元素item插入最大堆H。
  4. Boolean IsEmpty( MaxHeap H ):判断最大堆H是否为空。
  5. ElementType DeleteMax( MaxHeap H ):返回H中最大元素(高优先级)。

✅最大堆的创建

struct HeapStruct{int Capacity;//堆的最大容量 int Size;//堆的当前元素个数 ElementType *Elements;//存储堆元素的数组 
};MaxHeap Create( int MaxSize )
{//创建一个容量为MaxSize的空的最大堆MaxHeap H = malloc(sizeof(struct HeapStruct));H->Elements = malloc((MaxSize + 1) * sizeof (ElementType));H->Size = 0;H->Capacity = MaxSize;H->Elements[0] = MaxData;//在数组没有用到0号索引处放置一个哨兵。待会再理解为什么要放置它 return H;
} 

✅最大堆的插入

操作流程:

  1. 将这个新的元素插入到堆的最后一个元素,因为是用数组实现的堆,在数组的末尾直接修改数组的长度就可以实现插入或者删除操作。

  2. 将其和它的父结点作比较,倘若它的数值比根节点的大,它就往上走,再比较,直到调整为符合二叉堆的性质为止。

void Insert(MaxHeap H, ElementType item) //将元素item插入最大堆H,其中H->Elements[0]已经定义为哨兵
{int i;if(IsFull(H))  {printf("堆已满");return ;}i = ++H->Size;//i指向插入后堆中的最后一个元素的位置for(; H->Elements[i/2] < item;i /= 2) H->Elements[i] = H->Elements[i/2];//向上调整。将这个插入的数据调整到合适它的位置H->Elements[i]  = item;//将item插入 
} 

H->Element[ 0 ] 是哨兵元素,它不小于堆中的最大元素,控制顺环结束。在加了哨兵来控制循环的结束以后,我们要考虑的内容就减少到怎么实现逻辑,不用多花时间去想循环要执行多少次
哨兵

✅最大堆的删除

删除思路和插入是类似的,因为二叉堆的本质是用数组模拟的,删除数组的最后一位是很容易的,直接数组长度长度就可以了,所以这里的思路是用数组的最后一个元素替代要删除的元素,再调整

ElementType DeleteMax(MaxHeap H) 
{int Parent ,Child;//代表两个指针,分别指向父结点和子结点 ElementType MaxItem,temp;if(IsEmpty(H)) {printf("最大堆为空");return ;}MaxItem = H->Elements[1];//取出根节点的最大值temp = H->Elements[H->Size--] ;//获取最大堆的最后一个元素从根节点Parent开始过滤 for(Parent = 1;Parent * 2 <= H->Size;Parent = Child){Child = Parent * 2;if((Child != H->Size) && (H->Elements[Child] < H->Elements[Child+1])) Child ++;//Child指向左右子结点的较大者if(temp >= H->Elements[Child]) break;//满足最大堆的要求了,调整结束 else H->Elements[Parent] = H->Elements[Child];}H->Elements[Parent] = temp;return MaxItem;
}

手动模拟堆⭐⭐⭐

C++选手确实可以直接调用STL容器中的priority_queue实现优先队列。和queue一样,push()用于向队列中插入一个元素,top()用于访问(读取)开头元素,pop()用于删除开头元素。在priority_queue中,开头元素永远是拥有最高优先级的元素。

对于没有这个库函数的选手,可以练习如何手写堆
模拟堆

参考代码(C++版本)

#include <iostream>
#include <algorithm>
#include <string.h>using namespace std;const int N = 100010;int h[N], ph[N], hp[N], cnt;void heap_swap(int a, int b)
{swap(ph[hp[a]],ph[hp[b]]);swap(hp[a], hp[b]);swap(h[a], h[b]);
}void down(int u)
{int t = u;if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;if (u != t){heap_swap(u, t);down(t);}
}void up(int u)
{while (u / 2 && h[u] < h[u / 2]){heap_swap(u, u / 2);u >>= 1;}
}int main()
{int n, m = 0;scanf("%d", &n);while (n -- ){char op[5];int k, x;scanf("%s", op);if (!strcmp(op, "I")){scanf("%d", &x);cnt ++ ;m ++ ;ph[m] = cnt, hp[cnt] = m;h[cnt] = x;up(cnt);}else if (!strcmp(op, "PM")) printf("%d\n", h[1]);else if (!strcmp(op, "DM")){heap_swap(1, cnt);cnt -- ;down(1);}else if (!strcmp(op, "D")){scanf("%d", &k);k = ph[k];heap_swap(k, cnt);cnt -- ;up(k);down(k);}else{scanf("%d%d", &k, &x);k = ph[k];h[k] = x;up(k);down(k);}}return 0;
}

✅模块化剖析

heap_swap ✨ ✨ ✨

swap

int ph[N];//ph是从指针数组指向堆heap,存的是第k个插入的数在堆里面下标是多少 
int hp[N];//hp就是从堆heap映射到指针,堆里面的某一个点是第几个插入的点

手动实现堆最主要的就是实现它的交换。为此,我们额外开了两个数组。代表从指针数组pointer指向堆heap的数组ph 。 以及代表从堆heap指向指针数组pointer的hp数组。

因为是用数组模拟的堆,在使用指针的思想进行定位的时候,我们利用ph数组中存的索引下标定位到它在堆中的位置。也可以通过堆中的点,找回来它在数组中的索引
交换

down(int u)

对这个传入的位置进行向下调整。假如它的左右孩子存在,并且比它这个父结点小,就把父结点交换下去。

    int t = u;if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
up(int u)

当传入的位置的父结点存在,而且这个位置中的数据比父结点中的数据大,就将这个位置和父结点交换,实现向上调整

while (u / 2 && h[u] < h[u / 2]){heap_swap(u, u / 2);u >>= 1;}

✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨

下面的内容有个印象就好了。它们只会在考场上折磨咱们
退学

树和森林与二叉树的转换🌊🌊

✅树转换为二叉树

步骤:

  1. 加线:把除根节点之外的所有兄弟结点连起来
    树转二叉树加线

  2. 去线:把除长子(最左边的结点)以外的其他孩子结点之间的线去掉
    树转二叉树去线

  3. 层次调整:以根结点为圆心,每个结点顺时针旋转到看起来舒服的位置就好
    层次调整

✅森林转换为二叉树

森林是由若干树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,我们可以按照处理兄弟的处理方式来操作。

  1. 把森林中每一棵树转换二叉树
  2. 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的右孩子
    森林转二叉树

✅二叉树转换为树

  1. 加线:如果某个结点的左孩子有右子树,让该结点和其左孩子的右子树的右分支上各结点连起来
  2. 去线:去掉原二叉树中所有结点与其右子树的连线
  3. 调整
    二叉树转树

✅二叉树转换为森林

  1. 特判。只看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树
  2. 从根结点连线,若右孩子存在,就把它与右孩子的线去掉
  3. 看看分出的二叉树,若右孩子存在,就继续删除连线,直到没有👀
    二叉树转森林

哈夫曼树和哈夫曼编码🌊🌊

✅日常瞎叭叭叭点闲话

哈夫曼编码了,笔者目前也没有在竞赛题或者面试题中看到,但是期末考试好像要考吧~,所以这里梳理了一下,考试的时候解决它就行。前辈们发明的创世之作,感觉现在还拿捏不住
拿捏不住

✅哈夫曼树的定义

  1. 路径:从树中一个结点到另一个结点之间的分支构成两个结点之间的路径
  2. 路径长度:路径上分支数目
  3. 树的路径长度:树根到每一个结点的路径长度之和
    路径的长度

如上图所示,第一个图中,根结点到D结点的路径长度是4。第二个图中,根结点到D结点的路径长度是2

如果考虑带权的结点,结点的带权路径长度为从该结点到树根之间的路径长度与权值的乘积。

哈夫曼树:设二叉树有n个叶子结点,每个叶子结点带有权值 wk,从根结点到每个叶子结点的长度为 lk,则其中带权路径长度WPL最小的二叉树。

哈夫曼树的构造

每次把权值最小的两棵二叉树合并。

  1. 将给定的n个权值{w1,w2,…,wn}作为n个根结点的权值构造一个具有n棵二叉树的森林{T1,T2,…,Tn},其中每棵树只有一个根节点。也就是把每个权值单独放在一个结点中,直接作为一棵树
  2. 森林中选取两棵树中根结点权值最小的的二叉树作为左右子树构造一棵新的二叉树,新的二叉树的根结点权值为原来两棵树根结点的权值和
  3. 在森林中将被选取的两个根节点从森林中删除,将新构建的二叉树加入到森林中
  4. 重复2 和 3操作
    构造流程

参考实现代码

//注:这只是核心流程的代码,部分函数这里没有附上的~
typedef struct TreeNode *HuffmanTree;
struct TreeNode{int Weight;HuffmanTree Left , Right;
};HuffmanTree Huffman(MinHeap H)
{//假设H->Size个权值已经存在H->Elements[]->Weight里int i;HuffmanTree T;BuildMinHeap(H);//将H->Elements[]按权值调整为最小堆for(i = 1; i < H->Size;i++)  //做 H->Size -1 次合并 {T = malloc(sizeof (struct TreeNode)) ;T->Left = DeleteMin(H);//从最小堆中删除一个结点,作为新T的左子结点 T->Right = DeleteMin(H) ;//最小堆中删除一个结点,作为新T的右子结点 T->Weight = T->Left->Weight + T->Right->Weight;//计算新的权值Insert(H,T) ;//将新的T插入到最小堆 }T = DeleteMin(H) ;return T;
}

✅总结哈夫曼树的特点

  1. 没有度为1的结点;
  2. n个叶子结点的哈夫曼树共有2n-1个结点
  3. 哈夫曼树的任意非叶节点的左右子树交换后仍是哈夫曼树
  4. 对同一组权值{w1 ,w2 , …… , wn},存在不同构的两棵哈夫曼树
    不同构

哈夫曼编码

哈夫曼编码是一种用于使编码总长最短的编码方案。目的也很直接,压缩数据存储和传输的成本,就可以省钱啦~
没钱了

✅哈夫曼编码的生成方式

  1. 构造哈夫曼树,设需要编码的字符集合为{d1,d2,…,dn},它们在电文中的权重集合为{w1,w2,…,wn},以d1,d2,…,dn作为叶子结点,w1,w2,…,wn作为它们的权值,构造一棵哈夫曼树
  2. 在哈弗曼树上求叶子结点的编码。规定哈夫曼树中的左分支代表0,右分支代表1,则从根结点到每个叶子结点所经过的路径分支组成的0和1的序列便是该结点的对应字符的不等长编码。

写在最后🎈🎈🎈

若有偏僻,欢迎各位小伙伴及时雅正( ^ - ^ )

持续更新对数据结构的总结和理解喔💖~

小伙伴们都应该也在备战期末了,笔者也在慢慢备考,更文就会慢一点点哈,原谅我。期末加油~ 🌹🌹🌹

高分

这篇关于数据结构——被优先队列玩起来的“树“的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/666082

相关文章

【数据结构】线性表:顺序表

文章目录 1. 线性表2. 顺序表2.1 概念及结构2.2 接口实现2.3 顺序表的问题及思考 1. 线性表 线性表是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串… 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式

数据结构9——排序

一、冒泡排序 冒泡排序(Bubble Sort),顾名思义,就是指越小的元素会经由交换慢慢“浮”到数列的顶端。 算法原理 从左到右,依次比较相邻的元素大小,更大的元素交换到右边;从第一组相邻元素比较到最后一组相邻元素,这一步结束最后一个元素必然是参与比较的元素中最大的元素;按照大的居右原则,重新从左到后比较,前一轮中得到的最后一个元素不参4与比较,得出新一轮的最大元素;按照上述规则,每一轮结

算法与数据结构面试宝典——回溯算法详解(C#,C++)

文章目录 1. 回溯算法的定义及应用场景2. 回溯算法的基本思想3. 递推关系式与回溯算法的建立4. 状态转移方法5. 边界条件与结束条件6. 算法的具体实现过程7. 回溯算法在C#,C++中的实际应用案例C#示例C++示例 8. 总结回溯算法的主要特点与应用价值 回溯算法是一种通过尝试各种可能的组合来找到所有解的算法。这种算法通常用于解决组合问题,如排列、组合、棋盘游

嵌入式学习——数据结构(哈希、排序)——day50

1. 查找二叉树、搜索二叉树、平衡二叉树 2. 哈希表——人的身份证——哈希函数 3. 哈希冲突、哈希矛盾 4. 哈希代码 4.1 创建哈希表 4.2  5. 算法设计 5.1 正确性 5.2 可读性(高内聚、低耦合) 5.3 健壮性 5.4 高效率(时间复杂度)时间复杂度越低,效率越高, 5.5 低储存(空间复杂度)空间复杂度越低,存储空间越少 6.排序算法 6.1 冒

【数据结构与算法 经典例题】使用队列实现栈(图文详解)

💓 博客主页:倔强的石头的CSDN主页               📝Gitee主页:倔强的石头的gitee主页    ⏩ 文章专栏:《数据结构与算法 经典例题》C语言                                   期待您的关注 ​​ 目录  一、问题描述 二、前置知识 三、解题思路 四、C语言实现代码 🍃队列实现代码:

数据结构:二叉树详解 c++信息学奥赛基础知识讲解

目录 一、二叉树的定义 二、二叉树的形态 三、二叉树的性质 四、二叉树的存储 五、二叉树的创建与遍历(递归) 六、二叉树实现 创建二叉树 展示二叉树 1、计算数的高度 2、计算数的叶子数量 3、计算数的宽度 4、层次遍历 5、前序遍历 递归写法 非递归写法 6、中序遍历 递归写法 非递归写法 7、后序遍历 递归写法 非递归写法 8、输出根节点到所有叶

RabbitMQ实践——临时队列

临时队列是一种自动删除队列。当这个队列被创建后,如果没有消费者监听,则会一直存在,还可以不断向其发布消息。但是一旦的消费者开始监听,然后断开监听后,它就会被自动删除。 新建自动删除队列 我们创建一个名字叫queue.auto.delete的临时队列 绑定 我们直接使用默认交换器,所以不用创建新的交换器,也不用建立绑定关系。 实验 发布消息 我们在后台管理页面的默认交换器下向这个队列

Java并发编程—阻塞队列源码分析

在前面几篇文章中,我们讨论了同步容器(Hashtable、Vector),也讨论了并发容器(ConcurrentHashMap、CopyOnWriteArrayList),这些工具都为我们编写多线程程序提供了很大的方便。今天我们来讨论另外一类容器:阻塞队列。   在前面我们接触的队列都是非阻塞队列,比如PriorityQueue、LinkedList(LinkedList是双向链表,它实现了D

剑指offer—编程题7(用两个栈实现一个队列)

题目:用两个栈实现一个队列。队列的声明如下,请实现它的两个函数appendTail 和deleteHead,分别完成在队列尾部插入结点和在队列头部删除结点的功能。 代码如下: [java]  view plain copy print ? public class Test07 {       /**       * 用两个栈模拟的队列       *

Java数据结构4-链表

1. ArrayList的缺陷 由于其底层是一段连续空间,当在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低,因此ArrayList不适合做任意位置插入和删除比较多的场景。因此:java集合中又引入了LinkedList,即链表结构。 2. 链表 2.1 链表的概念及结构 链表是一种物理存储结构上非连续存储结构,数据元素