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

5.1 树

5.1.1 树的定义

(tree)是n(n≥0)个结点的有限集T。如果n=0,则称空树;如果n>0(非空树),则:

有且仅有一个特定的结点,称为(root) ;

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

5.1.2 树的特点

非空树中至少有一个结点——,树中各子树是互不相交的集合。

5.2 二叉树

5.2.1 二叉树的定义

二叉树是n(n≥0)个结点的有限集,它或为空树(n=0),或由一个根结点和两棵分别称为左子树右子树的互不相交的二叉树构成。

每个结点至多有二棵子树(即不存在度大于2的结点)。二叉树的子树有左、右之分,且其次序不能任意颠倒

5.2.2 二叉树的性质

(1)在二叉树的第 i 层上至多有2 i-1 个结点(i≥1) 。

(2)深度为 k 的二叉树上至多含 2 k-1 (k≥1)个结点。

(3)任何一棵二叉树,若叶子结点个数为n0 ,度为2的结点数为n2 ,则必存在关系式:n0 = n2+1。

(4)具有 n 个结点的完全二叉树的深度为 [log2n]+1。

(5)若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点:有
① 若 i = 1,则该结点i是二叉树的根,无双亲;如果i > 1,编号为 [i/2] 的结点为其双亲结点。

若 2i > n,则该结点无左孩子,如果2i < n,编号为 2i 的结点为其左孩子结点。

若 2i+1>n,则该结点无右孩子,如果2i+1 < n,编号为2i+1 的结点为其右孩子结点。

若i为奇数且不为1,则结点i的左兄弟是i - 1;否则无左兄弟。

⑤== 若i为偶数且小于n,则结点i的右兄弟是i + 1;否则无右兄弟。==

5.2.3 满二叉树和完全二叉树

(1)满二叉树

定义:指的是深度为k且含有2 (k-1)个结点的二叉树。

特点:每一层上的结点数都是最大结点数。

2)完全二叉树

定义:深度为k,有n个结点的二叉树当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称为完全二叉树。

特点:叶子结点只可能在层次最大的两层上出现。

对任一结点,若其右分支下子孙的最大层次为L,则其左分支下子孙的最大层次必为L 或L+1

5.3 二叉树的存储

5.3.1 顺序存储

按满二叉树的结点层次编号,依次存放二叉树中的数据元素。结点间关系蕴含在其存储位置中,但是浪费空间,适于存满二叉树和完全二叉树。

5.3.2 链式存储

对于任意的二叉树来说,每个结点只有两个孩子,一个双亲结点。可以设计每个结点至少包括三个域:数据域、左孩子域和右孩子域。

typedef	struct	node{    
	datatype data;
	struct node *lchild, *rchild;
}bitree;

若一个二叉树含有 n 个结点,则它的二叉链表中必含有 2n个指针域,其中必有 n+1 个空的链域。 有时,为了便于找到双亲结点,可以增加一个Parent域,以指向该结点的双亲结点。采用这种结点结构的存放方式称做二叉树的三叉链表存储结构。

5.4 遍历二叉树

遍历一棵非空二叉树,可分解为:访问根结点 D;遍历左子树 L;遍历右子树 R。因此有三种遍历方法,分别是:
先序(根)遍历(DLR)
中序(根)遍历(LDR)
后序(根)遍历(LRD)

设二叉树有n个结点,对每个结点都要进行一次入栈和出栈的操作,即入栈和出栈各执行n次,对结点的访问也是n次。这些二叉树递归遍历算法的时间复杂度为O(n)

5.4.1 先序遍历

递归

若二叉树为空,则空操作,否则依次执行如下 3 个操作:
① 访问根结点;
② 按先序遍历左子树;
③ 按先序遍历右子树。

void preorder(bitree *t) {  
	if(t!=NULL) { 
		cout>>t->data);
		preorder(t->lchild);
		preorder(t->rchild);
	}
}

非递归

