第五章 树与二叉树 学习总结

一、树的逻辑结构
树的定义:

树:n(n≥0)个结点的有限集合。
当n=0时,称为空树;
任意一棵非空树满足以下条件:
⑴ 有且仅有一个特定的称为根的结点;
⑵ 当n>1时,除根结点之外的其余结点被分成m(m>0)个互不相交的有限集合T1,T2,… ,Tm,其中每个集合又是一棵树,并称为这个根结点的子树

树的定义可采用递归方法或非递归方法

树的基本术语:

①结点的度:结点所拥有的子树的个数。
②树的度:树中各结点度的最大值。
③叶子结点:度为0的结点,也称为终端结点。
④分支结点:度不为0的结点,也称为非终端结点。
⑤孩子、双亲:树中某结点子树的根结点称为这个结点的孩子结点,这个结点称为它孩子结点的双亲结点;
⑥兄弟:具有同一个双亲的孩子结点互称为兄弟。
⑦路径:如果树的结点序列n1, n2, …, nk有如下关系:结点ni是ni+1的双亲(1<=i<k),则把n1, n2, …, nk称为一条由n1至nk的路径;路径上经过的边的个数称为路径长度。
⑧祖先、子孙:在树中,如果有一条路径从结点x到结点y,那么x就称为y的祖先,而y称为x的子孙。
⑨结点所在层数:根结点的层数为1;对其余任何结点,若某结点在第k层,则其孩子结点在第k+1层。
⑩树的深度:树中所有结点的最大层数,也称高度。

树的功能的实现:

树的遍历:从根结点出发,按照某种次序访问树中所有结点,使得每个结点被访问一次且仅被访问一次。
可划分为:前序遍历、后续遍历、层序遍历。

前序遍历:
若树为空,不进行遍历;否则
⑴ 访问根结点;
⑵ 按照从左到右的顺序前序遍历根结点的每一棵子树。

后续遍历
若树为空,则遍历结束;否则
⑴ 按照从左到右的顺序后序遍历根结点的每一棵子树;
⑵ 访问根结点。

层序遍历
从树的第一层(即根结点)开始,自上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。

二、树的存储结构
双亲表示法

基本思想:
用一维数组来存储树的各个结点(一般按层序存储),
数组中的一个元素对应树中的一个结点,
每个结点记录两类信息:结点的数据信息以及该结点的双亲在数组中的下标。

dataparent

data:存储树中结点的数据信息
parent:存储该结点的双亲在数组中的下标

双亲表示法中结点数据类型的定义:

template <class T>
struct PNode{
     T data;          //数据域
     int parent;   //指针域,双亲在数组中的下标
} ;

可定义根节点的parent为“-1”,并依次定义接下来的数组。
还可增添“firstchild”或“rightsib”从而可以更方便的查找孩子节点、兄弟节点的位置及数据。

孩子表示法-多重链表表示法(节点中的指针域表示孩子)

实现:链表中的每个结点包括一个数据域和多个指针域,每个指针域指向该结点的一个孩子结点。

方案一:指针域的个数等于树的度

datachild1child2childd

data:数据域,存放该结点的数据信息;
child1~childd:指针域,指向该结点的孩子。
缺点:浪费空间
方案二:指针域的个数等于该结点的度

datadegreechild1child2child3childd

data:数据域,存放该结点的数据信息;
degree:度域,存放该结点的度;
child1~childd:指针域,指向该结点的孩子。
缺点:结点结构不一致

孩子表示法-孩子链表表示法(每个节点创建一个单链表)

把每个结点的孩子排列起来,看成是一个线性表,且以单链表存储,则n个结点共有 n 个孩子链表。
这 n 个单链表共有 n 个头指针,这 n 个头指针又组成了一个线性表。
为了便于进行查找采用顺序存储存储每个链表的头指针。
最后,将存放 n 个头指针的数组和存放n个结点的数组结合起来,构成孩子链表的表头数组。
孩子链表表示法的实现:
孩子节点

struct CTNode
{   
     int child;
     CTNode *next;
};

表头节点

template <class T>
struct CBNode
{     
    T data;
    CTNode *firstchild;  
};

孩子兄弟表示法
firstchilddatarightsib

