数据结构笔记——06树和二叉树

news2025/4/18 10:55:19

文章目录

      • 一、树的基本概念
        • 1.树的定义
        • 2.树的逻辑表示方法
        • 3.树的基本术语
        • 4.树的性质
        • 5.树的基本运算
        • 6.树的存储结构
          • 1)双亲存储结构
          • 2)孩子链存储结构
          • 3)孩子兄弟链存储结构
      • 二、二叉树的概念和性质
        • 1.二叉树的定义
        • 2.二叉树的性质
        • 3.二叉树与树、森林之间的转换
          • 1)森林、树转换为二叉树
          • 2)二叉树还原为树\森林
      • 三、二叉树的存储结构
        • 1.二叉树的顺序存储结构
        • 2.二叉树的链式存储结构
      • 四、二叉树的基本运算及其实现
        • 1.二叉树的基本运算的概述
        • 2.二叉树的基本运算算法的实现
          • 1)创建二叉树CreateBTree(*b,*str)
          • 2)销毁二叉树DestroyBTree(&b)
          • 3)查找结点FindNode(b,x)
          • 4)找孩子结点LchildNode(p )或RchildNode(p )
          • 5)求高度BTHeight(b)
          • 6)输出二叉树DispBTree(b)
      • 五、二叉树的遍历
        • 1.二叉树遍历的概念
        • 2.先序、中序和后序遍历递归算法
        • 3.先序、中序和后序遍历非递归算法
          • 1)先序遍历非递归算法
          • 2)中序遍历非递归算法
          • 3)后序遍历非递归算法
        • 4.层次遍历算法
      • 六、二叉树的构造
      • 七、线索二叉树
        • 1.线索二叉树的概念
        • 2.线索化二叉树
        • 3.遍历线索化二叉树
      • 八、哈夫曼树
        • 1.哈夫曼树概述
        • 2.哈夫曼树的构造算法
        • 3.哈夫曼编码
      • 九、用并查集求解等价问题
        • 1.并查集的定义
        • 2.并查集的算法实现
          • 1)并查集的初始化
          • 2)查找一个结点所属的子集树
          • 3)两个结点所属子集树的合并

一、树的基本概念

1.树的定义

树是由n(n≥0)个结点(或元素)组成的有限集合(记为T)。
如果n=0,它是一棵空树,这是树的特例;如果n>0,这个结点中有且仅有一个结点为树的根结点,简称为根,其余结点可分为m(m≥0)个互不相交的有限集T1
T2、…、Tm,其中每个子集本身又是一棵符合本定义的树,称为根结点的子树。这中包含唯一根结点的树也称为有根树。

树的抽象数据类型描述如下:

ADT Tree{
数据对象:
D={ai | 1≤i≤n,n≥0,a,为ElemType类型 }//ElemType是自定义类型标识符
数据关系:
R={<ai,aj> l ai,aj∈D,1≤i,j≤n,其中有且仅有一个结点没有前驱结点,其余每个结点只有一个前驱结点,但可以有零个或多个后继结点}
基本运算:
lnitTree(&t):初始化树,造一棵空树t。
DestroyTree(&t):销毁树,释放为树t分配的存储空间。
TreeHeight(t):求树的高度。
Parent(t,p):求树t中p所指结点的双亲结点。
Brother(t,p):求树t中p所指结点的所有兄弟结点。
Sons(t,p):求树t中所指结点的所有子孙结点。
……
}

2.树的逻辑表示方法
  • 树的几种常见逻辑表示方法
    (1)树形表示法
    (2)文氏图表示法
    (3)凹入表示法
    (4)括号表示法
3.树的基本术语
  • 结点的度:树中该结点的子树的个数。
    树的度:树中所有结点的度中的最大值,通常将度为m的树称为m次树

  • 分支结点(非终端结点):树中度不为零的结点;在分支结点中,每个结点的分支数就是该结点的度;如对于度为1的结点,其分支数为1.被称为单分支结点;对于度为2的结点,其分支数为2,被称为双分支结点
    叶子结点:度为零的结点

  • 路径:对于树中的任意两个结点ki和kj若树中存在一个结点序列(ki,ki1,ki2.…kin,kj),使得序列中除ki以外的任一结点都是其在序列中的前一个结点的后继结点,则称该结点序列为由ki到kj的一条路径。
    路径长度:该路径所通过的结点数目减1(即路径上的分支数目)

  • 孩子结点,双亲结点,兄弟结点,子孙结点,祖先结点:在一棵树中,每个结点的后继结点被称为该结点的孩子结点。相应地,该结点被称为孩子结点的双亲结点。具有同一双亲结点的孩子结点互为兄弟结点。进一步推广这些关系,可以把每个结点对应子树中的所有结点(除自身外)称为该结点的子孙结点,把从根结点到达某个结点的路径上经过的所有结点(除自身外)称为该结点的祖先结点。

  • 结点层次和树的高度:树中的每个结点都处在一定的层次上。结点层次或结点深度是从树根开始定义的,根结点为第一层,它的孩子结点为第二层,以此类推,一个结点所在的层次为其双亲结点的层次加1。树中结点的最大层次称为树的高度)或树的深度。

  • 有序树和无序树:若树中各结点的子树是按照一定的次序丛左向右安排的,且相对次序是不能随意变换的,则称为有序树,否则称为无序树。一般情况下,如果没有特别说明,默认树都是指有序树。

  • 森林:m(m≥0)棵互不相交的树的集合称为森林。把含有多棵子树的树的根结点删去就成了森林。反之,给m(m>1)棵独立的树加上一个根结点,并把这m棵树作为该结点的子树,则森林就变成了一棵树。

4.树的性质

性质1:树中的结点数等于所有结点的度数之和加1。
性质2:度为m的树中第i层上最多有mi-1个结点(i≥
1)。
性质3:高度为h的m次树最多有
(mh-1)/(m-1)
个结点
性质4:具有n个结点的m次树的最小高度为[logm(n(m-1)+1)]

5.树的基本运算
  • 树的运算主要分为以下三大类:
    (1)寻找满足某种特定条件的结点,例如寻找当前结点的双亲结点等。
    (2)插入或删除某个结点,例如在树的指定结点上插入一个孩子结点或删除指定结点的第i个孩子结点等。
    (3)遍历树中的所有结点。

  • 树的遍历运算是指按某种方式访问树中的所有结点且每一个结点只被访问一次。树的遍历方式主要有先根遍历、后根遍历和层次遍历3种。注意,树的先根遍历和后根遍历的过程都是递归的。
    1.先根遍历
    (1)访问根结点。
    (2)按照从左到右的顺序先根遍历根结点的每一棵子树。
    例如,对于下图所示的树,采用先根遍历得到的结点序列为ABEFCGJDHIKLM。
    2.后根遍历
    (1)按照从左到右的顺序后根遍历根结点的每一棵子树。
    (2)访问根结点。
    例如,对于下图所示的树,采用后根遍历得到的结点序列为EFBJGCHKLMIDA。
    3.层次遍历
    从根结点开始按从上到下、从左到右的次序访问树中的每一个结点。
    例如,对于下图所示的树,采用层次遍历得到的结点序列为ABCDEFGHUKLM。
    在这里插入图片描述

6.树的存储结构

存储树的基本要求是既要存储结点的数据元素本身,又要存储结点之间的逻辑关系。

1)双亲存储结构

双亲存储结构是一种顺序存储结构,用一组连续空间存储树的所有结点,同时在每个结点中附设一个伪指针指示其双亲结点的位置(因为除了根结点以外,每个结点只有唯一的双亲结点,将根结点的双亲结点的位置设置为特殊值一1)。
双亲存储结构的类型声明如下:

typedef struct{
    ElemType data; //存放结点的值
    int parent; //存放双亲的位置
}PTree[MaxSize]; //PTree 为双亲存储结构类型