沿着左分支访问,直到左分支为空时,再依次对栈中节点的右分支进行处理。(遵循从左至右的遍历原则,体现深度优先搜索的思想):
每个节点只进栈一次,在进栈前访问节点。

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)

5.4.2 中序遍历

递归

若二叉树为空,则空操作,否则依次执行如下 3 个操作:
① 按中序遍历左子树;
② 访问根结点;
③ 按中序遍历右子树。

void preorder(bitree *t) {  
	if(t!=NULL) { 
		preorder(t->lchild);
		cout>>t->data);
		preorder(t->rchild);
   	}
}

非递归

  1. 栈s初始化(空栈);
  2. 循环直到root为空且栈s为空
    2.1 当root不空时循环
    2.1.1 将指针root的值保存到栈中;
    2.1.2 继续遍历root的左子树(root=root->lchild)
    2.2 如果栈s不空,则
       2.2.1 将栈顶元素弹出至root(root=s.pop());
       2.2.2 输出root->data;
       2.2.3 准备遍历root的右子树(root=root->rchild);
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; 
	   }
  }
} 

5.4.3 后序遍历

递归

若二叉树为空,则空操作,否则依次执行如下 3 个操作:
① 按后序遍历左子树;
② 按后序遍历右子树;
③ 访问根结点。

void preorder(bitree *t) {  
	if(t!=NULL) { 
      	preorder(t->lchild);
		preorder(t->rchild); 
		cout>>t->data);
   	}
}

非递归

算法分析

  1. 定义一个栈;从根节点出发开始遍历,p=root,如果,root==NULL, 不进行遍历;
  2. 无条件进行下面的工作
    ① 如果指针不空,指针打上left标记,并将指针进栈,执行②;否则,执行③
    ②p=p->lchild,重复①
    ③栈顶元素出栈P
    ④查看P的标志,如果标志为right,进行下面的工作,否则,执行⑤
    a)访问当前节点P
    b)如果栈空 ,算法结束;
    c)否则,栈顶元素出栈,转④
    ⑤修改P的标志,让P重新入栈,p=P->rchild,执行2
#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
}

5.4.4 层序遍历

队列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);
}

5.5 二叉树算法设计练习

5.5.1 设计算法求二叉树的结点个数

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;//左子树中节点的数目+右子树中节点的数目+1

	return number;
}

统计叶子节点的数目

增加一个数据成员,leafcount, 初值为0
对树进行遍历。 如果一个节点是叶子,则将leafcount+1
可以在前序、中序或后序的遍历过程中进行计算。
算法分析
从根节点出发
判断当前节点是否是叶子节点,如果是,则叶子数+1
否则,在左子树中遍历,并统计叶子节点的数目,在右子树中进行遍历,并统计叶子节点的数目

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;
}

5.5.2 计算树的高度

高度的定义
max(左子树高度,右子树高度)+1

算法分析
从根节点出发开始计算,
如果root= =NULL, 高度为0
否则,分别计算左子树的高度;右子树的高度;返回max(左子树高度,右子树高度)+1
递归的定义

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;
}

输出中缀表达式。并加上相应的括号
(a+(b*(c-d)))-(e/f)

基本思想
中序遍历。
中序遍历左子树前,输出左括号
中序遍历右子树后,输出右括号
如果遍历叶子结点的左右子树,不输出括号
如果遍历根节点的左右子树,不输出括号(否则,会得到形如(a+b)的表达式)

void BiTree<T>::In_Expression(BiNode<T>* root){
	if(root)
	{
  	   if(root!=this->root&&root->lchild!=0&&root->rchild!=0)
   		         cout<<"(";
	   In_Expression(root->lchild);
	   cout<<root->data;
   	   In_Expression(root->rchild);
 	   if(root!=this->root&&root->lchild!=0&&root->rchild!=0)
			cout<<")";
	}	
}

5.5.3 二叉树逆时针旋转90后输出