data:数据域,存储该结点的数据信息;
firstchild:指针域,指向该结点第一个孩子;
rightsib:指针域,指向该结点的右兄弟结点。

template   <class T>
struct TNode{
     T data;
     TNode <T> *firstchild, *rightsib;
};

树的存储结构小结

顺序存储(本质上是静态指针):
双亲表示法
双亲、孩子表示法
链式存储:
多重链表示法
孩子链表表示法
孩子兄弟表示法

二叉树的逻辑结构

定义:二叉树是n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
二叉树的特点:
⑴ 每个结点最多有两棵子树;
⑵ 二叉树是有序的,其次序不能任意颠倒。
二叉树的基本性质
1、二叉树的第i层上最多有2i-1个结点(i≥1)。
2、一棵深度为k的二叉树中,最多有2k-1个结点,最少有k个结点。
3、在一棵二叉树中,如果叶子结点数为n0,度为2的结点数为n2,则有: n0=n2+1。
4 、具有n个结点的完全二叉树的深度为 log2n +1。
5 、对一棵具有n个结点的完全二叉树中从1开始按层序编号,则对于任意的序号为i(1≤i≤n)的结点(简称为结点i),有:
(1)如果i>1,
则结点i的双亲结点的序号为 i/2;如果i=1,
则结点i是根结点,无双亲结点。
(2)如果2i≤n,
则结点i的左孩子的序号为2i;
如果2i>n,则结点i无左孩子。
(3)如果2i+1≤n,
则结点i的右孩子的序号为2i+1;如果2i+1>n,则结点 i无右孩子。

二叉树的遍历操作

前序遍历
中序遍历
后序遍历
层序遍历

前序(根)遍历
若二叉树为空,则空操作返回;否则:
①访问根结点;
②前序遍历根结点的左子树;
③前序遍历根结点的右子树
中序(根)遍历
若二叉树为空,则空操作返回;否则:
①中序遍历根结点的左子树;
②访问根结点;
③中序遍历根结点的右子树。
后序(根)遍历
若二叉树为空,则空操作返回;否则:
①后序遍历根结点的左子树;
②后序遍历根结点的右子树。
③访问根结点;
层序遍历
二叉树的层次遍历是指从二叉树的第一层(即根结点)开始,从上至下逐层遍历,在同一层中,则按从左到右的顺序对结点逐个访问。

二叉树的存储结构及实现

1、按照完全二叉树编号
2、以编号为下标
3、空的地方赋空
前序存储:

void Preorder(int root, char data[]){
	if(data[root]!='\0'){
		cout<<data[root] ;			
		Preorder(2*root,data);
		Preorder(2*root+1,data);

	}
	return;
}

中序存储:

void InOrder(int root, char data[]){
	if(data[root]!='\0'){
		InOrder(2*root,data);
		cout<<data[root] ;			
		 InOrder(2*root+1,data);	
	}
	return;
}

后续存储:

void PostOrder(int root, char data[]){
	if(data[root]!='\0'){
		 PostOrder(2*root,data);
		 PostOrder(2*root+1,data);
		cout<<data[root] ;			
	}
	return;
}
二叉链表

基本思想:令二叉树的每个结点对应一个链表结点,链表结点除了存放与二叉树结点有关的数据信息外,还要设置指示左右孩子的指针。

lchilddatarchild

data:数据域,存放该结点的数据信息;
lchild:左指针域,存放指向左孩子的指针;
rchild:右指针域,存放指向右孩子的指针。
实现:

template <class T>
struct BiNode
{
    T data;
    BiNode<T> *lchild, *rchild;
};

二叉链表类的声明:

