王道408---DS---树、二叉树、图

有序树、无序树的概念

有序树和无序树,树中结点的各子树从左到右是有次序的,不能互换,称该树为有序树,否则称为无序树。

树/二叉树的性质

树的性质

image-20231006153618412

常用的只有第一个

二叉树的性质

image-20231006153719700

常用公式也只有这一个

二叉树的存储

一般分为顺序存储与链式存储

要求顺序存储能默写

顺序存储:

typedef struct TreeNode { int data; //结点中的数据元素 bool isEmpty; //结点是否为空 } TreeNode; //初始化顺序存储的二叉树,所有结点标记为"空" void InitSqBiTree (TreeNode t[], int length) { for (int i=0; i<length; i++){ t[i].isEmpty=true; } } int main(){ TreeNode t[100]; //定义一棵顺序存储的二叉树 InitSqBiTree(t, 100); //初始化为空树 //... } //判断下标为 index 的结点是否为空 bool isEmpty(TreeNode t[], int length, int index){ if (index >= length || index < 1) return true; //下标超出合法范围 return t[index].isEmpty; } //找到下标为 index 的结点的左孩子,并返回左孩子的下标,如果没有左孩子,则返回 -1 int getLchild(TreeNode t[], int length, int index){ int lChild = index * 2; //如果左孩子存在,则左孩子的下标一定是 index * 2 if (isEmpty(t, length, lChild)) return -1; //左孩子为空 return lChild; } //找到下标为 index 的结点的右孩子,并返回右孩子的下标,如果没有右孩子,则返回 -1 int getRchild(TreeNode t[], int length, int index){ int rChild = index * 2 + 1; //如果右孩子存在,则右孩子的下标一定是 index * 2 + 1 if (isEmpty(t, length, rChild)) return -1; //右孩子为空 return rChild; } //找到下标为 index 的结点的父节点,并返回父节点的下标,如果没有父节点,则返回 -1 int getFather(TreeNode t[], int length, int index){ if (index == 1) return -1; //根节点没有父节点 int father = index / 2; //如果父节点存在,则父节点的下标一定是 index/2,整数除法会自动向下取整 if (isEmpty(t, length, father)) return -1; return father; }

线索二叉树

image-20231016141340169

image-20231016141405721

规定:若无左子树,令lchi1d指向其前驱结点:若无右子树,令rchi1d指向其后继结点。

1、先序遍历的线索二叉树不能直接找到度为2的前驱
2、后序遍历的线索二叉树不能直接找到度为2的后继

树和图的存储

二叉树的存储一般采用链表或数组

一般的树的存储手段有三种

1、双亲表示法
img
2、孩子表示法
img

有点像邻接表

3、孩子兄弟表示法
img

哈夫曼树的一些性质

带权路径长度的概念

在许多应用中,树中结点常常被赋予一个表示某种意义的数值,称为该结点的权。从树的根到任意结点的路径长度(经过的边数)与该结点上权值的乘积,称为该结点的带权路径长度。树中所有叶结点的带权路径长度之和称为该树的带权路径长度,记为

WPL=i=1nwili

其中带权路径长度(WPL)最小的二叉树称为哈夫曼树

image-20231016140902869

注意哈夫曼树的虚叶节点情况

image-20231016144900020

哈夫曼树不一定是是一棵完全二叉树

树中一定没有度为1的结点

树中两个权值最小的结点一定是兄弟结点

树中任一非叶结点的权值一定不小于下一层任一结点的权值

计算机网络中A,B,C类网络的划分以及动态子网的划分,其思想与哈夫曼编码如出一辙

并查集

代码

自从大纲改版还没考过,感觉今年很可能考,这是个重点!最次也要会默写朴素算法的并查集

代码如下(涉及了路径压缩、小树合并到大树等优化策略)

#include <stdio.h> #define MAXSIZE 50 int UFSets[MAXSIZE]; void InitUFSets(int S[]){ for(int i=0;i<MAXSIZE;i++){ S[i] = -1; } } int find(int S[],int x){ if(S[x] <= -1) return x; // -1说明到底了 return find(S,S[x]); } int pathCompFind(int S[],int x){ // 路径压缩策略优化Find 操作 if(S[x] >= 0){ S[x] = find(S,S[x]); return S[x]; } return x; } bool UnionElem(int S[],int elem1,int elem2){ //王道书上是直接合并的root,而不是元素成员 int first = find(S,elem1); int second = find(S,elem2); if(first != second){ printf("%d -- %d\n",first,second); S[second] = first; return 1; } return 0; } bool UnionRoot(int S[],int Root1,int Root2){ if(Root1 == Root2) return 0; //比较两个根是否来自同一集合 S[Root2] = Root1; // 将根Root2连接到Root1上 return 1; } bool optUnionRoot(int S[],int Root1,int Root2){ if(Root1 == Root2) return 0; if(S[Root2] > S[Root1]){ // Root2 结点数更少 // 因为初始化的时候S的值全为 -1 所以 若S[Root1]与S[Root2]相加后为更小的负数 //较小的树,树较大,这也是为什么 S[Root1] >= S[Root2]时 却把Root2合并到Root1中的原因 S[Root1] += S[Root2]; // 累加节点个数 S[Root2] = Root1; }else{ S[Root2] += S[Root1]; S[Root1] = Root2; } return 1; } void debug(){ for(int i=0;i<MAXSIZE;i++){ printf("%d ",UFSets[i]); } puts(""); } int main(){ InitUFSets(UFSets); int arr[100] = {0,1,2,3,4,5,6,7,8,9}; for(int i=0;i<=10;i++){ UnionElem(UFSets,arr[i*2],arr[i*2+1]); // 0 -- 1 // 2 -- 3 // 4 -- 5 // 6 -- 7 // 8 -- 9 } UnionElem(UFSets,1,3); UnionElem(UFSets,5,7); // 0 -- 1 // 2 -- 3 // 4 -- 5 // 6 -- 7 // 8 -- 9 // 0 -- 2 // 4 -- 6 UnionElem(UFSets,1,6); UnionElem(UFSets,2,7); // debug(); return 0; }

朴素算法代码如下

#define SIZE 13 int UFSets[SIZE]; //用一个数组表示并查集 //初始化并查集 void Initial(int S[]){ for(int i=0;i<SIZE;i++) S[i]=-1; } //Find “查”操作,找x所属集合(返回x所属根结点) int Find(int S[],int x){ while(S[x]>=0) //循环寻找x的根 x=S[x]; return x; //根的S[]小于0 } //Union “并”操作,将两个集合合并为一个 void Union(int S[],int Root1,int Root2){ //要求Root1与Root2是不同的集合 if(Root1==Root2) return; //将根Root2连接到另一根Root1下面 S[Root2]=Root1; }

优化方案

朴素算法

加入存在集合A与B

每次Union的时候,都会使集合B的根节点指向集合A的根节点:

image-20231124152425967

对于朴素算法的Find函数,最坏的时间复杂度可能到达O(n)

image-20231124152721690

树高越高,Find操作的时间复杂发也就越大

那么我们的优化思路就是,尽量减小树的高度:

小树合并到大树的优化策略---对Union优化

我们可以直观的感受一下,如果我们用大树合成到小树上,会发现整体树的高度会加1

image-20231124153046944

如果我们用小树合成到大树上,会发现整体树的高度不会改变!

image-20231124153200105

那么我们可以在每次Union的时候都让小树指向大树,为此我们需要用一个值来表示树的结点总数

我们选取根节点的绝对值作为结点总数:

//Union"并"操作,小树合并到大树 void Union(int S[],int Root1,int Root2){ if(Root1==Root2) return; if(S[Root2]>S[Root1]){//Root2结点数更少 S[Root1]+=S[Root2];//累加结点总数 S[Root2]=Root1;//小树合并到大树 }else{ S[Root2]+=S[Root1];//累加结点总数 S[Root1]=Root2;//小树合并到大树 } }

image-20231124153624343

这样优化之后会使整个树的高度不超过log2n+1 (注意如果M不是整数,⌊ M ⌋ + 1 != ⌈M⌉ )

这样就可以保证O(log2n)的时间复杂度

路径压缩---对Find函数的优化

压缩路径---Find操作,先找到根节点,再将查找路径上所有结点都挂到根结点下

image-20231124160704456

代码如下:

int optFind(int m){ if(A[m] > -1){ A[m] = optFind(A[m]); return A[m]; } return m; }

每次Find操作,先找根,再"压缩路径",可使树的高度不超过O(α(n)). α(n)是一个增长很缓慢的函数,对于常见的n值,通常α(n)≤4,因此优化后并查集的Find、Union梁作时间开销都很低

另外find的功能是这样的:

在把1合并到6结点的时候可以发现,find函数并不是把1所在的子树的所有结点都连接到6结点下,而是先把1所在子树的结点都指向1所在子树的根,再把根指向6所在结点的根

image-20231130194734634

算法复杂度分析

朴素算法

一次Find 最坏时间复杂度 : O(n)

总体是O(n^2)的时间复杂度

小树并到大树

一次Find 最坏时间复杂度 : O(log2n)

总体是 : O(log2n)

路径压缩

一次Find 最坏时间复杂度 : O(α(n))

总体是 : O(n · α(n))

树、森林、二叉树遍历对应关系

image-20231006164649384

这里需要注意的是

  1. 树的后根遍历相对于树来说是后序遍历,相当于其对应的二叉树来说是中序遍历
  2. 森林的中序遍历实际上是对森林后序遍历,其结果等价于对应二叉树的中序遍历,不知道为什么给起了一个中序遍历的名字,可能是其无法进行真正的中序遍历的原因吧

如图:

image-20231006165120982

对森林中序遍历(实质上是后序遍历)可得到 BCDAFEHIG ,等价于对二叉树的中序遍历


__EOF__

本文作者_TLSN
本文链接https://www.cnblogs.com/lordtianqiyi/p/17760058.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   TLSN  阅读(183)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示