树和二叉树

注:文章参考严老师的《数据结构与算法c语言》自复习用

一. 树和二叉树

(一) 树的定义及基本术语

1.1 知识点

  • 树的结点也可以是0,即空树。
  • 在非空树中,有且仅有一个根节点。

1.2 ADT基本操作(15)

左指针指向最左孩子,右指针指向右边的第一个兄弟

 1. 构造空树
 2. 销毁树
 3. CreateTree(&T,definition) 按definition的定义构造树T;
 4. 清空树
 5. 判断树是否为空
 6. 深度
 7. 返回树的根
 8. 返回结点的值
 9. 给结点赋值
 10. 返回一个结点的双亲
 11. 返回结点的最左孩子 
 12. 返回结点的右兄弟
 13.  InsertChild(&T,&p,i,c)	插入c作为中p指向的结点的第i棵子树
 14. 删除第i棵子树
 15. 遍历

补充的重要ADT

 16. 复制
 17. 显示
 18. 是否一样

(二) 二叉树BiTree

2.1 二叉树的定义

  • 知识点

      1. 每一个结点至多只有两棵子树
      2. 二叉树子树有左右之分,即有次序
      3. 二叉树或为空,或是由一个根节点加上两棵分别称为左、右子树的,互不相交的二叉树组成(也可以是空树)。
    
  • 特殊二叉树结构

    1. 满二叉树:有k层的二叉树有2k+1个结点
    2. 完全二叉树:满二叉树从后往前删去结点形成的树结构,即有n个结点k层的二叉树前k-1层为满二叉树,第k层从右向左有结点直到n。
  • ADT基础操作描述

      1-10. 同树的ADT基础操作相同
      11. 左孩子
      12. 右孩子
      13. 做兄弟
      14. 有兄弟
      15. 先序遍历
      16. 中序遍历
      17. 后序遍历
      18. 层序遍历
    

2.2 二叉树的性质