该存储结构利用了每个结点(根结点除外)只有唯一双亲的性质,在这种存储结构中,求某个结点的双亲结点十分容易,但在求某个结点的孩子结点时需要遍历整个存储结构。

2)孩子链存储结构

在孩子链存储结构中,每个结点不仅包含结点值,还包含指向所有孩子结点的指针。由于树中每个结点的子树的个数(即结点的度)不同,如果按各个结点的度设计变长结构,则会因为结点的孩子结点的指针域的个数不同而导致算法的实现非常麻烦。孩子链存储结构可按树的度(即树中所有结点度的最大值)设计结点的孩子结点的指针域的个数。
孩子链存储结构的结点类型声明如下:

typedef struct node{
ElemType data; //结点的值
struct node *sons[MaxSons];//指向孩子结点
}TSonNode; //孩子链存储结构中的结点类型

孩子链存储结构的优点是查找某结点的孩子结点十分方便,其缺点是查找某结点的双亲结点需要遍历树,另外,当树的度较大时存在较多的空指针域。

3)孩子兄弟链存储结构

孩子兄弟链存储结构是为每个结点设计3个域,即一个数据元素域,一个指向该结点的左边第一个孩子结点(长子)的指针域、一个指向该结点的下一个兄弟结点的指针域。
兄弟链存储结构中结点的类型声明如下:

typedef struct tnode{
    ElemType data;     //结点的值
    struct tnode *hp;  //指向兄弟
    struct tnode *vp;  //指向孩子结点
}TSBNode               //孩子兄弟链存储结构的结点类型

由于树的孩子兄弟链存储结构固定有两个指针域,并且这两个指针是有序的(即兄弟域和孩子域不能混淆),所以孩子兄弟链存储结构实际上是把该树转换为二叉树的存储结构
孩子兄弟链存储结构的最大优点是可以方便地实现树和二叉树的相互转换。孩子兄弟链存储结构的缺点和孩子链存储结构的缺点一样,就是查找一个结点的双亲结点需要遍历树。

二、二叉树的概念和性质

1.二叉树的定义
  • 二叉树是一个有限的结点集合,这个集合或者为空,或者由一个根结点和两棵互不相交的称为左子树和右子树的二叉树组成。
  • 二叉树和度为2的树(2次树)是不同的,对于非空树,其差别表现在以下两点:
    (1)度为2的树中至少有一个结点的度为 2,而二叉树没有这种要求。
    (2)度为2的树不区分左、右子树,而二叉树是严格区分左、右子树的
  • 二叉树有 5种基本形态,如下图所示,任何复杂的二叉树都可以看成这5种基本形态的复合。
    在这里插入图片描述
  • 满二叉树:在一棵二叉树中,所有分支点都有左右孩子结点,并且叶子结点都集中在二叉树的最下一层。可以对满二叉树的结点进行层序编号,约定编号从树根为1开始,按照层数从小到大、同一层从左到右的次序进行,图中每个结点外边的数字为对该结点的编号。当然也可以从结点个数和树高度之间的关系来定义,即一棵高度为h 且有 2h-1个结点的二叉树称为满二叉树。
    在这里插入图片描述非空满二叉树的特点如下:
    (1)叶子结点都在最下一层。
    (2) 只有度为0和度为 2的结点
  • 完全二叉树: 若二叉树中最多只有最下面两层的结点的度数可以小于 2,并且最下面一层的叶子结点都依次排列在该层最左边的位置上。同样可以对完全二叉树中的每个结点进行层序编号,编号的方法和满二叉树相同.图中每个结点外边的数字为对该结点的编号。
    在这里插入图片描述
    满二叉树是完全二叉树的一种特例,并且完全二叉树与同高度的满二叉树的对应位置结点的编号相同。图 中所示的完全二叉树与等高度的满二叉树相比在最后一层的右边缺少了 4 个结点。
    非空完全二叉树的特点如下
    (1)叶子结点只可能在最下面两层中出现
    (2)对于最大层次中的叶子结点,都依次排列在该层最左边的位置上
    (3)如果有度为1的结点,只可能有一个,且该结点只有左孩子而无右孩子。
    (4) 在按层序编号时,一旦出现编号为 i的结点是叶子结点或只有左孩子,则编号大于i的结点均为叶子结点
    (5)当结点总数 n 为奇数时,n,=0,当结点总数 n 为偶数时,n=1。
2.二叉树的性质
  • 性质1:非空二叉树上的叶子结点数等于双分支结点数加1.
  • 性质 2:非空二叉树的第i层上最多有 2i-1 个结点(i≥1)
  • 性质3:高度为h的二叉树最多有 2h-1个结点(h≥1)。
  • 性质 4:完全二叉树中层序编号为i的结点(1 ≤i≤n,n≥1,n 为结点数)有以下性质
    (1)若i≤n/2,即 2i≤n,则编号为i的结点为分支结点,否则为叶子结点。
    (2)若n为奇数,则每个分支结点都既有左孩子结点,又有右孩子结点; 若 n为偶数,则编号最大的分支结点(编号为n/2)只有左孩子结点,没有右孩子结点,其余分支结点都有左、右孩子结点。
    (3)若编号为i的结点有左孩子结点,则左孩子结点的编号为 2i; 若编号为i的结点有右孩子结点,则右孩子结点的编号为 2i+1。
    (4)除根结点以外,若一个结点的编号为 i,则它的双亲结点的编号为i/2。
  • 性质 5:具有n个(n>0)结点的完全二叉树的高度为log2(n+1)或log2n+1。

说明:对于一棵完全二叉树,结点总数n 可以确定其形态,n1只能是0或1。当n 为偶数时,n1=1;当n为奇数时,n1=0。

3.二叉树与树、森林之间的转换

树、森林与二叉树之间是一一对应的,可以把在树中处理的问题对应到二叉树中进行处理,从而把问题简单化。