template <class T>
class BiTree
{    
  public:
       BiTree(); 
        ~BiTree( );            
        void PreOrder(){PreOrder(root);} 
        void InOrder() {InOrder(root);} 
        void PostOrder() {PostOrder(root);} 
        void LevelOrder(){LeverOrder(root)};
  private:
        BiNode<T> *root; 
        BiNode<T> * Creat( ); 
        void Release(BiNode<T> *root);
        void PreOrder(BiNode<T> *root); 
        void InOrder(BiNode<T> *root); 
        void PostOrder(BiNode<T> *root); 
        void LevelOrder(BiNode<T> *root);

具体算法实现:
前序遍历:
1、前序遍历——递归算法

template<class T>
void BiTree::PreOrder(BiNode<T> *root) 
{
        if (root ==NULL)  return;     
        else {
            cout<<root->data;         
            PreOrder(root->lchild);    
            PreOrder(root->rchild);    
        }
 }

2、前序遍历——非递归算法
思想:
遇到一个结点,就访问该结点,并把此结点推入栈中,然后遍历它的左子树;
遍历完它的左子树后,从栈顶托出这个结点,并按照它的右链接指示的地址再去遍历该结点的右子树结构。
伪代码:
1.栈s初始化(空栈);
2.循环直到root为空且栈s为空
 2.1 当root不空时循环
  2.1.1 输出root->data;
 2.1.2 将指针root的值保存到栈中;
 2.1.3 继续遍历root的左子树(root=root->lchild)
 2.2 如果栈s不空,则
  2.2.1 将栈顶元素弹出至root(root=s.pop());
  2.2.2 准备遍历root的右子树(root=root->rchild);
  
代码实现:

template <class T>
void BiTree::PreOrder(BiNode<T> *root) {
  SeqStack<BiNode<T> *>  s;
     while (root!=NULL | | !s.empty()) {
         while (root!= NULL)  {
             cout<<root->data;
             s.push(root);
             root=root->lchild;  
         }
         if (!s.empty()) { 
             root=s.pop();
             root=root->rchild;  
         }
     }
}

3、前序遍历建立二叉树
思想:为了建立一棵二叉树,将二叉树中每个结点的空指针引出一个虚结点,其值为一特定值如“#”,以标识其为空,把这样处理后的二叉树称为原二叉树的扩展二叉树。
代码实现:

template <class T>
BiTree ::BiTree(){ 
      root=creat()}

template <class T>
BiNode<T> * BiTree ::Creat(){
     BiNode<T> *root; char ch;
    cin>>ch;
    if (ch=='# ')     root=NULL; 
    else {
        root=new BiNode<T>; 
        root->data=ch;
        root->lchild=creat(); 
        root->rchild= creat(); 
    }  
  return root
}

中序遍历
1、中序遍历——递归算法
template
void BiTree::InOrder (BiNode *root)
{
if (root==NULL) return;
else {
InOrder(root->lchild);
cout<data;
InOrder(root->rchild);
}
}
2、中序遍历——非递归算法

template <class T>
void BiTree::InOrderwithoutD (BiNode<T> *root)
	 {
     	stack<BiNode<T>*> aStack;
     	while(!aStack.empty()||root) {
     	while(root)
     	{
           aStack.push(root);
           root=root->lchild;
        }
          if(!aStack.empty()){
		      root=aStack.top();				
		      aStack.pop(); 
              cout<<root->data;
              root=root->rchild; 
	   }
  }

后序遍历
1、后序遍历——递归算法

template <class T>
void BiTree::PostOrder(BiNode<T> *root)
{ 
    if (root==NULL) return; 
    else {
         PostOrder(root->lchild); 
         PostOrder(root->rchild); 
         cout<<root->data;          
    }
}

2、后序遍历——非递归算法
算法分析
1.定义一个栈;从根节点出发开始遍历,p=root,如果,root==NULL, 不进行遍历;
2.无条件进行下面的工作
①如果指针不空,指针打上left标记,并将指针进栈,执行②;否则,执行③
②p=p->lchild,重复①
③栈顶元素出栈P
④查看P的标志,如果标志为right,进行下面的工作,否则,执行⑤
1访问当前节点P。
2如果栈空 ,算法结束;
3否则,栈顶元素出栈,转④。
⑤修改P的标志,让P重新入栈,p=P->rchild,执行②

栈中的元素类型定义StackElement

enum Tags{Left,Right};	//特征标识定义
	template <class T>
	class StackElement		//栈元素的定义
	{
	public:
	BiTreeNode<T>* pointer;     //指向二叉树结点的指针
	Tags tag; //特征标识申明
	};	

实现:

#include <stack>
Using namespace std;
template<class T>
void BiTree<T>::PostOrderWithoutRecusion(BiTreeNode<T>* root){
	StackElement<T> element;
	stack<StackElement<T > > aStack;//栈申明
	BiTreeNode<T>* pointer;
	if(root==NULL)
		return;//空树即返回
	else    pointer=root;				
	while(true){
	  while(pointer!=NULL){//进入左子树
		element.pointer=pointer;
		element.tag=Left; //沿左子树方向向下周游
		aStack.push(element);
		pointer=pointer->lchild; 	
		}
   		element=aStack.pop();
            pointer=element.pointer; 
            while(element.tag==Right){
        cout<<pointer->data;
        if(aStack.empty())  return;
	    else{
	       element=aStack.pop();
		     pointer=element.pointer;
	  	 }//end else
        } //endwhile
    element.tag=Right; 
    aStack.push(element);
    pointer=pointer->rchild(); 
     }//end while



层序遍历
思想:
1.队列Q初始化;
2.如果二叉树非空,将根指针入队;
3.循环直到队列Q为空
3.1 q=队列Q的队头元素出队;
3.2 访问结点q的数据域;
3.3 若结点q存在左孩子,则将左孩子指针入队;
3.4 若结点q存在右孩子,则将右孩子指针入队;

#include <queue>
using namespace std;
template<class T>
void BiTree<T>::LevelOrder(BinaryTreeNode<T>* root){
	queue<BiTreeNode<T>*> aQueue;
	if(root)
		aQueue.push(root);
	while(!aQueue.empty())
	{
		root=aQueue.front(); //取队列首结点
	 	aQueue.pop();
                     cout<<pointer->data;//访问当前结点
		if(root->lchild)	//左子树进队列
			aQueue.push(root->lchild);
		if(root->rchild) //右子树进队列
			aQueue.push(root->rchild); 	
	}//end while
}

二叉树的析构

template<class T>
void BiTree<T>::Release(BiNode<T>* root){
  if (root != NULL){                  
      Release(root->lchild);   //释放左子树
      Release(root->rchild);   //释放右子树
      delete root;
  }  
}
template<class T>
BiTree<T>::~BiTree(void)
{
	Release(root);
}

二叉树的结点个数

void Count(BiNode *root){
    if (root) {
         Count(root->lchild);
         number+ +;  //number为数据成员
         Count(root->rchild);
   }
}

树中节点的数目

template<class T>
int BiTree<T>::count(BiNode<T>* root){
	int number=0;

	if (root==NULL)
		number=0;
	else
		number=count(root->lchild)+count(root->rchild)+1;
	return number;
}

统计叶子节点的数目

template<typename T> 
void BiTree<T>:: countleaf(BiTreeNode<T> * root){
	if (root) {
		if (root->lchild==NULL && root->rchild==NULL)
			leafcount=leafcount+1;
		else
		{
			countleaf(root->lchild);
			countleaf(root->rchild);
		}
	}
	return;
}

统计叶子节点的数目

template<class T>
int BiTree<T>::leafcount(BiNode<T>* root){
	int number=0;

	if (root==NULL)
		number=0;
	else if(root->lchild==NULL && root->rchild==NULL)
		number=1;
	else
	    number=leafcount(root->lchild)+leafcount(root->rchild);
	return number;
}

计算树的高度

template<typename T> 
 int BiTree<T>::cal_height(BiTreeNode<T> * root){
	int lheight=0,rheight=0;
	if (root==0)  	 return 0;
	lheight=cal_height(root->lchild);
	rheight=cal_height(root->rchild);
	if (lheight>rheight)	return lheight+1;
	else 		return rheight+1;
}

最优二叉树-哈夫曼树及哈夫曼编码

相关概念
叶子结点的权值:对叶子结点赋予的一个有意义的数值量。
二叉树的带权路径长度:设二叉树具有n个带权值的叶子结点,从根结点到各个叶子结点的路径长度与相应叶子结点权值的乘积之和。 记为:
WPL=w1l1+w2l2+w3l3+…+wn+ln
wk:从根结点到第k个叶子的路径长度
lk:第k个叶子的权值
哈夫曼树:给定一组具有确定权值的叶子结点,带权路径长度最小的二叉树。
哈夫曼树的特点:
1.权值越大的叶子结点越靠近根结点,而权值越小的叶子结点越远离根结点。
2.只有度为0(叶子结点)和度为2(分支结点)的结点,不存在度为1的结点。
哈夫曼算法基本思想:
⑴ 初始化:由给定的n个权值{w1,w2,…,wn}构造n棵只有一个根结点的二叉树,从而得到一个二叉树集合F={T1,T2,…,Tn};
⑵ 选取与合并:在F中选取根结点的权值最小的两棵二叉树分别作为左、右子树构造一棵新的二叉树,这棵新二叉树的根结点的权值为其左、右子树根结点的权值之和;
⑶ 删除与加入:在F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到F中;
⑷ 重复⑵、⑶两步,当集合F中只剩下一棵二叉树时,这棵二叉树便是哈夫曼树。
哈夫曼算法的存储结构
设置一个数组huffTree[2n-1]保存哈夫曼树中各点的信息,数组元素的结点结构 。

weightlchildrchildparent

weight:权值域,保存该结点的权值;
lchild:指针域,结点的左孩子结点在数组中的下标;
rchild:指针域,结点的右孩子结点在数组中的下标;
parent:指针域,该结点的双亲结点在数组中的下标。

struct element
{     int weight;
      int lchild, rchild, parent;
};

伪代码:

  1. 数组huffTree初始化,所有元素结点的双亲、左
    右孩子都置为-1;
  2. 数组huffTree的前n个元素的权值置给定值w[n];
  3. 进行n-1次合并
    3.1 在二叉树集合中选取两个权值最小的根结点,
    其下标分别为i1, i2;
    3.2 将二叉树i1、i2合并为一棵新的二叉树k(初值为n;依次递增);
void HuffmanTree(element huffTree[ ], int w[ ], int n ) {
    for (i=0; i<2*n-1; i++) {
       huffTree [i].parent= -1;
       huffTree [i].lchild= -1;
       huffTree [i].rchild= -1;   
    }
    for (i=0; i<n; i++) 
       huffTree [i].weight=w[i];
    for (k=n; k<2*n-1; k++) {
        Select(huffTree, &i1, &i2); 
        huffTree[k].weight=huffTree[i1].weight+huffTree[i2].weight;
        huffTree[i1].parent=k;     
        huffTree[i2].parent=k; 
        huffTree[k].lchild=i1;    
        huffTree[k].rchild=i2;
    }
}

哈夫曼树应用——哈夫曼编码
编码:给每一个对象标记一个二进制位串来表示一组对象。
例:ASCII,指令系统
等长编码:表示一组对象的二进制位串的长度相等。
不等长编码:表示一组对象的二进制位串的长度不相等。

前缀编码:一组编码中任一编码都不是其它任何一个编码的前缀 。
前缀编码保证了在解码时不会有多种可能。

线索链表

线索:将二叉链表中的空指针域指向前驱结点和后继结点的指针被称为线索;
线索化:使二叉链表中结点的空链域存放其前驱或后继信息的过程称为线索化;
线索二叉树:加上线索的二叉树称为线索二叉树。

结点结构:

ltaglchilddatarchildrtag

ltag=0:lchild指向该结点的左孩子
ltag=1:lchild指向该结点的前驱结点
rtag=0:rchild指向该结点的右孩子
rtag=1:rchild指向该结点的后继结点

enum flag {Child, Thread}; 
template  <class T>
struct ThrNode
{
     T data;
     ThrNode<T>  *lchild, *rchild;
     flag ltag, rtag;
};

二叉树的遍历方式有4种,故有4种意义下的前驱和后继,相应的有4种线索二叉树:
⑴ 前序线索二叉树
⑵ 中序线索二叉树
⑶ 后序线索二叉树
⑷ 层序线索二叉树
线索化规则
书上也是奇奇怪怪的,其实很简单,就按照某一遍历规则,记录当前节点(Cur),上一次访问的节点(Pre)
①如果当前节点Cur->lchild=NULL(左孩子节点空),则Cur->lchild=Pre;Cur->Ltag=1(左指针域指向前驱节点(前一个节点Pre),修改Ltag为线索模式。
②如果前一节点Pre->rchild=NULL(前节点的右孩子节点空),则Pre->rchild=Cur;Pre->Rtag=1(前节点的右指针域指向后继节点(也就是当前节点),修改Rtag为线索模式)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值