性质1 在二叉树的第i层上,最多有2i-1个结点( i ≥ 1 i\ge1 i1


性质2 深度为k的二叉树至多有2k-1个结点


性质3 对任何一颗二叉树T,如果其终端节点数为 n 0 n_0 n0,度为2的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1

设结点总数为n,则显然有:
n = n 0 + n 1 + n 2 n=n_0+n_1+n_2 n=n0+n1+n2
且除顶点外,每一个结点的入度为一,而树的入度等于出度,故:
n = n 1 + 2 n 2 + 1 n=n_1+2n_2+1 n=n1+2n2+1
可知: n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1


性质4 具有n个结点的完全二叉树的深度为 ⌊ log ⁡ 2 n ⌋ + 1 \lfloor{\log_2{n}\rfloor}+1 log2n+1
设具有n个结点的二叉树为k层
则有:
n = 2 k − 1 − 1 + c , c = 1 , 2 , 3 , … … , 2 k − 1 n=2^{k-1}-1+c,c=1,2,3,……,2^{k-1} n=2k11+c,c=1,2,3,2k1
所以 ⌊ log ⁡ 2 n ⌋ = k − 1 \lfloor{\log_2{n}\rfloor}=k-1 log2n=k1,故可得上式。


性质5
按完全二叉树顺序从1开始将结点编号,则对于一个普通结点i:
(1)它的双亲的结点编号为 ⌊ i 2 ⌋ \lfloor{\frac{i}{2}}\rfloor 2i
(2)它的左孩子节点为2n,右孩子结点为2n+1;
显然还需要讨论特殊情况,即结点i是否为根节点,是否存在左孩子、右孩子


2.3 二叉树的存储结构

2.3.1 顺序存储结构
  • 默认按对应于完全二叉树的(编号-1)存储在相应位置(即编号中的根为1,但要存于A[0]中)
  • 一般的非完全二叉树的顺序结构中,无结点的位置可用指定常数或字符代替。
2.3.2 链式存储结构
  • 节点结构

     1. 二叉链表:数据域、左、右指针域
     2. 三叉链表:加双亲指针域
    
  • 链表的头指针指向根节点

  • 含有n个结点的二叉链有n+1个空链域

  • 结点实现

template <class T>
struct binaryTreeNode
{
	T element;
	binaryTreeNode<T> *lchild, *rchild;		//左右子树

	//重载的构造函数
	binaryTreeNode() {
		lchild = rchild = NULL;
	}
	binaryTreeNode(const T& theElement)
	{
		element(theElement);	//变量的初始化
		lchild = rchild = NULL;
	}
	binaryTreeNode(const T& theElement,binaryTreeNode* theLeftChild,binaryTreeNode* theRightChild) {
		element(theElement);
		lchild = theLeftChild;
		rchild = theRightChild;
	}
};
2.3.3 二叉树的遍历
  1. 前序遍历
template <class T>
void PreOrderTraverse(binaryTreeNode<T> *t)
{
	if (t != NULL)
	{
		visit(t);
		PreOrderTraverse(t->lchild);
		PreOrderTraverse(t->rchild);
	}
}

  1. 中序遍历
//中序遍历
template <class T>
void InOrderTraverse(binaryTreeNode<T> *t)
{
	if (t != NULL)
	{
		InOrderTraverse(t->lchild);
		visit(t);
		InOrderTraverse(t->rchild);
	}
}
  1. 后序遍历
template <class T>
void PostOrderTraverse(binaryTreeNode<T> *t)
{
	if (t != NULL)
	{
		PostOrderTraverse(t->lchild);
		PostOrderTraverse(t->rchild);
		visit(t);
	}
}
  • 层次遍历(需使用队列)
  • 栈实现深度优先遍历

  1. 先捋一下用栈实现二叉树的先序、中序、后序遍历问题

    - 先序(入栈时即输出)
    对于一个结点p:
    1.输出,入栈。判断左孩子是否为空
    2.若不为空,则p=pL。返1。
    3.若为空,栈顶退栈,且p=pr。判断p是否为空:
    4.若p为空,则p=栈顶右孩子,栈顶退栈。返4.5
    5.若p不为空,返1
    6.直到p=NULL且栈为空。(ps:要先把根退栈,再遍历该根的右子树)
    
while (p||(!isEmpty(S)))
{
	visit(p);
	S.Push(P);
	if(p->left!=NULL)
		{p=p->left;}
	else
		{
			p=S.GetTop()->right;
			S.Pop();
			while(p==NULL)
				{
					p=S.GetTop()->right;
					S.Pop();
				}//while
		}//else			
}//while

  1. 中序(出栈时输出)

    对于结点p:
    1.若左孩子不为空,则p入栈,p=p->pL;返1;
    2.若左孩子为空,输出p。p=pr,看p是否为空
    3.若p不为空,返1;
    4.若p为空,则出栈栈顶,输出栈顶。并将p=出栈.pr;返3,4;
    5.直到p为NULL且栈为空
    *结合**自己遍历**时的方法步骤翻译成编码语言!*
    
while(p||S/!S.isEmpty())
{
	if(p->left!=NULL)
	{S.Push(p);p=p->left;}
	else
	{
		visit(p);
		p=p->right;
		while(p==NULL)
		{
			q=S.Pop();
			visit(q);
			p=q->right;
		}//while
	}//else
}//while	
}

  • 后序

     对于任一节点P,
    
     1)先将节点P入栈;
    
     2)若P不存在左孩子和右孩子,或者P存在左孩子或右孩子,但左右孩子已经被输出,则可以直接输出节点P,并将其出栈,将出栈节点P标记为上一个输出的节点,再将此时的栈顶结点设为当前节点;
    
     3)若不满足2)中的条件,则将P的右孩子和左孩子依次入栈,当前节点重新置为栈顶结点,之后重复操作2);
    
     4)直到栈空,遍历结束。
    
while(!S.isEmpty())
{
	p=S.GetTop();
	if((p->pL==NULL && p->pR==NULL) ||   
            (pPre!=NULL && (p->pL==pPre || p->pRchild==p)))
    {
    	visit(p);
    	S.Pop();
		pPre=p;
	}
	else
	{
		if(p->pr!=NULL)
			S.Push(p->pr);
		if(p->pl!=NULL)
			S.Push(p->pl);//进栈:先右孩子再左孩子。保证退栈的时候从左至右至父,或直接左->父,)
	}
}
  • 后序遍历非递归算法(2)
    Loop:
    {
    if(BT 非空)
    {进栈;左一步;}//每次找到以bt为根的树的最左结点
    else
    {
    当栈顶指针所指结点的右子树不存在或已访问时,退栈并输出;否则右一步;}
    }//loop