1)森林、树转换为二叉树
  • 将一棵树转换成二叉树的过程如下:
    (1)树中所有相邻兄弟之间加一条连线。
    (2)对树中的每个结点只保留它与长子(即最左边的孩子结点之间的连线,删除与其他孩子之间的连线。
    (3)以树的根结点为轴心,将整棵树顺时针转动 45°,使之结构层次分明
  • 若要转换为二叉树的森林由两棵或两棵以上的树构成,将这样的森林转换为二叉树的过程如下:
    (1)将森林中的每棵树转换成相应的二叉树
    (2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子结点,当所有二叉树连在一起后,此时得到的二叉树就是由森林转换得到的二叉树。
    实际上,当森林F由两棵或两棵以上的树{T1,T2,.,Tn}构成时,所有这些树的根结点构成兄弟关系,所以森林F转换成一棵二叉树 BT后,将第一棵树 T1的根结点作为 BT的根结点t1,T2的根结点作为t1的右孩子结点 t2,T3的根结点作为t2的右孩子结点 t3以此类推。
2)二叉树还原为树\森林
  • 若一棵二叉树是由一棵树转换而来的,则该二叉树还原为树的过程如下.
    (1)若某结点是其双亲的左孩子,则把该结点的右孩子、右孩子的右孩子等都与该结点的双亲结点用连线连起来
    (2)删除原二叉树中所有双亲结点与右孩子结点之间的连线。
    (3)整理由前面两步得到的树,即以根结点为轴心,逆时针转动45°,使之结构层次分明
    实际上,二叉树的还原就是将二叉树中的左分支保持不变,将二叉树中的右分支还原成兄弟关系。
  • 若一棵二叉树是由 m 棵树构成的森林转换而来的,该二叉树的根结点一定有 m-1个右下孩子,该二叉树还原为森林的过程如下:
    (1)抹掉二叉树根结点右链上的所有结点之间的“双亲一右孩子”关系,将其分成若干个以右链上的结点为根结点的二叉树,设这些二叉树为 bt1,bt2,……,btm
    (2)分别将二叉树 bt1,bt2,……,btm各自还原成一棵树

三、二叉树的存储结构

1.二叉树的顺序存储结构

二叉树的顺序存储结构就是用一组地址连续的存储单元来存放二叉树的数据元素,因此必须确定好树中各数据元素的存放次序,使得各数据元素在这个存放次序中的相互位置能反映出数据元素之间的逻辑关系。
对于完全二叉树和满二叉树,树中结点的层序编号可以唯一地反映出结点之间的逻辑关系,所以可以用一维数组按从上到下、从左到右的顺序存储树中的所有结点值,通过数组元素的下标关系反映完全二叉树或满二叉树中结点之间的逻辑关系。
例如,图(b)所示的完全二叉树对应的顺序存储结构如图下下图 所示,编号为i的结点值存放在数组下标为i的元素中('#‘表示空结点)。由于 C/C++语言中的数组下标从 0开始,这里为了一致性而没有使用下标为 0的数组元素。
在这里插入图片描述
在这里插入图片描述
然而对于一般的二叉树,如果仍按照从上到下和从左到右的顺序将树中的结点顺序存储在一维数组中,则数组元素下标之间的关系不能够反映二叉树中结点之间的逻辑关系,这时可将一般二叉树进行改造,增添一些并不存在的空结点,使之成为一棵完全二叉树的形式, 再对所有结点按层序编号,然后仅保留实际存在的结点,接着把各结点值按编号存储到一维数组中,在二叉树中人为增添的结点(空结点)在数组中所对应的元素值为一特殊值,例如’#'字符。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
对于一个编号(下标)为i的结点,如果有双亲,其双亲结点的编号(下标)为i/2;如果它有左孩子,其左孩子结点的编号(下标)为 2i;如果它有右孩子,其右孩子结点的编号(下标)为 2i+1。

  • 二叉树顺序存储结构的类型声明如下.
typedef ElemType SqBinTree[MaxSize] ;

其中,ElemType 为二叉树中结点的数据值类型,MaxSize 为顺序表的最大长度。为了方便运算,通常将下标为0的位置空着,空结点用’#'值表示。
显然,完全二叉树或满二叉树采用顺序存储结构比较合适,既能够最大可能地节省存储空间,又可以利用数组元素的下标确定结点在二叉树中的位置以及结点之间的关系。对于一般二叉树,如果它接近于完全二叉树形态,需要增加的空结点个数不多,也可以采用顺序存储结构。如果需要增加很多空结点才能将一棵二叉树改造成一棵完全二叉树,采用顺序存储结构会造成空间的大量浪费。最坏情况是右单支树(除叶子结点外每个结点只有一个右孩子),一棵高度为h 的右单支树只有h 个结点,却需要分配 2h-1个元素空间。在顺序存储结构中,查找一个结点的孩子、双亲结点都很方便,编号(下标)为的结点的层次为log2(i+1)。

2.二叉树的链式存储结构

二叉树的链式存储结构是指用一个链表来存储一棵二叉树,二叉树中的每一个结点用链表中的一个结点来存储。二叉树链式存储结构中结点的标准存储结构如下:
在这里插入图片描述
其中,data 表示值域,用于存储对应的数据元素,lchild 和 rchild 分别表示左指针域和右指针域,分别用于存储左孩子结点和右孩子结点的存储地址。这种链式存储结构通常简称为二叉链。在二叉链中通过根结点指针b来唯一标识整个存储结构,称为二叉树b。
二叉链中结点类型 BTNode的声明如下

typedef struct node{
    ElemType data;        //数据元素
    struct node *lchild;  //指向左孩子结点
    struct node * rchild; //指向右孩子结点
}BTNode;

二叉链存储结构的优点是对于一般的二叉树比较节省存储空间,在二叉链中访问一个结点的孩子很方便,但访问一个结点的双亲结点需要遍历树。有时为了高效地访问一个结点的双亲结点,可在每个结点中再增加一个指向双亲的指针域 parent,这样就构成了二叉树的3叉链表,其结点结构如下图所示。
二叉树的3叉链表结点结构

四、二叉树的基本运算及其实现

1.二叉树的基本运算的概述

CreateBTree(b,str):创建二叉树,根据二叉树的括号表示法字符串str生成对应的二叉链存储结构,b为创建的二叉链的根结点指针。
DestroyBTree(&b):销毁二叉树,释放二叉树b中所有结点的分配空间。
FindNode(b,x):查找结点,在二叉树b中查找data域值为x的结点,并返回指向该结点的指针。
LchildNode§和RchildNode§:找孩子结点,分别求二叉树中p所指结点的左孩子结点和右孩子结点。
BTHeight(b):求二叉树b的高度。
DispBTree(b):以括号表示法输出一棵二叉树b。

2.二叉树的基本运算算法的实现
1)创建二叉树CreateBTree(*b,*str)

假设采用括号表示法表示的二叉树字符串str是正确的,用ch遍历str,其中只有4类字符,其处理方式如下。

  • 若ch=‘(’,表示前面刚创建的结点p存在孩子结点,需要将其进栈,以便建立它和它的孩子结点之间的关系(如果一个结点刚创建完毕,其后一个字符不是’(’,表示该结点是叶子结点,不需要进栈)。然后开始处理该结点的左孩子,置k=1(表示其后创建的结点将作为当前栈顶结点的左孩子结点)。
  • 若ch=‘)’,表示以栈顶结点为根结点的子树创建完毕,将其退栈。
  • 若ch=',’,表示开始处理栈顶结点的右孩子结点,置k=2(表示其后创建的结点将作为当前栈顶结点的右孩子结点)。
  • 其他情况:只能是单个字符,对应二叉树中的某个结点值,需要创建一个结点p存
    放该结点值。根据k值建立它与栈顶结点之间的联系。当k=1时,将结点p作为栈顶结点的左孩子;当k=2时,将结点p作为栈顶结点的右孩子。
    如此循环,直到str遍历完毕。在算法中使用一个栈保存双亲结点,为了简单用数组St表示栈,top为栈顶指针,k指定其后处理的结点是双亲结点(栈顶结点)的左孩子(k=1)还是右孩子(k=2)。
#include "btree.h" 											//包含二叉树的存储结构声明
void CreateBTree(BTNode *&b,char *str){
	BTNode *St[MaxSize],*p;									//St数组作为顺序栈
	int top=-1.k,j=0; 										//top 为栈顶指针
	char ch;
	b=NULL; 												//初始时二叉链为空
	ch=str;
	while(ch!='\0'){										//遍历str中的每个字符
		switch(ch)
		case'(':top++;St[top]=p;k=1;break; 					//开始处理左孩子结点
		case')':top--;break;								//栈顶结点的子树处理完毕
		case',':k=2;break; 									//开始处理右孩子结点
		default:p=(BTNode *)malloc(sizeof(BTNode)); 		//创建一个结点,由p指向它
				p->data=ch; 								//存放结点值
				p->lchild=p->rchild=NULL;	 				//左、右指针都设置为空
				if(b==NULL) 								//若尚未建立根结点
					b=p; 									//p所指结点作为根结点
				else{										//已建立二叉树根结点
				 	switch(k){
						case 1:St[top]->lchild=p;break; 	//新建结点作为栈顶结点的左孩子
						case 2:St[top]->rchild=p;break; 	//新建结点作为栈顶结点的右孩子
					}
				}
		}
		j++; 												//继续遍历str
		ch=str[j];
	}
}
2)销毁二叉树DestroyBTree(&b)
void DestroyBTree(BTNode *&b){	//销毁二叉树
	if (b!=NULL){
		DestroyBTree(b->lchild);
		DestroyBTree(b->rchild);
		free(b);
	}
}
3)查找结点FindNode(b,x)
BTNode *FindNode(BTNode *b,ElemType x) {//查找值为x的结点
	BTNode *p;
	if (b==NULL)
		return NULL;
	else if (b->data==x)
		return b;
	else {
		p=FindNode(b->lchild,x);
		if (p!=NULL) 
			return p;
		else 
			return FindNode(b->rchild,x);
	}
}
4)找孩子结点LchildNode(p )或RchildNode(p )
BTNode *LchildNode(BTNode *p){
    return p->lchild;
}
BTNode *RchildNode(BTNode *p){
    return p->rchild;
}
5)求高度BTHeight(b)