按照从右向左的顺序,中序遍历每行输出一个结 按照结点的层次,进行缩进。

template <class T>
void BiTree<T>::Left_Rotate(BiNode<T>* root,int level){
	if(root){
		Left_Rotate(root->rchild, level+1);
		for(int i=0;i<level;i++)
			cout<<"\t";
		cout<<root->data<<endl;
		Left_Rotate(root->lchild, level+1);
	}
}

5.5.4 计算树的的宽度

所谓宽度是指在二叉树的各层上,具有结点数最多的那一层上的结点总数

struct q_element{	BiNode * root;		int level;};
int BiTree::Width(){
	queue< struct q_element > q;
	int num[100]={0,1};
	q_element s,child;
	BiNode *root;
	root=this->root;
	if(root==NULL)
		return 0;
	s.root=root;	s.level=1;	q.push(s);	
	while(!q.empty())	{
		s=q.front();
		if(s.root->lchild){
			num[s.level+1]++;
			child.root=s.root->lchild;
			child.level=s.level+1;
			q.push(child);
		}
		if(s.root->rchild)	{
			num[s.level+1]++;
			child.root=s.root->rchild;
			child.level=s.level+1;
			q.push(child);
		}
		q.pop();
	}
	int max=0,i=1;
	while(num[i]>0){
		if(max<num[i])
			max=num[i];
		i++;
	}
	return max;
}

5.5.5 判断一棵树是否是完全二叉树

基本思想:
基于层次遍历。
定义bool变量is_leaf,初值为false
如果is_leaf的值为true, 表示遍历的过程中遇到了叶子结点。
一旦在叶子结点之后再出现度为1、2的结点,则该树不是完全二叉树。
基于层次遍历。
在层次遍历中,如果遇到一个节点
只有右儿子,没有左儿子,
false;
只有左,没有右
If (is_leaftrue) return false;
Else is_leaf=true;
两个儿子均为空
is_leaf=true
两个儿子均不空
If (is_leaf
true) return false;
将存在的儿子入队
能遍历完所有节点,即为完全二叉树

template<class T>
bool BiTree<T>::Is_Wanquan(BiNode<T> *root){
	queue<BiNode<T>*> q;
	BiNode <T>* pointer;
	bool is_leaf=false;
	if(!root)
		return false;
	q.push(root);
	while(!q.empty())	{
		pointer=q.front();	q.pop();
		if(pointer->rchild!=NULL && pointer->lchild==NULL)
			return false;
		else if(pointer->rchild==NULL && pointer->lchild!=NULL )
			if(is_leaf)	
				return false;
			else  //如果是完全二叉树,则,该结点之后的结点应为叶子节点
				is_leaf=true;
		else if(pointer->rchild==NULL && pointer->lchild==NULL )
			is_leaf=true;
		if(pointer->lchild!=NULL)
			q.push(pointer->lchild);
		if(pointer->rchild!=NULL)
			q.push(pointer->rchild);
	}
	return true;
}

5.6 二叉树的存储结构及实现

三叉链表
在二叉链表的基础上增加了一个指向双亲的指针域。

三叉链表的实现
结点数据类型声明:

template<class T>
struct Node
{
	T data;
	Node<T> * lchild, *rchild,*parent;
};
template <class T>
BiNode<T> * BiTree<T>::Creat(BiNode<T> * &root ,BiNode<T> *parent){
	T ch;
	cout<<"请输入创建一棵二叉树的结点数据"<<endl;
	cin>>ch;
    if (ch=="#") root = NULL;
    else{ 
	     root = new BiNode<T>;       //生成一个结点
          root->data=ch;
	     root->parent=parent;
         Creat(root->lchild,root );    //递归建立左子树
         Creat(root->rchild,root);    //递归建立右子树
    } 
    return root;
}
template<class T>
BiTree<T>::BiTree( int i)
{
	number=0;
	Creat(root,NULL);
}