do{
	while(root!=NULL)
	{
		//此处省略判断栈满的情况
		S.Push(root);
		root=root->lchild;
	}//while
	pre = NULL;//指向刚刚访问的结点。当上面的循环结束时,刚刚访问的结点root满足=NULL的条件,故pre=root=NULL;
	flag=1;//结点右子树不存在或已访问.当上个循环结束时,结点为NULL,不存在右节点。
	while(!S.isEmpty()&&flag)//栈非空且满足flag==1
	{
		root=S.GetTop()//将root置为栈顶元素
		if(root->rchild==NULL||pre=root->rchild)
		{
			visit(root);
			pre=S.Pop()
			root=S.GetTop()
		}//if
		else//否则将root置为新的树根节点,停止当前的退栈loop,进行新一轮大loop
		{
			root = root->rchild;
			flag=0;//终止小循环进入大循环
		}//else
	}//while

可见该后序遍历中栈存放的是从根节点到任意节点的路径。可利用性更强

2.3.4 二叉树的三叉链表表示
  • 存储结构
typedef struct TNode{
	TNode *lchild,*rchild,*parent;
	int data;
}TNode
  • 非递归遍历不用设置栈
2.3.5 应用题
  • 再记一下棋盘法还原二叉树的方法
    应用题:由中序+前序/后序序列还原二叉树
    棋盘投影法

  • 思考

       1. 如何判断满二叉树?
      	结点数&&层数的关系式
      
       2. 判断完全二叉树?
      	按层遍历,第一个null出现后后面再无非空子节点
    
       3. 求二叉树任意节点所在层数?
      	(1)非递归后序遍历法,找到节点后数栈的长度。
      	(2)两个队列来回倒,按层遍历+计数器
       4. 任意节点的所有祖先结点
      		(1)非递归的后序遍历
      		(2)的栈的使用
       5. 统计节点个数
      	随便来个遍历搞个计数器就行啊
       6. 将二叉树链表存储到按照完全二叉树存储的数组中,“*”表示空结点。
      get层数->结点数
      	对于所有节点,找到它的路径。据此通过“左孩子时双亲的二倍,右孩子时双亲的二倍+1”计算该点的位序(也可以递归)。扫描数组在为赋值的地方加空标记“*”之类的
    

2.4 线索二叉树

  • 知识点
    1. 保存遍历信息
    2. 在n个节点的二叉树左右链表示,有n+1个空链域。考虑利用起来:让其指向前驱/后继(加标记与左右孩子区分开)
    3. 规定:若结点有左子树,则lchild为左孩子指针,否则lchild指向它的前驱;rchild相同。
    4. 增加两个标志域(或用位运算存储在信息里)
    5. 在线索树上进行遍历,只需找到序列中的第一个节点,然后依次找到结点后继直至后继为空(p==T)。
    6. 线索二叉树中中序遍历,寻找一个结点的前驱:
      若结点左孩子非空,则其前驱为左子树的最右结点;否则为左指针作为线索指针直接指向其前驱。
    7. 时间复杂度与普通二叉树中序遍历相同O(n),但常数因子较小且不需要设栈。

另:前驱:$p,后继:p$

  • 线索二叉树的结点类型
struct Node
{
	struct Node lchild,rchild;
	ElementType data;
	int ltag,rtag;//定义:1指向孩子,0指向前驱后继
};//但是考虑到多用了2n个标志位,可以有把标值与data进行位运算,读数据/读标记的时候进行数据的截取。
  • 线索树的另设头结点
  1. 设:
    head->lchild=T(二叉树的根)
    head->rchild=head(指向自己)
    head->ltag=1;(代表左节点为孩子而不是前驱后继)
    head-> rchild=1;

  2. 当线索树为空时:
    head->lchild=head;//左节点指向自己的意思时前驱为自己
    head->rchild-head;
    head->ltag=0;
    head->rtag=1;

  3. 作用:使最后一个节点的后继是“head的右子树的最左结点”,即头元素。第一个结点的前驱是“head的左子树的最后一个结点”,即尾元素,实现循环链表一样的作用。

  • 在线索二叉树上求中序的后继前驱
  1. 前驱$p
    对于结点p:
    (1)当p->ltag=0,p->lchild即为所求
    (2)当p->ltag=1时,$p是p的左子树的最右结点
    (3)树的最左节点的前驱指向head

    故据此递归:
THTREE InPre(THETREE p)
{
	THTREE Q;
	Q=p->lchild;//Q为右子树的根
	if(p->ltag==1)//左子树不为空,执行一下,否则Q直接就是p的前驱就可以直接返回了
	{
		while(Q->rtag==1)//当Q结点有右子树
		{
			Q=Q->rchild;
		}//while,找到最右结点
	}//if
	return Q;//返回的是p的前驱
}
  1. 后继p$
    同前驱的求法。后继是右子树的最左结点。
    树的最右结点的后继指向head
THTREE InNext(THTREE p)
{
	//balabalalalalalala
}
  • 利用InNext(),中序遍历线索二叉树
void THInOrder(THTREE HEAD)
{
	THTREE temp=HEAD;
	do{
		temp=InNext(temp);//注意,最开始时temp指向head,其“右子树的最左节点”因“右子树为head本身”而返回的是原树的最左节点,当树为空时,返回的是head本身
		if(temp!=HEAD)
			visit(temp);
  	}while(temp!=HEAD);//最右结点的后继是HEAD
}//THInOrder

同时还可以根据(后继是右子树的最左节点)而非递归非栈的中序遍历线索二叉树

  • 线索二叉树的删除与插入
    与普通树相比,要多考虑线索的修正
    例如:将R插入作为S的右孩子结点
    (1)右孩子为空
    (2)非空,则R插入后,原来的S的右子树作为R的右子树
void Rinsert(THTREE S,THTREE R)//S下面插R
{
	THTREE w;
	R->rchild=S-rchild;
	R->rtag=S->rtag;
	S->rchild=R;
	R->lchild=S;
	R->ltag=0;
	S->rtag=1;
	if(R->rtag=1)//说明右节点有孩子,其后继不为rchild,以它为前驱的结点的前驱没有改变,所以需要找到R的后继,让它的前驱指向R(本来是指向S的!!!)
	{
		w=InNext(R);
		w->lchild=R;
	}
}//Rinsert
  • 树的线索化
    中序线索化:

      1. 申请头结点空间,按定义初始化头结点,pre设为head;
      2. 当头结点的左孩子存在时,对左孩子进行中序线索化(递归函数)(第一层递归时便将头元素的前驱设为pre(即head了)
      3. 中序线索化后,pre指向树的最后一个结点,此时再令其指向head(最后一个节点线索化
      4. 完成!!!
    

    线索化函数:
    pre是一个全局变量,指向上一个访问的结点。只需T为根一个参数,void不返回参数。
    递归的新思路
    线索的实质是将空指针改为指向前驱或后继的线索,而前驱或后继的信息只有在遍历的时候才能找到,因此线索化的过程就是在遍历的过程中修改空指针的过程。所以要全局变量设一个pre来始终记录遍历过程中访问的当前节点的上一个刚刚访问过的结点。

    1. 若T不为空,则先将左孩子线索化,然后再考虑:如果p的左孩子为空,则p的左孩子指向pre,若pre的右孩子为空,则将pre的右孩子指向p;然后pre=p,再对右子树进行线索化。

    2. 若T为空,则无操作。

      ThNode *pre=NULL;//全局变量
      Status InOrderThreading(THTREE &Thrt,THTREE T)
      {
      	if(!(Thrt=(THTREE)malloc(sizeof(ThNode))))exit(OVERFLOW);//头结点,注意此处不是申请了一个指针。
      	//初始化头结点
      	Thrt->lchild=T;
      	Thrt->rchild=Thrt;
      	Thrt->rtag=1;
      	Thrt->ltag=1;
      	if(!T)//如果T为空
      	{
      		Thrt->lchild=Thrt;
      		Thrt->ltag=0;
      	}
      	InThread(T);
      	pre->rchild=Thrt;
      	pre->rtag=0;//最后结点的线索化
      }//InOrderThreading
      
      InThread(THTREE p)
      {
      	if(p)//若p非空
      	{
      		InThread(p->lchild);//当不考虑左右结点是否为空时就对其进行线索化,需与本函数中的“若非空则没有操作”对应
      
      		//中间这部分相当于后序遍历的visit()
      		if(!p->lchild)//若为空
      		{
      			p->lchild=pre;
      			p->ltag=0;
      		}
      		if(!pre->rchild)//若为空
      		{
      			pre->lchild=p;
      			pre->ltag=0;
      		}
      		pre=p;
      		//注意注意其与中序遍历相同的结构使pre始终指向该访问节点的中序遍历的前驱!!!
      		InThread(p->rchild);
      	}//if
      }//InThread,考虑其具有与递归中序遍历相同的结构及作用,只不过在抓到一些空链域时对其进行线索化的操作
      

      后序线索化:
      结点x的后继是它的双亲的右孩子的后序遍历列出来的第一个结点(不考虑其他的了),需要知道双亲,故每个节点要设置成带标志域的三叉链表。

  • 二叉树的判断是否相等

    • 定义:
      相似:都是空的或结构相同
      等价:相似且节点包含信息相同

      int Equal(firstbt,secondbt)
      {
      	int x=0;//相等返回1,否则返回零
      	if(IsEmpty(firstbt)&&IsEmpty(secondbt))
      		x=1;
      	else if(!IsEmpty(firstbt)&&!IsEmpty(secondbt))
      	{
      		if(Data(firstbt)==Data(secondbt))//该节点值相等
      			if(Equal(Lchild(firstbt),Lchild(secondbt)))//左子树相等
      				x = Equal(Rchild(firstbt),Rchild(secondbt));//右子树相等
      	}
      	return x;
      }//Equal
      
  • 二叉树的复制(又又又是递归调用)

    BTREE Copy(BTREE oldtree)
    {
    	BTREE temp;
    	if(oldtree!=NULL)
    	{
    		temp=new Node;
    		temp->lchild=Copy(oldtree->lchild);//注意是copy好的子树
    		temp->rchild=Copy(oldtree->rchild);
    		temp->data=oldtree->data;
    		return temp;//返回复制好的一个结点
    	}//if
    	return NULL;
    }//Copy
    
  • 二叉树的镜像复制

2.5 二叉树的应用

2.5.1 堆
  • 最大堆、最小堆(完全二叉树,双亲与孩子的大小关系确定)
    类型定义

    typedef struct{
    	int key;
    	//其它
    }ElementType;
    typedef struct{
    	ElementType elements[MaxSize];
    	int n;//元素个数计数器
    }HEAP;
    

    插入、删除操作
    数据的存储方式还是数组的形式,只是用完全二叉树的思维去考虑。

    • 插入:
      先将元素放于数组末尾,逻辑上其双亲是第(n+1)/2个结点。
      与双亲比较,若满足条件,则交换位置。重复……

    • 删除:
      一般都是删除根
      先将最后一个结点与第一个结点的数值交换位置,置n=n-1,然后第一个节点与其逻辑上的孩子比较,选择合适的孩子进行交换((注意终止条件包括孩子是否位序是否小于n)

2.5.2 选择树

选择树是二叉树,每一个结点都代表该结点两个儿子中的较小(大)者。这样树根结点就表示树中的最小(大)元素结点
于是乎,把他放到归并排序上面,可以优化比较次数,每次只需比较层数次便可找出最值取出

2.6 树和森林

  • 树的三种遍历
    普通树中,将最左结点当作左孩子,其余结点一起当作右孩子,根在1,2之间,分为先序、中序、后序。

  • 树的存储结构

  1. 树的双亲表示法(数组实现的方法,要求连续空间)
  2. 树的孩子表示法(孩子用动态链表接在双亲结点的后面,双亲结点排为顺序存储结构)
  3. 树的孩子兄弟表示法(即开头定义)
  • 森林与二叉树的转换
  • 树和森林的遍历
    孩子兄弟表示法的树的先根遍历和后根遍历可以借助二叉树的先序遍历和中序遍历实现

一类称球问题解法
赫夫曼树和编码

2.7 树与等价问题

2.8 赫夫曼树及其应用

2.9 回溯法与树的遍历

赫夫曼树和编码

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值