数据结构---树和二叉树

🌞欢迎来到数据结构的世界 
🌈博客主页:卿云阁

💌欢迎关注🎉点赞👍收藏⭐️留言📝

🌟本文由卿云阁原创!

🙏作者水平很有限,如果发现错误,请留言轰炸哦!万分感谢!


目录

树的定义和基本术语

 树的常考性质

二叉树的定义和基本术语 

二叉树性质

二叉树的存储结构

二叉树先/中/后序遍历

二叉树层次遍历

由遍历序列构造二叉树

 线索二叉树的概念 

二叉树的线索化

线索二叉树找前驱/后继 

树存储结构

树和森林的遍历

 哈夫曼树


树的定义和基本术语

     顾名思义它和我们在大自然中看到的树是很相似的,从树根出发向上生长,会长出很多的分支,每一个分支又会长出很多小的分支。树的数据结构在逻辑上也是具有这样的特性,从根节点出发,长出各种分支(边),每个分支上又可以连接新的节点,如果一个节点还有下一个节点,我们把它叫做分支节点(如橙色的节点),绿色的节点没有下一个节点了,我们把它叫做根节点。每一个节点就是用于存放数据元素(比如int char 等)。树还有一种特殊的数据结构(空树-节点数为0的树)。

   当n > 1时,其余结点可分为m(m > 0)个互不相交的有限集合T1, T2,…, Tm,其中每个集合本身又是一棵树,并且称为根结点的子树。我们可以把子树看成一个新的树。

树在生活中有那些应用呐? 

      比如一个国家可以划分成多个省,省可以划分成多个市,市可以划分成多个县等等,这就是一个典型的树形结构。

节点之间关系的描述

节点和树的属性描述(节点的层次默认是从1开始)

 有序树VS无序树(左右位置是否需要反应逻辑关系)

 树VS森林

   森林。森林是m(m≥0)棵互不相交的树的集合(m可为0, 空森林)   eg:全中国所有

人家的家谱就可以用森林表示。

 


 树的常考性质

常见考点1:结点数=总度数+1

实际上我们不难发现,总度数就等于边的数量,节点数=边的数量+根节点

 常见考点2:度为m的树、m叉树的区别

 

 


二叉树的定义和基本术语 

二叉树的定义和基本术语 

 几种特殊的二叉树

      在二叉排序树上进行搜索操作就会很简单,比如要搜索值为60的节点,从根节点出发,发现60是大于根节点的,因此向右子树方向找,发现60是大于50的,因此向右子树方向找,发现60是小于66的,因此向左子树方向找。

     假设我们要插入一个新的节点68,从根节点出发,发现60是大于19的,说明这个节点要插在右子树,发现68是大于50的,说明这个节点要插在右子树,发现68是大于66的,说明这个节点要插在右子树,发现68是小于70的,说明这个节点要插在左子树。这样我们就能保证插入一个元素之后,仍然是一棵二叉排序树。

         如果我们要搜索70的节点,在左边的二叉树上往下走两步就可以找到,右边的二叉树走很多步才可以找到。平衡二叉树就是希望树在生长的过程中,就是长的越胖越好,这样搜索效率就会高。


二叉树性质

 

 


二叉树的存储结构

       我们要把这个二叉树在内存里面顺序的,连续的存放,所以我们可以定义一个数组TreeNode t[MaxSize],TreeNode t[MaxSize]里面的每一个元素就对应着一个节点,每一个节点里面包含着value(这个节点里面存放的值),和bool isEmpty(这个节点是否是空节点),初始化数组时我们需要把所有的isEmpty设置成true,也就是说刚开始这些节点都是空的没有任何实际的数据按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点。可以让第一个位置空缺,保证数组下标和结点编号一致)