5.7 树、森林与二叉树的转换

树和二叉树之间的对应关系

  1. 兄弟加线.
  2. 保留双亲与第一孩子连线,删去与其他孩子的连线.
  3. 顺时针转动,使之层次分明
    .

森林转换为二叉树

加线——若某结点x是其双亲y的左孩子,则把结点x的右孩子、右孩子的右孩子、……,都与结点y用线连起来;
去线——删去原二叉树中所有的双亲结点与右孩子结点的连线;
层次调整——整理由⑴、⑵两步所得到的树或森林,使之层次分明。

森林的遍历

森林有两种遍历方法:
前序(根)遍历:前序遍历森林即为前序遍历森林中的每一棵树。
后序(根)遍历:后序遍历森林即为后序遍历森林中的每一棵树。

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

叶子结点的权值:对叶子结点赋予的一个有意义的数值量。

二叉树的带权路径长度:设二叉树具有n个带权值的叶子结点,从根结点到各个叶子结点的路径长度与相应叶子结点权值的乘积之和。

哈夫曼树: 给定一组具有确定权值的叶子结点,带权路径长度最小的二叉树。
哈夫曼树的特点:

  1. 权值越大的叶子结点越靠近根结点,而权值越小的叶子结点越远离根结点。
  2. 只有度为0(叶子结点)和度为2(分支结点)的结点,不存在度为1的结点.

哈夫曼算法基本思想:
⑴ 初始化:由给定的n个权值{w1,w2,…,wn}构造n棵只有一个根结点的二叉树,从而得到一个二叉树集合F={T1,T2,…,Tn};
⑵ 选取与合并:在F中选取根结点的权值最小的两棵二叉树分别作为左、右子树构造一棵新的二叉树,这棵新二叉树的根结点的权值为其左、右子树根结点的权值之和;
⑶ 删除与加入:在F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到F中;
⑷ 重复⑵、⑶两步,当集合F中只剩下一棵二叉树时,这棵二叉树便是哈夫曼树。

5.9 线索二叉树

  • 二叉树的遍历运算是将二叉树中结点按一定规律线性化的过程。
  • 当以二叉链表作为存储结构时,只能找到结点的左、右孩子信息,而不能直接得到结点在遍历序列中的前驱和后继信息。
  • 要得到这些信息可采用以下两种方法:
    第一种方法是将二叉树遍历一遍,在遍历过程中便可得到结点的前驱和后继,但这种动态访问浪费时间;
    第二种方法是充分利用二叉链表中的空链域, 将遍历过程中结点的前驱、 后继信息保存下来。

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

结点结构

enum flag {Child, Thread}; 
template  <class T>
struct ThrNode
{
     T data;
     ThrNode<T>  *lchild, *rchild;
     flag ltag, rtag;
};
template <class T>ThrNode<T>* InThrBiTree<T>::Creat( ){
    ThrNode<T> *root;
    T ch;
    cout<<"请输入创建一棵二叉树的结点数据"<<endl;
    cin>>ch;
    if (ch=="#") root = NULL;
    else{	
         root=new ThrNode<T>;    
         root->data = ch;
         root->ltag = Child; root->rtag = Child;
         root->lchild = Creat( );
         root->rchild = Creat( ); 
    } 
	return root;
}

线索二叉树的建立

ThrNode<T>* pre = NULL

template <class T>
InThrBiTree<T>::InThrBiTree( )
{ 
	//ThrNode<T>* pre = NULL;
	this->root = Creat( );    
	ThrBiTree(root);
}
if (root==NULL) return;
 
ThrBiTree(root->lchild);  
if (!root->lchild){             
        root->ltag = Thread;
        root->lchild = pre;      
 }
 if (!root->rchild) 
        root->rtag = Thread;          
 if(pre != NULL){
      if (pre->rtag==Thread)
           pre->rchild = root;
 }
 pre = root;
 ThrBiTree(root->rchild);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值