int BTHeight(BTNode *b){		//求二叉树b的高度
   	int lchildh,rchildh;
   	if (b==NULL) 				//空树的高度为0
   	    return(0); 
   	else {
		lchildh=BTHeight(b->lchild);	//求左子树的高度为lchildh
		rchildh=BTHeight(b->rchild);	//求右子树的高度为rchildh
		return (lchildh>rchildh)? (lchildh+1):(rchildh+1);
   	}
}
6)输出二叉树DispBTree(b)
void DispBTree(BTNode *b){  //以括号表示法输出二叉树
	if (b!=NULL){
		printf("%c",b->data);
		if (b->lchild!=NULL || b->rchild!=NULL){
			printf("(");						//有孩子结点时才输出(
			DispBTree(b->lchild);				//递归处理左子树
			if (b->rchild!=NULL) printf(",");	//有右孩子结点时才输出,
			DispBTree(b->rchild);				//递归处理右子树
			printf(")");						//有孩子结点时才输出)
		}
	}
}

五、二叉树的遍历

1.二叉树遍历的概念

二叉树遍历是指按照一定的次序访问二叉树中的所有结点,并且每个结点仅被访问一次的过程。它是二叉树最基本的运算,是二叉树中所有其他运算实现的基础。
一棵二叉树由3个部分(即根结点、左子树和右子树)构成,可以从任何部分开始遍历,所以有3!(即6)种遍历方法,若规定子树的遍历总是先左后右(先右后左与之对称),则对于非空二叉树,可得到3种递归的遍历方法,即先序遍历、中序遍历和后序遍历。另外还有一种常见的层次遍历方法。
在这里插入图片描述

  1. 先序遍历
    先序遍历二叉树的过程如下:
    (1)访问根结点。
    (2)先序遍历左子树。
    (3)先序遍历右子树。
    例如,如图所示的二叉树的先序序列为ABDGCEF。
  2. 中序遍历
    中序遍历二叉树的过程如下:
    (1)中序遍历左子树。
    (2)访问根结点。
    (3)中序遍历右子树。
    例如,如图所示的二叉树的中序序列为DGBAECF。
  3. 后序遍历
    后序遍历二叉树的过程如下:
    (1)后序遍历左子树。
    (2)后序遍历右子树。
    (3)访问根结点。
    例如,如图所示的二叉树的后序序列为GDBEFCA。
  4. 层次遍历
    层次遍历不同于前面3种遍历方法,它是非递归定义的,用于一层一层地访问二叉树中的所有结点。其过程如下:
    若二叉树非空(假设其高度为h),则:
    (1)访问根结点(第1层)。
    (2)从左到右访问第2层的所有结点。
    (3)从左到右访问第3层的所有结点、…、第h层的所有结点。
    例如,如图所示的二叉树的层次遍历序列为ABCDEFG。
2.先序、中序和后序遍历递归算法
void PreOrder(BTNode *b){
	if(b!=NULL){ //先序遍历递归算法
		printf("%c",b->data); //访问根结点
		PreOrder(b->lchild); //先序遍历左子树
		PreOrder(b->rchild); //先序遍历右子树
	}
}
void InOrder(BTNode *b){ //中序遍历递归算法
	if (b!=NULL){
		InOrder(b->lchild); //中序遍历左子树
		printf("%c",b->data); //访问根结点
		InOrder(b->rchild); //中序遍历右子树
	}
}
void PostOrder(BTNode *b){ //后序遍历递归算法
	if(b=NULL){
		PostOrder(b->lchild); //后序遍历左子树
		PostOrder(b->rchild); //后序遍历右子树
		printf("%c",b->data); //访问根结点
	}
}
3.先序、中序和后序遍历非递归算法
1)先序遍历非递归算法

先序遍历非递归算法主要有两种设计方法。
1)先序遍历非递归算法1
由先序遍历过程可知,先访问根结点,再遍历左子树,最后遍历右子树。由于在二叉链中左、右子树是通过根结点的指针域指向的,在访问根结点后遍历左子树时会丢失右子树的地址,需要使用一个栈来临时保存左、右子树的地址。由于栈的特点是先进后出,而先序遍历是先遍历左子树,再遍历右子树,所以当访问完一个非叶子结点后应先将其右孩子进栈,再将其左孩子进栈.

void PreOrderl(BTNode *b){ 				//先序遍历非递归算法1
	BTNode *p;
	SqStack st; 						//定义栈指针 st
	InitStack(st);						//初始化栈 st
	if(b!=NULL){
		Push(st,b); 					//根结点进栈
		while(!StackEmpty(st)){			//栈不为空时循环
			Pop(st,p)//退栈结点p并访问它
			printf("%c",p->data);
			if(p->rchild!=NULL)			//有右孩子时将其进栈
				Push(st,p->rchild);
			if(p->lchild!=NULL) 		//有左孩子时将其进栈
				Push(st,p->lchild);
		}
		printf("\n");
	}
	DestroyStack(st);	 				//销毁栈
}

2)先序遍历非递归算法2
先序遍历的顺序是根结点、左子树和右子树,所以先访问根结点b及其所有左下结点、由于在二叉链中无法由孩子找到其双亲,所以需要将这些访问过的结点进栈保存起来。此时当前栈顶结点要么没有左子树(实际上是没有左孩子),要么左子树已遍历过,所以在栈不空时出栈结点p并转向它的右子树,对右子树的处理与上述过程类似。

void PreOrder2(BTNode b){ 				// 先序遍历非递归算法 2
	BTNode *p;
	SqStack *st; 						//定义一个顺序栈指针 st
	InitStack(st);						//初始化栈 st
	p=b; 
	while(!StackEmpty(st)|| p!=NULL){
		while(p!=NULL){ 				//访问结点p及其所有左下结点并进栈
	 		printf("%c",p->data); 		//访问结点p
			Push(st,p); 				//结点p进栈
			p=p->lchild; 				//移动到左孩子
		}
		if (!StackEmpty(st)){ 			//若栈不空
			Pop(st,p); 					//出栈结点p
			p=p->rchild; 				//转向处理其右子树
		}
	printf("\n");
	DestroyStack(st); 					//销毁栈
}
2)中序遍历非递归算法

中序遍历非递归算法是在前面先序遍历非递归算法2的基础上修改的,中序遍历的顺序是左子树、根结点、右子树,所以需要将根结点及其左下结点依次进栈,但还不能访问,因为它们的左子树没有遍历。当到达根结点的最左下结点时,它是中序序列的开始结点,也是栈顶结点,出栈并访问它,然后转向它的右子树,对右子树的处理与上述过程类似。

void InOrderl(BTNode b){ //中序遍历非递归算法
	BTNode *p;
	SqStack *st; //定义一个顺序栈指针st
	InitStack(st); //初始化栈st
	p=b;
	while(!StackEmpty(st)||p!=NULL){
		while(p!=NULL){ //找结点p的所有左下结点并进栈
			Push(st,p); //结点p进栈
			p=p->lchild; //移动到左孩子
		}
		if(!StackEmpty(st)){ //若栈不空
			Pop(st,p); //出栈结点p
			printf("%c",p->data); //访问结点p
			p=p-> rchild; //转向处理其右子树
		}
	}
	printf("\n");
	DestroyStack(st);
}

3)后序遍历非递归算法