#define MaxSize 5
typedef struct TreeNode
{
  int value;
  bool isEmpty;
}TreeNode;
void InitTree(TreeNode t[])
{
  int i;
  for(i=1;i<MaxSize;i++)
      t[i].isEmpty=true;
}
int main() 
{
  TreeNode t[MaxSize];
  InitTree(t);
  return 0;
}

     下面我们希望用这种顺序存储的方案可以反映出前驱和后继的关系,或者说父子的关系,我们至少需要实现这样的几个基本操作,给一个节点i可以找到它的左孩子,它的右孩子,它的父节点和所在的层次,由于这一棵二叉树是完全二叉树,我们知道i节点的左孩子节点的编号是2i,右孩子节点的编号是2i+1。

       但是如果这个树不是完全二叉树怎么办呐? 如果按照刚才的方式,按照一层一层将节点分别编号,然后放到相应的位置中,此时这些节点的编号已经没有办法反映出节点之间的逻辑关系了,因此普通的二叉树不能按照这种方式存储。

    为了解决这个问题,我们可以让二叉树的节点编号和完全二叉树对应起来,但是采用这种方式的时候我们就不能用用n和2i作比较来确定,是否存在左子树。(非完全二叉树) 此时我们可以检查isEmpty的值是否为空来判断。显然这样会造成大量的空间的浪费。结论:二叉树的顺序存储结构,只适合存储完全二叉树。

       对于左边的二叉树采用链式存储后的,如右所示,这个树的每一个节点(BiTNode)都会有一个data,用来存放数据元素,还会有两个指针分别指向左孩子(lchild)右孩子(rchild),如果一个节点没有左孩子或者右孩子的话,我们会把指针设置成NULL,如果一棵树有n节点的话,那么就有2n指针域。 

 二叉树从无到有的过程?

#include<stdio.h>
#include<stdlib.h>
typedef int ElemType;
typedef struct BiTNode{
 
 ElemType data;                    
 struct BiTNode *lchild,*rchild;        
 
}BiTNode,*BiTree;
int main() {
  BiTree root =NULL;
  root=(BiTree)malloc(sizeof(BiTNode));
  root->data=1;
  root->lchild=NULL;
  root->rchild=NULL;
  BiTNode *p=(BiTNode *)malloc(sizeof(BiTNode));
  p->lchild=NULL;
  p->rchild=NULL;
  root->lchild=p;
  return 0;
}

 ​​​​​​

        如果我们给出一个节点P,想要找到这个节点的左孩子和右孩子,我们只要检查它的左孩子和右孩子指针指向的位置就可以了,所以要找到它的左右孩子是很简单的,但是如果要找到父节点的话,就必须从根节点遍历,所以如果在应用场景中需要经常的逆向的找到父节点的话,我们可以在结构体中再定义一个指针,指向它的父节点。


二叉树先//后序遍历

什么是遍历?

按照某种次序把所有结点都访问一遍

 二叉树的遍历

         我们可以根据三个部分的访问规则,指定这样的顺序。分别是先序,中序,后序遍历。所谓的先序遍历就是先访问根节点,再访问左子树,然后访问右子树。如果左子树还有下一级的节点,同样按照先序遍历的规则来遍历这个左子树。

 

先序遍历的代码实现

先序遍历的基本思想就是:

     如果二叉树为空,就啥也不做,如果二叉树非空我们按照先序遍历的规则,访问根结点visit(T)这个函数可以实现打印结点的值,先序遍历左子树PreOrder(T->lchild),先序遍历右子树PreOrder(T->rchild),中序和后序遍历的代码也是类似的。

下面我们以先序遍历代码为例子,看一下代码的执行过程。

   我们要遍历下面这个二叉树,首先传递的根节点是A,当我们在调用一个函数时,系统会在背后给我们开辟一个系统调用栈,这个栈里面会保存当前函数执行的相关的信息。

第1次PreOrder函数,此时的参数T指向的是A这个节点。由于当前节点非空,执行visit(A),然后访问左子树PreOrder(T->lchild),系统也会记录我们访问的是126行代码

第2次调用PreOrder函数,此时的参数T指向的是B这个节点,由于当前节点非空,执行visit(B),然后访问左子树PreOrder(T->lchild),系统也会记录我们访问的是126行代码

第3次调用PreOrder函数,此时的参数T指向的是D这个节点,由于当前节点非空,执行visit(B),然后访问左子树PreOrder(T->lchild),系统也会记录我们访问的是126行代码

第4次调用PreOrder函数,此时的参数T指向的是NULL,由于当前节点空函数执行结束,把这层的函数弹出栈

     回到上一层函数的调用处,也就是对D节点的处理,我们知道之前执行的是126行代码,因此接下来应该执行127行代码

 第5次调用PreOrder函数,此时的参数T指向的是G,由于当前节点非空,执行visit(G),然后访问左子树PreOrder(T->lchild),系统也会记录我们访问的是126行代码

 第6次调用PreOrder函数,此时的参数T指向的是NULL,由于当前节点空函数执行结束,把这层的函数弹出栈回到上一层函数的调用处,也就是对G节点的处理,我们知道之前执行的是126行代码,因此接下来应该执行127行代码

 第7次调用PreOrder函数,此时的参数T指向的是NULL,由于当前节点空函数执行结束,把这层的函数弹出栈回到上一层函数的调用处,也就是对G节点的处理,我们知道之前执行的是127行代码,后面就没有代码了说明这层函数执行结束返回上层函数调用处。

     下面的执行类似,我们可以看到这种递归实现的算法,它的空间复杂度是O(h+1)这个量级。 h指的是二叉树的高度,+1是因为处理根节点时也需要把它的信息压倒栈顶。我们可以把常数项舍去,这种算法的空间复杂度就是O(h)。我们在画这些箭头的时候,红色箭头表示函数执行流第一次经过这个节点,绿色箭头表示函数执行流第二次经过这个节点,紫色箭头表示函数执行流第三次经过这个节点,这些节点的处理路径有这些规律,我们从根节点出发,画一条路,如果左边还有没走的路,优先往左边走,走到路的尽头(空结点)就往回走

如果左边没路了,就往右边走,如果左、右都没路了,则往上面走

应用 

     比如现在我们要求一棵树的深度,我们可以递归的求出左子树的高度,然后是右子树的高度,接下来,我们选取左右子树高度较高的一边,然后再加1。其实这个算法就是后序算法的变种。

int treeDepth(BiTree T)