后序遍历非递归算法是在前面中序遍历非递归算法的基础上修改的,后序遍历的顺序是左子树、右子树、根结点,所以先将根结点及其左下结点依次进栈,即使栈顶结点p的左子树已遍历或为空,仍不能访问结点p,因为它们的右子树没有遍历,只有当这样的p结点的右子树已遍历完才能访问结点p。
需要进一步解决以下两个问题:
一是如何判断当前处理的结点p是栈顶结点,这比较简单,设置一个布尔变量flag,在do-while 循环中的第一个while循环结束后开始处理栈顶结点,置flag为true;一旦转向处理右子树,置flag为false。
二是如何判断结点p的右子树已遍历过,这是算法的主要难点。在一棵二叉树中,任何一棵非空子树的后序遍历序列中最后访问的一定是该子树的根结点,也就是说,若结点p的右孩子刚访问过,说明它的右子树已遍历完,可以访问结点p了。当然,若结点p的右孩子为空,也可以访问结点p。为此设置一个指针变量r,它指向刚访问过的结点,其初始值为NULL。对于正在处理的栈顶结点p,一旦p->rchild==r成立,说明结点p的左、右子树都遍历过了,可以访问结点p。
不同于中序遍历非递归算法,这里的第二个阶段可能重复执行多次,当访问栈顶结点p之后,将其出栈,需要对新栈顶结点做同样的处理,直到p转向一棵右子树为止。

void PostOrderl(BTNode * b){ 			//后序遍历非递归算法
	BTNode p,*r;	
	bool flag;
	SqStack * st; 						//定义一个顺序栈指针st
	InitStack(st); 						//初始化栈 st
	p=b;
	do{									//栈结点p的所有左下结点并进栈
		while(p!=NULL){
			Push(st,p);					//结点p 进栈
			p=p->lchild; 				//移动到左孩子
		}
		r=NULL; 						//r指向刚访问的结点,初始时为空
		flag=true; 						//flag为真表示正在处理栈顶结点
		while(!StackEmpty(st) && flag){
			GetTop(st,p); 				//取出当前的栈顶结点p
			if(p->rchild==r){ 			//若结点p的右孩子为空或者为刚访问过的结点
				printf("%c",p->data); 	//访问结点p
				Pop(st,p);
				r=p; 					//r指向刚访问过的结点
			}
			else{
				p=p->rchild; 			//转向处理其右子树
				flag=false; 			//表示当前不是处理栈顶结点
			}
		}
	}while(!StackEmpty(st)); 			//栈不空时循环
	printf("\n");
	DestroyStack(st); 					//销毁栈
}

4.层次遍历算法

一棵二叉树的层次遍历就是按层次从上到下、每一层从左到右的顺序访问树中的全部结点。某一层中先访问的结点在下一层中它的孩子也先访问,这样与队列的特征相吻合。因此层次遍历算法采用一个环形队列qu来实现。
层次遍历过程是先将根结点进队,在队不空时循环:出队一个结点p并访问它,若它有左孩子,将左孩子结点进队;若它有右孩子,将右孩子结点进队。如此操作直到队空为止,该过程称为基本层次遍历过程。
对应算法如下:

void LevelOrder(BTNode *b){//基本层次遍历算法
	BTNode *p; 
	SqQueue *qu;//定义环形队列指针
	InitQueue(qu); //初始化队列
	enQueue(qu,b); //根结点进队
	while(!QueueEmpty(qu)){//队不空时循环
		deQueue(qu,p);//出队结点 p
		printf("%c",p->data); //访问结点 p
		if(p->lchild!=NULL) //有左孩子时将其进队
			enQueue(qu,p->lchild);
		if(p->rchild!=NULL) //有右孩子时将其进队
			enQueue(qu,p->rchild);
	}
	DestroyQueue(qu); //销毁队列
}

在前面的基本层次遍历中结点是一层一层地访问的,但无法判断某一层的结点何时访问完毕,可以通过队列状态来判断。首先将根结点进队,在队不空时循环:此时队列元素个数cnt表示当前层的结点个数,做cnt次这样的操作,出队一个结点p并访问它,若它有左孩子,将左孩子结点进队,若它有右孩子,将右孩子结点进队,cnt次操作后表示当前层次的结点访问完毕,此时队列中恰好包含下一层的全部结点,依次处理直到队列为空。该过程称为分层次的层次遍历过程,对应的算法如下:

void LevelOrderl(BTNode b){ //分层次的层次遍历算法
	BTNode *p;
	SqQueue *qu;
	InitQueue(qu); //初始化队列
	int curl=1; //表示当前层次(初始化为1)
	enQueue(qu,b); //根结点指针进入队列
	while(!QueueEmpty(qu)){ //队不空时循环
		printf("第%d层:",curl);
		int cnt=Count(qu); //求当前层次的结点个数cnt
		for(int i=0;i<cnt;i++){ //循环cnt次访问当前层的全部结点
			deQueue(qu,p); //出队结点p
			printf("%c",p->data); //访问结点 p
			if (p->lchild!=NULL) //有左孩子时将其进队
				enQueue(qu,p->lchild);
			if(p->rchild!=NULL) //有右孩子时将其进队
				enQueue(qu,p->rchild);
		}
		curl++//当前层访问完毕,进入下一层处理
		printf("\n");
	}
	DestroyQueue(qu); //销毁队列
}

六、二叉树的构造

定理1:任何n(n≥0)个不同结点的二叉树,都可由它的中序序列和先序序列唯一地确定。