{

   if(T==NULL)

        return 0;

  else{

    int l=treeDepth(T->lchild);

    int r=treeDepth(T->rchild);

    return l>r ? l+1 :r+1;

}


二叉树层次遍历

      一层一层的遍历二叉树的结点,如果想实现二叉树的层次遍历我们要设置一个辅助队列,首先我们要把根结点放到队列中,下面我们需要循环执行下面的操作,如果队列不空,就让队头元素出队,同时访问该节点,并将左右孩子插入队尾(如果有的话),直到队列为空。

代码实现?真正入队出队的是节点的指针。

void LevelOrder(BiTree T)

{

   LinkQueue Q;

   InitQueue(Q);

   BiTree p;

   EnQueue(Q,T)

   while(!IsEmpty(Q))

{

  DeQueue(Q,p);

  visit(p);

  if(p->lchild!=NULL)

      EnQueue(Q,p->lchild)


 if(p->rchild!=NULL)

      EnQueue(Q,p->rchild)

}

}


由遍历序列构造二叉树

结论:若只给出一棵二叉树的 前///层 序遍历序列中的一种,不能唯一确定一棵二叉树

结论:一个中序遍历序列可能对应多种二叉树形态

结论:一个前序遍历序列可能对应多种二叉树形态 

 

 

 创建二叉树


int CreateBiTree(BiTree &BT)
{
	//按先序次序输入扩展的正则二叉树中结点的值(一个字符)
	//构造二叉链表表示的二叉树BT
	scanf("%c",&ch);
	if (ch==' ')  BT=NULL;    //当读到空格符时建立空二叉树
	else
	{
		if (!(BT=(BiTree)malloc(sizeof(BiTNode)))) exit(OVERFLOW);
		BT->data=ch;                //建立根结点
		CreateBiTree(BT->lchild);      //建立左子树
		CreateBiTree(BT->rchild);      //建立右子树
	}
	return 1;
}


 线索二叉树的概念 

        对于普通的二叉树我们可以对其进行遍历,树中的各个元素之间是一种非线性的关系,可以我们通过中序遍历后得到的是一个线性的序列。每当我们进行中序遍历的时候都要从根节点出发,只有知道根节点是那一个才有可能对二叉树进行中序遍历,但是在某些场景中我们只能知道某个节点的指针,比如G节点的指针。如果我们只知道G节点的指针,我们是否能从G节点出发对树进行中序遍历(GBEAFC)。显然这种操作是完成不了的,因为G节点只有指向它孩子的指针,不能从G节点往回找。找到双亲或者后继节点B(指的是中序遍历序列中的后继)。

       第一个问题不能从一个指定结点开始中序遍历

这个时候可以想到线性表,它是可以从任意一个位置向后遍历的。

      第二个问题是如何找到指定结点p在中序遍历序列中的前驱?

     从根节点出发,重新进行遍历,p指向指定的节点q指向当前节点pre指向当前节点的上一个节点。
                                 ①当q==p时, pre 为前驱
                                 ②当pre==p时, q为后继
     比如在中序遍历中,D是第一个被遍历的节点,所以刚开始会让q指向D,pre指向NULL,D节点是没有前驱的,所以前驱指针指向NULL,此时q和p没有指向同一个节点,所以需要继续往后遍历,pre指向当前q指向的节点,q指向下一个被访问的节点,此时pre指向的节点是q指向节点的前驱节点。
       为了解决上述的问题,有人就提出来了线索二叉树。 

         目前这个节点的访问序列我们已经知道了,我们也标上了每个节点的访问序列,现在我们开始对二叉树进行改造,使其方便找到它的前驱和后继,n个结点的二叉树,有 n+1个空链域!可用来记录前驱、后继的信息,比如像G这个节点第二个被访问到的,它的前驱应该是D这个节点,所以我们可以让它的左孩子指针指向D,它的后继应该是B这个节点,所以我们可以让它的右孩子指针指向B,这样我们就很方便从一个节点出发找到它的前驱和后继,像D这个节点,它是第一个被访问的节点它没有前驱,我们可以让它的左孩子指针指向NULL,线索化之后得到的二叉树如右图所示,这棵二叉树,就是中序线索二叉树(制造的规则是按照中序遍历序列的)如果一个节点的左孩子指针和右孩子指针指向的前驱和后继而不是它的左右孩子的话,我们就把这种类型的指针称之为线索。到现在为止我们就解决了问题2,但是问题1我们要怎么解决呐?

       其实我们所谓的遍历就是不断找后继的过程,对于节点D来说,它的后继线索本来就是指向后继的,但是对于一个节点的右孩子指针指向的是它的右孩子,这样的一个节点又怎么找它的后继呐?这个问题我们后面再来探讨。现在我们只需要知道线索化之后找前驱和后继以及遍历会更加的方便。

我们可以再举个栗子 

线索二叉树的存储结构

       在普通的二叉树中我们只定义了一个数据域data,以及左孩子指针和右孩子指针,但是在线索二叉树中它指向的可能不是左右孩子,而是指向的是它的前驱和后继,所以我们需要增加两个标志位ltag,rtag。

#include<stdio.h> 
//线索二叉树的存储结构
typedef struct ThreadNode
{
	int data;//存储元素
	struct ThreadNode *lchild,*rchild;
	int ltag,rtag; 
}ThreadNode,*ThreadTree;
int main()
{
   ThreadTree T;
   printf("线索二叉树的存储结构");
}

 


二叉树的线索化

        前面是用手算的方法来求线索二叉树,接下来我们用代码的方式来实现,我们前面已经说了三种二叉树,与这三种二叉树所对应的线索化的过程就分别称为中序线索化,先序线索化,后序线索化我们首先来看一下如何进行中序线索化。上面我们介绍了怎么找到中序遍历的前驱,它的代码怎么实现呐?我们可以在visit中判断我们当前节点q最终要找的p是不是同一个节点,如果q=p,则说明pre指向的就是p的前驱节点。

#include<stdio.h> 
//辅助变量用于寻找p的前驱
BiTNode *p;  //指向目标节点 
BiTNode *pre=NULL;  //指向当前的节点的前驱节点
BiTNode *final=NULL;  //用于记录最后的结果
 
//中序遍历找到某一个节点的前驱节点 
void InOrder(BiTree T)
{
	if(T!=NULL)
	{
		InOrder(T->lchild)
		visit(T)
		InOrder(T->rchild)
	}
 } 
//定义visit函数 
void visit(BitNode *q)
{
	if(q==p)
	    final=pre;
	else
	    pre=q;	
 } 
int main()
{
   return 0;
}

  

    下面回到我们这一小结的问题怎么对二叉树进行线索化,当我们对二叉树的某一个节点进行线索化实际上就是让这个节点的左孩子连上它的前驱,让它的右孩子连上它的后继。如果当前节点的左孩子为空q->lchild==NULL),让pre的值等于它的左孩子指针q.ltag=1           假设我们现在已经有了一个初步建成的二叉树了,ltag和rtag=0(这些指针指向的是左右孩子)没有进行线索化,这里我们仍然定义一个全局变量pre指向当前访问的前驱。中序遍历的第一个节点是D,是没有前驱的,所以pre的值一开始指向NULL(ThreadNode *pre=NULL)。由于当前的节点D没有左孩子的(q->lchild==NULL),所以需要对其进行线索化,可以做visit中完成,当前节点的左孩子指针指向前驱(q->lchild=pre),修改标志位(q->ltag=1)。同时修改pre指针(pre=q)。当q指针指向B时,左右孩子指针都是非空的,但是它的前驱节点pre指向的节点右孩子是空的(pre!=NULL&&pre->rchild==NULL),所以我们需要把这个节点进行线索化(pre->rchild=q,pre->rtag=1)。当q指向第7个节点C的时候,pre=q执行结束之后,不会再有节点会被visit,但是现在存在一个问题,最后一个节点的右孩子本来因该是空的我们因该把它线索化,不过我们设置的pre指针是一个全局变量,所以我们可以在其它函数中对当前pre指针指向的节点再进行一个处理,当pre的值指向NULL时说明没有后继,同时把它的rtag设置为1,我们需要对最后一个节点进行特别的处理下面时一个完整的带代码。CreateInThread()是中序线索化二叉树的函数,一开始pre的值指向NULL,如果二叉树非空的话(T!=NULL),就可以对其进行线索化(InThread()),我们还需要特殊处理最后一个节点,如果最后一个节点的右孩子指针指向的是NULL,就让它的rtag=1。

 

       接下来我们来看一下,先序线索化,执行过程类似,一开始pre指向NULL,q指向的是A这个节点,A的左右孩子都不为空,所以不需要进行处理,只需要让pre指向当前的节点q即可,然后q指向下一个节点B,现在我们来注意这样一个问题,现在q真正访问第三个节点D,此时pre指向第二个节点B,,此时我们应该让q的左孩子指向pre。pre指向当前访问的节点,下面我们会处理D节点的左子树,但是之前我们已经让它的左孩子指向了B这个节点,所以接下来如果要访问它的左子树的话,就会导致q节点再一次的指向B,也就是对节点的访问会出现这种无限的循环。怎处理了呐?我们可以通过ltag指针变量来判断指向的是否是它真正的左孩子,所以只有(当前节点的ltag==0)才需要对其进行线索化。因为左孩子节点一旦被线索化之后,指向的节点是当前访问节点的前驱节点,所以我们按照前驱线索往前访问上一个节点的话,就相当于访问一个节点后又去访问它的前驱,这样就会导致转圈问题。

最后我们看一下,后序遍历不会出现转圈的问题,原因在于,当我们访问节点q的时候,左孩子和右孩子的那条路我们肯定已经处理完了,所以访问完这个节点后不可能回头再访问它的左孩子所指向的那颗子树。

 


线索二叉树找前驱/后继 

        中序二叉树找中序后继,我们的目的是找指定节点p中序后继next,如果指定节点的右孩子指针已经被线索化(p->rtag==1)next=p->rchild对于B节点,它的右孩子指针没有被线索化,接下来怎么找到它的中序后继呐,由于p->rtag==0,说明指定的节点是有右孩子的,我们是要找按照中序遍历找到当前节点的后面的一个节点,中序遍历的访问顺序应该是左根右,所以p节点的右子树中序遍历的第一个节点就应该是p的后继。

       假设p只有一个右孩子(且是叶子节点),显然这个节点就是p的后继,但是如果这个节点是分支节点,那么对这个右子树进行中序遍历,这个节点左下角的节点就是,后面第一个被访问的节点,再假设这个节点仍然是分支节点,这个节点的左孩子就是p后被访问的第一个节点,所以规律就是,p节点的右子树中,左下角的最后一个节点就是后继节点。

      然后我们来看一下怎么实现这个代码。首先,我们可以实现这样一个函数Fistnode,从指定的节点出发,如果他还有左孩子的话,一直会顺着左分支,找到最左下角的节点,按照您中序遍历的规则来看,这个节点是最第一个被中序遍历的节点,所以如果想找到p的后继节点如果右指针没有被线索化,我们就需要找到右子树中第一个被遍历的节点。

      下面我们来看一下如何找到它的前驱,如果左孩子被线索化(p->ltag==1),那么左指针指向的节点,就是前驱节点,如果没有被线索化,说明其肯定是有左孩子的,p的前驱一定是左子树按照中序遍历后最后一个元素,如果左子树只有一个节点,这个节点就是左子树的最后一个节点,如果它还有下一级的分级的话,它的右孩子节点是最后一个被访问的节点,所以p的左子树最右下的节点就是p的前驱节点,首先Lastnode做的事情就是,找到最后一个被遍历的节点。

    先序二叉树找先序后继,如果指定节点的右孩子指针已经被线索化(p->rtag==1)next=p->rchild。它的右孩子指针没有被线索化,(p->rtag==0)说明指定的节点是有右孩子的,但是我们不知道它是否是有左孩子,先假设这个节点既有左孩子又有右孩子,p节点的的最后一个节点,一定是左子树中第一个被访问的节点,如果没有左孩子,p节点的的最后一个节点,一定是右子树中第一个被访问的节点。

     怎么找先序前驱,当如果左孩子被线索化(p->ltag==1),那么左指针指向的节点,就是前驱节点,如果没有被线索化,说明其肯定是有左孩子的,p的左子树和右子树的所有节点都只能是p的后继(先序的遍历规则),因此不可能在左右子树上找到它的前驱,所以在这种情况下我们是找不到前驱的,除非是按照我们之前的土办法,从头开始遍历,这个结论是建立在每个节点只有指向孩子的基础上,其实我们可以把二叉链表改成三叉链表,给各个节点设置一个指向它父节点的指针。

      假设我们能找到p的父节点,且p是父节点的左孩子,p节点是父节点被访问后的节点,因此它的父节点一定就是它的前驱。

    假设p是右孩子且左兄弟为空,因此它的父节点一定就是它的前驱。

   假设p是右孩子且左兄弟为非空,p节点的先序前驱一定是,左子树中按照先序遍历最后被访问的节点(从根节点出发优先往右走,如果右边的路没有了,可以往左边的路走,然后找到最下面一层的节点,这个节点就是最后一个被 访问的节点。

    如果p没有父节点,而是根节点,是没有先序前驱的,

   后序二叉树找后序前驱,如果左孩子被线索化(p->ltag==1),那么左指针指向的节点,就是前驱节点,如果没有被线索化,说明其肯定是有左孩子的,但是无法知道是否有右孩子,我们先假设有右孩子的情况,p的后序前驱一定是右子树按照后序遍历的最后一个节点,假设没有右孩子的情况,p的后序前驱一定是左子树按照后序遍历的最后一个节点。

    后序二叉树找后序后继,这种情况说明肯定是有右孩子的,节点p的左子树和右子树只可能是它的前驱而不可能是它的后继,所以我们只能用土办法重新进行一次后续遍历,当然如果我们用三叉链表来存储一棵二叉树。

如果能找到p的父节点,且p是右孩子,p的父节点就是它的后继。

如果能找到p的父节点,且p是左孩子,且右兄弟为空,p的父节点就是它的后继。

如果能找到p的父节点,且p是左孩子,且右兄弟非空,p的后继是右兄弟子树中第一个被后序遍历的节点。(尽可能的往左走,左边的路没了,再往右走,找到最下面一个叶子节点)

如果p是根节点,这种情况下,p是没有后继的。

 


树存储结构

      树是一种递归定义的结构,一个树要不是一棵空树要不是由一个根节点和几棵互补相交的子树组成的。下面我们来看一下怎么存储?

      第一种方法叫做双亲表示法,除了根节点外,其余节点一定都有自己的双亲节点,所以每个节点除了保存本身的数据(data)外,还要保存指向父节点的指针(parent),我们可以把根节点放在0号位置。-1表示它的上面已经没有父节点了,像C这个节点,它的父节点是根节点,所以它的父节点的指针设置成0,我们可以用这样的方式定义一个数据结构。

#define MaxSize 5
typedef struct
{
 char data;
 int parent;
}PTNode;
typedef struct
{
  PTNode nodes[MaxSize];
  int n;
}PTree;
int main() 
{
  PTree T;
  T.nodes[1].data='A';
  T.nodes[1].parent=-1;
  return 0;
}

增加一个新的结点

      假设我们要增加一个H的孩子M节点,我们在数组里记录这个节点的值和它的父节点的位置,我们看到数组中的排列顺序好像是一层一层排的,但是插入新节点的时候我们不一定要遵守刚才的那种规则。

  删除一个结点

     假设我们要删除G这个节点,可以有这样的两种方案

第一种方案是把G这个节点的双亲节点设置成-1,这就表示这个位置是空的。

第二种方案是用尾部的元素填充这个位置,还需要更改节点数n的值。

       假设我们要删除的节点不是叶子节点,那怎么进行删除操作呐?

     就不能简单的删除这个节点,因为这样的操作实际上删除的是以D为根节点的子树,所以我们还需要找到它的子孙节点,然后把它们删除,这就涉及到从一个节点找到它的孩子节点这样的操作,即查询操作,当我们采用这种存储方式的时候找到它的双亲是很简单的,比如节点D的父节点是数组的0号位置,但是如果想找到它的孩子只能从头到尾进行遍历,然后对比节点的双亲指针是否是指向D节点,所以找孩子是很不方便的,同时这里也暴露了我们删除的第一种方案存在的问题,如果我们没有用最后一个元素来填充删除的位置,会让我们多遍历一个无效的节点,导致遍历的速度变慢。

     下面我们来看一下孩子表示法(CTree)我们会用一个数组来保存节点的数据,每个节点(CBox)包含两个信息,除了要保存它本身的数据域(data)之外,还要保存它第一个节点的指针(*firstChild),链表只是保存了各个节点的数组下标,有了存储结构之后我们就开始考虑相关的操作了。显然找孩子是很简单的,但是找双亲是很复杂的。

#define MaxSize 3
typedef struct
{
  int data;
  struct CTNode *next;
}CTNode;
typedef struct
{
  int data;
  struct CTNode *firstChild;
}CTBox;
typedef struct
{
  CTBox nodes[MaxSize];
  int n,r;
}CTree;
int main() 
{
  CTree T;
  return 0;
}

    下面我们看孩子兄弟表示法,每个节点包含了数据域(data),和两个指针(*firstchild,*nextsibling),A的左指针应该指向B,B的右指针连上C,同样让C的右指针连上D,B的做左指针应该指向E,E的右指针指向F,E的左指针指向F等等,实际上就是树和二叉树的转换。

 

typedef struct
{
  int data;
  struct CSTNode *firstChild,*nextsibling;
}CSTNode,*CSTree;

 下面我们再来看一下如何把二叉树转换成树

森林转换成二叉树,我们也可以用二叉链表的方式保存一棵森林,森林是由几棵互不相交的树组成,我们把树依次转换成二叉树,接下来由于这些树在逻辑上来看是平级的,因此我们可以把这些树的根节点看作是兄弟节点,这就是森林转换成二叉树的过程。

 

二叉树转换成森林,从根节点出发右路的节点都是平级的关系,对应了几棵互不相交的树,分别是A,C,F,L。A这个节点左边连了B,所以B是A的孩子,同理D是B的孩子,G是D的孩子,D的右边连了H,所以H和D是兄弟关系。

 


树和森林的遍历

 

      首先我们看一下树的先根遍历,先访问根节点,然后依次对各个子树进行遍历,如果当前的根节点不空,就访问根节点,由于一棵普通的树中一棵树可能有多个子树,所以我们可以用While循环检查,它是否还有没有访问的子树,然后对各个子树采取先根遍历。树的中根和后根遍历类似。(树的后根和先根遍历也叫做深度优先遍历)

 

 

 

树的层次遍历,首先让根节点入队。尽可能的先树的广度遍历(广度优先遍历)

 

每一次让队头元素出队并访问这个元素。

 

如果这个节点有孩子的话,需要依次把孩子入队

 

下面重复上述的操作。

森林的遍历

 

 

 

 


 哈夫曼树

 

掌握相关的概念:结点的权,结点的带权路径长度,树的带权路径长度

 哈夫曼树的构造

       思路: 选择权值最小的两个结点,让他们成为兄弟,然后把这两个结点的权值之和作为新结点的权值。

       结点的总数:2n-1

 (我们每次会让两棵树结合,一共是n个结点,需要结合n-1次,每次的结合会导致一个新结点的出现,所以结点总数是2n-1。)

      哈夫曼树不唯一,不存在度为1的结点

 哈夫曼编码

       固定长度编码

       可变长度编码

        我们都看到过看电视剧中发电报的场景,发电报的时候就需要用到哈夫曼编码,而哈夫曼编码的构造是基于哈夫曼树的。电报——点、划 两个信号(二进制0/1),收听电报的人只需要把点、划翻译成有意义的字符就行了。

       有两个兄弟再弄清楚编码的原理后,约定着要在考试中互相传答案,用二进制0和1来传递选择题的答案,如果小渣咳嗽表示是二进制的0,打嗝表示是二进制的1,用这样的方式悄悄的传递信息,选择题中只有可能出现A,B,C,D这4个选项,他们就可以用熟悉的ASCII编码来传递信息,ASCII编码使用的是8个比特位,这种编码方式就叫做固定长度编码,使用这种方式传递是不高效的,因为一个答案要传递8次,实际上我们只要用两个二进制位就可以表示这4个答案,假设100题中有80题选C,10题选A,8题选B,2题选D,他最终只要传递200个比特位即可,这种编码方式我们可以把它映射到树这种方式,从根节点出发向左走表示是二进制的0,向右走表示是二进制的1,如果采用哈夫曼编码只要传递130次即可。我们的目的是用四个二进制串来区分4个字符,我们也可以使用一个二进制位1来表示A这个选项呀,但是假设发送的答案是CAAABD,对应的编码是0111111110,可能会被翻译成CBBD。

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卿云阁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值