//构造二叉树的算法
BTNode CreateBT1(char * pre,char *in,int n){
//pre存放先序序列,in存放中序序列.n为二叉树的结点个数,本算法执行后返回构造的二叉链的根结点指针 b
	BTNode *b;
	char *P;
	int k;
	if(n<=0) return NULL;
	b=(BTNode *)malloc(sizeof(BTNode);   	//创建二叉树结点b
	b->data=*pre;
	for(p=in;p<in+n;p++) 					//在中序序列中找等于*pre字符的位置k
		if(*p==*pre)						//pre指向根结点
			break; 							//在in中找到后退出循环
	k=p-in; 								//确定根结点在in中的位置
	b->lchild=CreateBT1(pre+1,in,k); 		//递归构造左子树
	b->rchild=CreateBT1(pre+k+1,p+1,n-k-1);	//递归构造右子树
	return b; 
}

定理2:任何n(n≥0)个不同结点的二叉树,都可由它的中序序列和后序序列唯一地确定。

//构造二叉树的算法
BTNode CreateBT2(char post,char in,int n){
//post存放后序序列,in存放中序序列,n为二叉树的结点个数,本算法执行后返回构造的二叉链的根结点指针b
	BTNode *b;
	char r,*p;
	int k;
	if(n<=0) return NULL;
	r=*(post+n-1); 							//post中最后元素是根结点值
	b=(BTNode *)malloc(sizeof(BTNode)); 	//创建二叉树结点b
	b-> data=r;
	for(p=in;p<in+n;p++) 					//在in中查找根结点
		if(*p==r) break;
	k=p-in; 								//k为根结点在in中的下标
	b->lchild=CreateBT2(post,in,k); 		//递归构造左子树
	b->rchild=CreateBT2(post+kp+l,n-k-1); 	//递归构造右子树
	return b;
}

七、线索二叉树

1.线索二叉树的概念

对于具有n个结点的二叉树,当采用二叉链存储结构时,每个结点有两个指针域,总共有 2n 个指针域,又由于只有 n-1个结点被有效指针域所指向(n 个结点中只有根结点没有被有效指针域指向),则共有 2n-(n-1)=n+1个空链域。
遍历二叉树的结果是一个结点的线性序列,可以利用这些空链域存放指向结点的前驱结点和后继结点的地址。其规定是当某结点的左指针为空时,令该指针指向这个线性序列中该结点的前驱结点;当某结点的右指针为空时,令该指针指向这个线性序列中该结点的后继结点,这样的指向该线性序列中“前驱结点”和“后继结点”的指针称为线索
创建线索的过程称为线索化。线索化的二叉树称为线索二叉树。
由于遍历方式不同,产生的遍历线性序列也不同,会得到相应的线索二叉树,一般有先序线索二叉树、中序线索二叉树和后序线索二叉树。创建线索二叉树的目的是提高该遍历过程的效率。

为了能区分左指针指向的是左孩子结点还是前驱结点,右指针指向的是右孩子结点还是后继结点,在结点的存储结构上增加两个标志位来区分这两种情况:

在这里插入图片描述
存储结构如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.线索化二叉树
typedef struct node{
	ElemType data;			//结点数据域
	int ltag ,rtag;			//增加的线索标记
	struct node *lchild;	//左孩子或线索指针
	struct node * rchild;	//右孩子或线索指针
}TBTNode;					//线索二叉树中的结点类型

下面以中序线索二叉树为例讨论建立线索二叉树的算法:

  • CreateThread(b)算法的功能是将以二叉链存储的二叉树 b 进行中序线索化,并返回线索化后头结点的指针 root。Thread§算法的功能是对以结点p为根的二叉树进行中序线索化。在整个算法中p指向当前被线索化的结点,而 pre 作为全局变量,总是指向刚访问过的结点,结点 pre 是结点 p的前驱结点,结点p是结点 pre 的后继结点’
  • CreateThread(b)算法的思路是先创建头结点 root,其 lchild 域为链指针,rchild 域为线索。将Ichild 指针指向根结点b,如果b为空,则将其 lchild 指向自身,否则将 root 的 lchild指向结点b,首先p 指向结点b,pre 指向头结点 root。再调用 Thread(b)对整个二叉树线索化,最后加入指向头结点的线索,并将头结点的 rchild 指针域线索化为指向最后一个结点(由于线索化直到p等于NULL 为止,所以最后访问的是结点 pre)。
  • Thread(p )算法类似于中序遍历的递归算法。在中序遍历中,指向当前访问的结点,pre指向中序遍历的前一个结点(初始时,pre指向中序线索二叉树的头结点 root)。若结点p原来左指针为空,改为指向结点 pre的左线索,若结点 pre 原来右指针为空,改为指向结点p的右线索,如下图所示
    将空指针改为线索的过程
TBTNode *pre;					//全局变量
void Thread(TBTNode *&p){		//对二叉树 p进行中序线索化
	if(p!=NULL){
		Thread(p->lchild);		//左子树线索化
		if(p->lchild==NULL){	//左孩子不存在进行前驱结点线索化
			p->lchild=pre;		//建立当前结点的前驱结点线索
			p->ltag=1;
		}
		else					//p结点的左子树已线索化
			p->ltag=0;
		if (pre->rchild==NULL){
			pre->rchild=p;		//对pre的后继结点线索化
			pre-> rtag=l;		//建立前驱结点的后继结点线索
		}
		else
			pre->rtag=0;
		pre=p;
		Thread(p-> rchild);		//右子树线索化
	}
}
TBTNode *CreateThread(TBTNode*b){//中序线索化二叉树
	TBTNode*root;
	root=(TBTNode * )malloc(sizeof(TBTNode));//创建头结点
	root-> ltag=0;
	root-> rtag=1;
	root-> rchild=b;
	if (b==NULL)				//空二叉树
		root->lchild=root;
	else{
		root->lchild=b;
		pre=root;				//pre是结点p的前驱结点,供加线索用
		Thread(b);				//中序遍历线索化二叉树
		pre-> rchild=root;		//最后处理加入指向头结点的线索
		pre-> rtag=1;
		root-> rchild=pre;		//头结点右线索化
	}
	return root;
}
3.遍历线索化二叉树

遍历某种次序的线索二叉树就是从该次序下的开始结点出发,反复找到该结点在该次序下的后继结点,直到头结点。
对应算法如下:

void ThInOrder(TBTNode * tb){				//tb指向中序线索二叉树的头结点
	TBTNode *p=tb->lchild;					//p指向根结点
	while(p!-tb){
		while(p->ltag==0)					//找开始结点
			p=p->lchild;					
		printf("%c",p-> data);				//访问开始结点
		while(p->rtag==1&&p->rchild!=tb){
			p=p->rchild:
			printf("%c",p-> data);
		}
		p=p->rchild;
	}
}

八、哈夫曼树

1.哈夫曼树概述

权:在许多应用中经常将树中的结点赋予一个有某种意义的数值,称此数值为该结点的权。
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点上权的乘积。
树的带权路径长度:树中所有叶子结点的带权路径长度之和称为该树的带权路径长度,通常记为:
在这里插入图片描述

其中,n0表示叶子结点的个数,wi和li分别表示第i个叶子结点的权值和根到它之间的路径长度(即从根结点到该叶子结点的路径上经过的分支数)。
哈夫曼树或最优二叉树:在n0个带权叶子结点构成的所有二叉树中,带权路径长度最小的二叉树
佛定。个权前,细何构造一棵含有 个有给定权值的叶子结点的二爱树、使基带安带校长座服小呢哈关业早概待有节有能规的算法,称为哈夫算,哈大

2.哈夫曼树的构造算法

给定n0个权值,如何构造一棵含有n0个带有给定权值的叶子结点的二叉树,使其带权路径长度最小呢?哈夫曼最早给出了一个带有一般规律的算法,称为哈夫曼算法。哈夫曼算法如下:
(1)根据给定的n0个权值(w1,w2,…,wn),对应结点构成n0棵二叉树的森林F=(T1,T2,…,Tn0),其中每棵二叉树Ti,(1<=i<=n0)中都有一个权值为wi,的根结点,其左右子树均为空。
(2)在森林F中选取两棵结点的权值最小的子树分别作为左、右子树构造一棵新的叉树,并且置新的二叉树的根结点的权值为其左、右子树上根的权值之和。
(3)在森林F中,用新得到的二叉树代替这两棵树。
(4)重复(2)和(3),直到F中只含一棵树为止。这棵树便是哈夫曼树

  • 定理3:对于具有n0个叶子结点的哈夫曼树,共有2n0-1个结点。
  • 设计哈夫曼树的结点类型如下:
typedef struct{
	char data;	    //结点值
	double weight;  //权重
	int parent;		//双亲结点
	int lchild;		//左孩子结点
	int rchild;		//右孩子结点
} HTNode;
  • 构造哈夫曼树的算法如下:
void CreateHT(HTNode ht[],int n){  //由ht的叶子结点构造完整的哈夫曼树
	int i,k,lnode,rnode;
	double min1,min2;
	for (i=0;i<2*n-1;i++)			//所有结点的相关域置初值-1
		ht[i].parent=ht[i].lchild=ht[i].rchild=-1;
	for (i=n;i<2*n-1;i++){			//构造哈夫曼树的分支结点
		min1=min2=32767;			//lnode和rnode指向权重最小的两个结点
		lnode=rnode=-1;
		for (k=0;k<=i-1;k++){		//查找最小和次小的结点
			if (ht[k].parent==-1){	//只在尚未构造二叉树的结点中查找
				if (ht[k].weight<min1){
					min2=min1;
					rnode=lnode;
					min1=ht[k].weight;
					lnode=k;
				}
				else if (ht[k].weight<min2){
					min2=ht[k].weight;
					rnode=k;
				}
			}
		}
		ht[lnode].parent=i;
		ht[rnode].parent=i;	//合并两个最小和次小的结点
		ht[i].weight=ht[lnode].weight+ht[rnode].weight;
		ht[i].lchild=lnode;
		ht[i].rchild=rnode;
	}
}
3.哈夫曼编码

在数据通信中,经常需要将传送的文字转换为由二进制字符0和1组成的二进制字符串,称这个过程为编码。
哈夫曼树可用于构造使电文编码的代码长度最短的编码方案。
具体构造方法如下,设需要编码的字符集合为{d1,d2,…,dn0},各个字符在电文中出现的次数集合为{w1,w2,…,wn0},以d1,d2,…,dn0作为叶子结点,以w1,w2,…,wn0作为各根结点到每个叶子结点的权值构造一棵哈夫曼树,规定哈夫曼树中的左分支为0.右为1,则从根结点到每个叶子结点所经过的分支对应的0和1组成的序列便是该结点对应字符的编码,这样的编码称为哈夫曼编码。
哈夫曼编码的实质就是使用频率越高的字符采用越短的编码。
为了实现构造哈夫曼编码的算法,设计存放每个结点的哈夫曼编码的类型如下,

typedef struct{ 	
	char cd[N];		//存放当前结点的哈夫曼编码
	int start;		//表示cd[start..n0]部分是哈夫曼编码
}HCode; HCode类型变量

由于哈夫曼树中每个叶子结点的哈夫曼编码长度不同,为此采用HCode类型变量的
cd[start…n0]存放当前结点的哈夫曼编码,只需对叶子结点求哈夫曼编码。对于当前叶子结点ht[i],先将对应的哈夫曼编码hcd[i]的start域值置初值n0,找其双亲结点ht[f],若当前结点是双亲结点的左孩子结点,则在hcd[i]的cd数组中添加’0’,若当前结点是双亲点的右孩子结点,则在hed[i]的cd数组中添加’1’,并将start域减1。再对双亲结点进行同样的操作,如此这样,直到无双亲结点(即到达根结点),所以start指向哈夫曼编码最开始字符。

根据哈夫曼树求对应的哈夫曼编码的算法如下:

void CreateHCode(HTNode ht[]HCode hcd[], int n){
	int i,f,c;
	HCode hc;
	for(i=0;i<n;i++){ 					//根据哈夫曼树求哈夫曼编码
		hc.start=n;
		c=i;
		f=ht[i].parent;
		while (f!=-1){ 					//循环,直到无双亲结点,即到达根结点
			if (ht[f].lchild==c) 		//当前结点是双亲结点的左孩子
				hc.cd[hc.start--]='1';
			else 
				hc.cd[hc.start--]='0'; 	//当前结点是双亲结点的右孩子
		c=f;
		f=ht[f].parent; 				//再对双亲结点进行同样的操作
		hc.start++;
		hcd[i]=hc; 						//start指向哈夫曼编码最开始的字符
	}
}

哈夫曼编码的平均长度=
(**(((((*****************************************************((((((((((((((((((((((((((

九、用并查集求解等价问题

等价关系:对于集合S中的关系R,若有自反、对称和传递性,则R是一个等价关系。由等价关系R可以产生集合S的等价类,可以采用并查集高效地求解等价类问题。

1.并查集的定义

给定的数据是n个结点的集合U,结点编号为1~n,即U={1,2,…,n},再给定一个等价关系R(如所有表示亲戚关系的二元组就是一个等价关系),由等价关系R产生所有结点的一个划分S=U/R=(S1,S2,…,Sk),每个集合Si(1<=i<=k)表示一个等价类[x]R,其中x作为Si的一个代表,x可以是Si中的任意结点。U中每个结点属于一个等价类,所有的等价类是不相交的。并查集包含的基本运算如下:
(1)Init(S,n):初始化。
(2)Find(S,x):查找x结点所属的等价类。
(3)Union(S,x,y):将x和y所属的两个等价类合并。
上述数据结构的主要运算是查找和合并,所以称为并查集,也称为不相交集。

2.并查集的算法实现

并查集的实现方式有多种,这里采用树结构来实现。将并查集看成一个森林,每个等价类用一棵有根树表示,树中包含该等价类中的所有结点,用根结点作为其代表,由于树中所有结点是U的一个子集,所以称为子集树。每棵子集树采用双亲存储结构存储,这样并查集结点类型声明如下:

typedef struct{
	int rank; //结点秩
	int parent; //结点的双亲
}UFSTree;//并查集树的结点类型

其中,结点秩rank大致为该结点对应子树的高度,准确地说是对应子树高度的下界;parent 指向该结点的双亲结点,如果一个结点是子集树的根结点,则其parent指向自己。

1)并查集的初始化
void MAKE_SET(UFSTree s ],int n){
	int i;
	for(i=1;i<=n;i++){
		s[i].rank=0;		//秩初始化为0
		s[i].parent=i;		//双亲初始化指向自己
	}
}
2)查找一个结点所属的子集树

该运算是查找x结点所属子集的根结点(根结点rx满条件S[rx].parent=rx),这是通过S[x].parent向上找双亲实现的,显然树的高度越小查找性能越好。为此,在查找过程中进行路径压缩(即在查找过程中把查找路径上的结点逐一指向根结点),如下图所示,查找x结点的根结点为A,查找路径是x→B→A,找到A结点后,将路径上的所有结点的双亲置为A结点。这样,以后再查找x和B结点的根结点时效率更高
在这里插入图片描述
那么,为什么不直接将一棵子集树中的所有结点的双亲都置为根结点呢?这是因为还有合并运算,合并运算可能破坏这种结构。
查找运算的递归算法如下:

int Find( UFSTree S[] , int x){				//递归算法:查找x的集合编号
	if(x!=S[x].parent) 						//非根结点
		S[x].parent=Find(S,S[x].parent);	//路径压缩
	return S[x].parent;
}

时间复杂度为O(n);
查找运算的非递归算法如下:

int Find(UFSTree S(],int x){		//非递归算法:查找x的集合编号
	int rx=x;
	while (S[rx].parent!=rx)		//找x的根rx
		rx=S[rx].parent;
	int y=x;
	while(y!=rx){					//路径压缩
		int tmp=S[y].parent;
		S[y].parent=rx;				//将结点y的双亲置为rx
		y=tmp;
	}
	return rx;						//返回根
}

由于任何一棵子集树的高度不超过 log2n,上述两个查找算法的时间复杂度均不超过O(log2n)。实际上,由于采用了路径压缩,当总结点个数n<10000时,每一棵子集树的高度一般不超过 8,从而查找算法的时间复杂度可以看成常数级。

3)两个结点所属子集树的合并

所谓合并,是给定一个等价关系(x,y)后,需要将x和y所属的子集树合并为一棵子集树。首先查找x和y所属子集的根结点rx和ry,若rx==ry,说明它们属于同一棵子集树.不需要合并;否则需要合并。注意,合并是根结点rx和ry 的合并,并且希望合并后的子集树高度(rx或者ry子集树的高度通过秩rank[rx]或者rank[ry]反映)尽可能小。
其合并过程是:
(1)若rank[rx]<rank[ry],将高度较小的rx结点作为ry的孩子结点,ry子树高度不变。
(2)若rank[rx]>rank[ry],将高度较小的ry结点作为rx的孩子结点,rx子树高度不变。
(3)若rank[rx]==rank[ry],将rx结点作为ry的孩子结点或者将ry结点作为rx的孩子结点均可,但此时合并后的子树高度增1。
简单地说,高度不同时将高度较高的结点作为合并子集树的根结点,合并子集树高度不变:高度相同时可以任意合并结点,但合并子集树高度增1。对应的合并算法如下

void Union(UFSTree Sl ,int x,int y){	//将x和y所属子集树合并
	int rx=	Find(S,x);
	int ry= Find(S,y);
	if(rx==ry)							//x和属于同一棵子集树
		return;
	if (S[rx].rank>S[ry].rank)			//rx结点秩大于 ry 结点秩
		S[ryl.parent=rx;				//将结点 ry作为结点 rx 的孩子结点
	else{								//rx结点秩小于等于ry结点秩
		S[rx].parent=ry;				//将结点 rx 作为结点 ry 的孩子结点
		if(S[rx].rank==S[ry].rank)		//秩相同时
			S[ry].rank++;				//ry结点的秩增1
	}
}

合并算法的主要时间花费在查找上,其时间复杂度可以看成接近O(1)

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

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

相关文章

蓝桥杯之日期题

文章目录 1.蓝桥杯必备知识点2. 题型13.需求2 1.蓝桥杯必备知识点 蓝桥杯是一个面向全国高校计算机相关专业学生的学科竞赛&#xff0c;涵盖多个赛道&#xff0c;常见的有软件类&#xff08;如 C/C 程序设计、Java 软件开发、Python 程序设计&#xff09;和电子类&#xff08;…

PV Elite 27是专业的压力容器和热交换器设计解决方案

Intergraph PV Elite 27是专业的压力容器和热交换器设计解决方案。提供完整的容器和热交换器的设计&#xff0c;分析和评估解决方案。提供的完整的容器设计和分析、交换器设计和分析、管板设计和分析、矩形和非圆形容器分析、单个组件分析、综合误差检查、鞍座/支腿/吊耳/耳轴和…

Visual Studio 中的 /MD 与 /MT、动态库与静态库的深入解析

文章目录 1. /MD 与 /MT 的区别1.3 调试版本1.4 注意事项 2. 动态库与静态库的联系与区别2.3 联系与区别 3. 结合你的错误分析3.1 错误原因3.2 解决方案3.3 经验教训 4. 总结 在 Visual Studio 中进行 C/C 项目开发时&#xff0c;开发者经常需要对运行时库选项&#xff08;例如…

QT入门--QMainWindow

从上向下依次是菜单栏&#xff0c;工具栏&#xff0c;铆接部件&#xff08;浮动窗口&#xff09;&#xff0c;状态栏&#xff0c;中心部件 菜单栏 创建菜单栏 QMenuBar* mybar1 menuBar(); 将菜单栏放到窗口中 setMenuBar(mybar1); 创建菜单 QMenu *myfilemenu mybar1-…

深圳南柯电子|医疗设备EMC测试整改检测:零到一,保障医疗安全

在当今医疗科技飞速发展的时代&#xff0c;医疗设备的电磁兼容性&#xff08;EMC&#xff09;已成为确保其安全、有效运行的关键要素之一。EMC测试整改检测不仅关乎设备的性能稳定性&#xff0c;更是保障患者安全、避免电磁干扰引发医疗事故的重要措施。 一、医疗设备EMC测试整…

【链 表】

【链表】 一级目录1. 基本概念2. 算法分析2.1 时间复杂度2.2 空间复杂度2.3 时空复杂度互换 线性表的概念线性表的举例顺序表的基本概念顺序表的基本操作1. 初始化2. 插入操作3. 删除操作4. 查找操作5. 遍历操作 顺序表的优缺点总结优点缺点 树形结构图形结构单链表基本概念链表…

一周学会Flask3 Python Web开发-Jinja2模板过滤器使用

锋哥原创的Flask3 Python Web开发 Flask3视频教程&#xff1a; 2025版 Flask3 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 在Jinja2中&#xff0c;过滤器(filter)是一些可以用来修改和过滤变量值的特殊函数&#xff0c;过滤器和变量用一个竖线 | &a…

【STM32H743IIT6】STM32H7的ADC时钟频率设置问题 —— 网上大多文章未注意到的要点!

前言 我使用的是定时器触发ADC采样。最近在想达到ADC的最高采样率的时候&#xff0c;发现一直却卡在1Msps上不去&#xff0c;直到在硬汉嵌入式的论坛里才发现了答案&#xff1a;[ADC] STM32H743/H750的Y版和V版芯片ADC的主频区别 这篇文章就详细的讲一下这个问题&#xff0c;这…

JavaScript基础(函数及面向对象)

函数 定义函数 Java定义方法&#xff1a; public 返回值类型 方法名(){ return 返回值 } 定义函数方法一 eg&#xff1a;定义一个绝对值函数 function abs(x) {if (x>0){return x;}else {return -x;}} 调用函数&#xff1a; 注意&#xff1a;一旦执行到return代表函数…

2025面试Go真题第一场

前几天参加了一场面试&#xff0c;GoLang 后端工程师&#xff0c;他们直接给了我 10 道题&#xff0c;我留了一个截图。 在看答案之前&#xff0c;你可以先简单做一下&#xff0c;下面我会对每个题目做一个说明。 文章目录 1、golang map 是否并发安全?2、协程泄漏的原因可能是…

【有奖实践】轻量消息队列(原 MNS)订阅 OSS 事件实时处理文件变动

当你需要对对象存储 OSS&#xff08;Object Storage Service&#xff09;中的文件变动进行实时处理、同步、监听、业务触发、日志记录等操作时&#xff0c; 你可以通过设置 OSS 的事件通知规则&#xff0c;自定义关注的文件&#xff0c;并将 OSS 事件推送到轻量消息队列&#x…

关于Postman自动获取token

在使用postman测试联调接口时&#xff0c;可能每个接口都需要使用此接口生成的令牌做Authorization的Bearer Token验证&#xff0c;最直接的办法可能会是一步一步的点击&#xff0c;如下图&#xff1a; 在Authorization中去选择Bearer Token&#xff0c;然后将获取到的token粘贴…

Baklib知识中台构建企业智慧中枢

智能技术架构构建路径 Baklib知识中台的技术架构设计以模块化和可扩展性为核心&#xff0c;通过分层解耦的架构体系实现知识管理的全流程覆盖。底层依托智能语义分析引擎与多模态知识图谱&#xff0c;完成非结构化数据的自动清洗与语义关联&#xff1b;中间层构建统一的知识资…

解决安卓recyclerView滚到底部不彻底问题

问题分析&#xff1a; 传统recycleview滚到到底部方式scrollToPosition(lastpositon)&#xff0c;只能定位到最后一条数据的顶部。由于数据过长&#xff0c;无法滚动到最底部。 问了下deepseek&#xff0c;给了个方案&#xff1a; private void recyclerViewScrollToBottom()…

StepAudio:语音大模型

Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统&#xff0c;支持多语言对话&#xff08;如 中文&#xff0c;英文&#xff0c;日语&#xff09;&#xff0c;语音情感&#xff08;如 开心&#xff0c;悲伤&#xff09;&#xff0c;方言&#xff…

Kafka可视化工具EFAK(Kafka-eagle)安装部署

Kafka Eagle是什么&#xff1f; Kafka Eagle是一款用于监控和管理Apache Kafka的开源系统&#xff0c;它提供了完善的管理页面&#xff0c;例如Broker详情、性能指标趋势、Topic集合、消费者信息等。 源代码地址&#xff1a;https://github.com/smartloli/kafka-eagle 前置条件…

[Web 安全] PHP 反序列化漏洞 —— PHP 反序列化漏洞演示案例

关注这个专栏的其他相关笔记&#xff1a;[Web 安全] 反序列化漏洞 - 学习笔记-CSDN博客 PHP 反序列化漏洞产生原因 PHP 反序列化漏洞产生的原因就是因为在反序列化过程中&#xff0c;unserialize() 接收的值可控。 0x01&#xff1a;环境搭建 这里笔者是使用 PhpStudy 搭建的环…

2.部署kafka:9092

官方文档&#xff1a;http://kafka.apache.org/documentation.html (虽然kafka中集成了zookeeper,但还是建议使用独立的zk集群) Kafka3台集群搭建环境&#xff1a; 操作系统: centos7 防火墙&#xff1a;全关 3台zookeeper集群内的机器&#xff0c;1台logstash 软件版本: …

springboot博客系统详解与实现(后端实现)

目录 前言&#xff1a; 项目介绍 一、项目的准备工作 1.1 数据准备 1.2 项目创建 1.3 前端页面的准备 1.4 配置配置文件 二、公共模块 2.1 根据需求完成公共层代码的编写 2.1.1 定义业务状态枚举 2.1.2 统一返回结果 2.1.3 定义项目异常 2.1.4 统一异常处理 三、业…

seacmsv9注入管理员账号密码+orderby+limit

一、网上收集&#xff1a; 海洋影视管理系统&#xff08;seacms&#xff0c;海洋cms&#xff09;是一套专为不同需求的站长而设计的视频点播系统&#xff0c;采 用的是 php5.Xmysql 的架构&#xff0c;seacmsv9漏洞文件&#xff1a;./comment/api/index.php&#xff0c;漏洞参数…