树与二叉树
1.树的定义与相关概念
树的示例:
树的集合形式定义
Tree=(K,R)
元素集合:K={ki|0<=i<=n,n>=0,ki∈ElemType}(n为树中结点数,n=0则树为空,n>0则为非空树)
对于一棵非空树,关系R满足下列条件:
1.有且仅有一个结点没有前驱,称为根结点。
2.处根结点外,其余每个结点有且仅有一个前驱。
3.每个结点(包括根),可以有任意多个(含0个)后继。
树的递归形式定义
树是由n(n>=0)个元素构成的有限集合。
任意一颗非空树,都满足:
1.有且仅有一个根节点。
2.其余结点被分成m(m>=0)个
3.互不相交的有限集T1,T2,...Tm,其中每一个集合Ti(1<=i<=m)又是一颗树(递归),称为根的子树。
树结构反映了元素间的层次关系,分类分级的问题都可以考虑用树来描述。
树的基本术语——1
结点的度:结点所拥有的子树的个数。
树的度:树中所有结点的度的最大值。
叶结点:度为零的结点。
分支结点:度不为零的结点。
孩子结点和双亲结点:在一颗树中,每个结点的后继,被称为该结点的孩子结点,相应地,该结点被称为孩子结点地双亲结点。
兄弟结点:具有同一双亲的孩子结点。
树的基本术语——2
结点的祖先:从根到该结点所经分支上的所有结点都称为该结点的祖先。
结点的子孙:以某节点为根的子树中的任一结点。
结点的层次:树是一种层次结构,树中的每个结点都处于某个层次上。
树的深度:树中所有结点层次的最大值,也成为树的高度。
树的基本术语——3
有序树:树中的各结点的子树是按照从左到右有序排序的,即各子树的位置不能交换。
无序树:树中各结点的子树排序是无序的。
森林:m(>=0)颗互不相交的树的集合。
2.二叉树的定义
二叉树:每个结点最多有两棵子树的有序树
二叉树或者是一棵空树,或者是一棵由一个根节点和两棵互不相交的分别称做根的左子树和右子树组成的非空树,左子树和右子树同样是一颗二叉树。
注意:
二叉树的度只能是0、1或2;
二叉树是有序树,它的左、右子树是有次序的,即使只有一棵子树也要区分是左子树还是右子树。
3.满二叉树与完全二叉树
满二叉树:二叉树的所有分支结点都有左子树和右子树,并且所有叶子结点都在二叉树的最下一层。
完全二叉树:具有n个结点的完全二叉树,它的结构与满二叉树的前n个结点的结构相同。
4.二叉树的性质
性质1:非空二叉树上的叶结点数等于双分支结点数加1.即:n0=n2+1
证明:
设n0,n1,n2分别代表度为0,1,2的结点的个数,则结点总数n=n0+n1+n2
除根结点以外,每个结点上层都有一个分支与之相连,因此,具有n个结点的二叉树的额分支总数为B=n-1
这些分支来自度为1和度为2的结点,因此,分支总数B=n1+2n2
由上述三个式子得出:n0=n2+1
性质2:非空二叉树的第i(i>=1)层上最多有2的i-1次方个结点。
性质3:深度为h(h>=1)的非空二叉树最多有2的h次方-1个结点
性质4:具有n(n>0)个结点的完全二叉树的深度:h=[log2的n次方]+1
性质5:n个结点的完全二叉树,按从上至下,从左至右的次序对结点编号,则编号为i的结点有以下性质:
1、若编号为i的结点满足:i<=[n/2],即2i<=n.则该结点为分支结点,否则为叶子结点。
2、若n为奇数,则每个分支结点既有左孩子又有右孩子;
3、若n为偶数,则编号最大的分支结点(编号为n/2)只有左孩子,没有右孩子,其余分支结点都有左、右孩子。
示例:已知一棵完全二叉树的第6层有7个结点,则:
前5层为满二叉树,共有结点2的5次方-1=31,第6层7个结点,总结点数为31+7=38.
由于完全二叉树最后一层的结点必须从左至右连续出现,所以 第6层的7个结点中,只有最后一个结点的双亲是度为1的结点,即度为1的结点有1个。
5.二叉树的顺序存储结构
在一组连续的存储单元中,按照完全二叉树中结点编号将结点自上而下、自左至右的顺序存储。
元素的位置序号和结点的编号相对应,即结点在数组中的位置表示了结点之间的关系:
1、结点编号为i,则:
2、结点i的双亲结点[i/2]
3、结点i的左子结点2i,右子结点2i+1
非完全二叉树 的存储方法:
6.二叉树的链式存储结构
二叉树结点类型:
typedfe struct node{
ElemType data;//数据域
struct node *left;
struct node *right;//结点的左右子树指针
}BTNode;//二叉树结点类型
一个二叉链表由根指针root唯一确定。
若二叉树为空,则root=NULL;
若结点的某个孩子不存在,则相应的指针为空。
三叉链表:根据实际问题的需要,还可以在结点中添加父结点的指针。
7.逻辑关系与存储结构
8.二叉树的遍历以及基本算法操作
遍历:按一定次序访问二叉树中的每个结点,且每个结点仅被访问一次。
注意:把不要将整棵树看成有多个结点组成,而要看成是由根、左子树、右子树组成。
遍历顺序:
若限定先左后右的次序,则二叉树的遍历有以下三种顺序:
1、前序遍历(根->左子树->右子树)
2、中序遍历(左子树->根->右子树)
3、后序遍历(左子树->右子树->根)
层次遍历:对二叉树按照从上至下,每层按照从左至右的顺序进行遍历。(不常用)
前序遍历(根左右)
中序遍历(左根右)
后序遍历(左右根)
示例:
写出下面二叉树的前序、中序和后序遍历序列
前序:ABDFGCEH
中序:BFDGAEHC
后序:FGDBHECA
根据遍历序列确定二叉树的形态:
由任意一个遍历序列均不能唯一确定一颗二叉树。
只有知道中序和后序,或中序和前序遍历序列才能唯一确定一棵二叉树。
确定方法:
由前序(或后序)遍历序列确定树或子树的根;
再由中序遍历序列确定根的左子树和右子树的范围。
示例:
后序遍历序列:EBDCA(左右根)
中序遍历序列:BEADC(左根右)
前序遍历序列:ABCDEFG
中序遍历序列:CBEDAFG
前序遍历算法
若二叉树非空,则按以下次序进行(递归)遍历:
1、访问根结点;
2、前序遍历根结点的左子树;
3、前序遍历根结点的右子树。
void PreOrder(BTNode *root)
{
if(root!=NULL)
{
cout<<root->data; //访问根
PreOrder(root->left);//前序遍历左子树
PreOrder(root->right);//前序遍历右子树
}
}
中序遍历算法
若二叉树非空,则按以下次序进行(递归)遍历:
1、前序遍历根结点的左子树;
2、访问根结点;
3、前序遍历根结点的右子树。
void PreOrder(BTNode *root)
{
if(root!=NULL)
{
PreOrder(root->left);//前序遍历左子树
cout<<root->data; //访问根
PreOrder(root->right);//前序遍历右子树
}
}
后序遍历算法
若二叉树非空,则按以下次序进行(递归)遍历:
1、前序遍历根结点的右子树。
2、访问根结点;
3、前序遍历根结点的左子树;
void PreOrder(BTNode *root)
{
if(root!=NULL)
{
PreOrder(root->right);//前序遍历右子树
cout<<root->data; //访问根
PreOrder(root->left);//前序遍历左子树
}
}
以二叉树的前序遍历序列建立二叉树
如果遍历序列中没有指明空指针的位置,则需要“前序遍历序列+中序遍历序列”或“中序遍历序列+后序遍历序列”才能建立一棵树。
如果在前序遍历序列中以#号表明空指针的位置,则只需要一个前序遍历序列就可以建立这棵树。
例如:
前序:AB###
void CreateBTree_Pre(BTNode *&root)
{
//要求按照前序遍历序列输入结点值item
char item;
cin>>item;
if (item=='#')
{
//如果读入#字符,创建空树
root=NULL;
}
else
{
root=new BTNode; //建根结点
root->data=item;
CreateBTree_Pre(root->left);//建左子树
CreateBTree_Pre(root->right);//建右子树
}
}
示例:
根据前序遍历序列:ABC##DE#G##F###创建二叉树的过程。
统计二叉树中结点总数
左子树结点数+右子树结点数+1(根结点)
int BTeeCount(BTNode *root)
{
if (root==NULL)
return 0;//空树的结点数为0
else
return BTreeCount(root->left)+BTreeCount(root->right)+1;
}
计算二叉树深度
左、右子树中深度较大的子树深度+1(根结点)
int BTreeDepth(BTNode *root)
{
if(root==NULL)
return 0;
else
{
int dep1=BTreeDepth(root->left);
int depr=BTreeDepth(root->right);
if(dep1>depr)
return dep+1;
else
return depr+1;
}
}
查找二叉树中值为item的结点
先找根,再找左子树,最后找右子树。
BTNode *FindBTree(BTNode *root,ElemType item)
{
if(root==NULL)
return NULL;
if(root->data==item)
return root;
BTNode *p=FindBTree(root->left,item);
if(p!=NULL)
return p;
else
return FindBTree(root->right,item);
}
清空二叉树
释放二叉树中所有结点
void ClearBTree(BTNode *&root)
{
if(root!=NULL)
{
ClearBTree(root->left);
ClearBTree(root->right);
delete root;
root=NULL;
}
}
9.线索二叉树的定义
n个结点的二叉树,每个结点均有2个指针与,则n个结点的二叉树共有2n个指针域。
处根结点外,二叉树中每一个结点均由一个指针域指向,则n个结点的二叉树使用了n-1个指针域。
空指针数目=2n-(n-1)=n+1
由此可见,二叉链表空间利用率较低。
为解决此问题我们将空指针保存前驱和后驱。
中序遍历序列为BDCEAFHG,则A结点的前驱为E,后继为F。
普通二叉树只能找到结点的左、右孩子信息,而结点的前驱和后继只能在遍历过程中获得。
可以利用n+1个空指针域保存前驱和后继的信息,从而提高遍历过程的效率。
利用空指针域时,为了避免混淆,给结点增加两个标志域,如下图所示:
约定:
左标志ltag:0表示left指向左孩子结点;1表示left指向前驱结点。
右标志rtag:0表示right指向右孩子结点;1表示right指向后继结点
线索二叉树
指示前驱和后继的指针域称为线索;
对二叉树以某种次序遍历,创建线索的过程称为线索化。
线索化之后的二叉树称为线索二叉树;
分别按中序、前序、后序遍历可得到:中序线索二叉树、前序线索二叉树、后序线索二叉树。
示例:
该二叉树中序遍历结果为:D,G,B,A,E,C,F
中序线索二叉树存储结构
中序遍历结果为:D,G,B,A,E,C,F
线索化的过程:
建立某种次序的线索二叉树过程:
1、以该遍历方法遍历一棵二叉树。
2、在遍历的过程中,检查结点的左、由指针域是否为空:
3、如果左指针域为空,将它改为指向前驱的线索;
4、如果右指针域为空,将它改为指向后继的线索。
中序线索化
以中序遍历为例,建立中序线索二叉树。
在遍历过程中,设定p和pre用于保存相应位置:
p:总是指向当前线索化的结点。
pre:作为全局变量,指向刚刚访问过的结点。
*pre时*p的中序前驱结点;
*p是*pre的中序后继结点。
若p->left==NULL,则{p->ltag=1;p->left=pre;}
若pre->right==NULL,则{pre->rtag=1,pre->right=p;}
10.线索二叉树的建立与相关操作
中序线索化的实现
TBTNode *pre;//pre为全局变量
bool InOrderThr(TBTNode *&head,TBTNode *T)
{
//建立中序线索二叉树,head指向头结点。
if(!(head=new TBTNode))//建立头结点
return false;
head->ltag=0;
head->rtag=1;
head->right=head;//右指针回指
if(!T)
head->left=head;//若二叉树为空,则左指针回指
else
{
head->left=T;
pre=head;//pre是*p的前驱,供加线索用
InThreading(T);//中序遍历进行中序线索化
pre->rtag=1;
pre->right=head;//最后一个线索化
head->right=ptr;
}
return true;
}
void InThreading(TBTNode *p)
{
//中序遍历进行中序线索化
if(p)
{
InThreading(p->left);//左子树线索化
if(!p->left)
{
//前驱线索
p->ltag=1;
p->left=pre;
}
if(!pre->right)
{
//后继线索
pre->rtag=1;
pre->right=p;
}
pre=p;//访问右子树前将当前结点位置p保存于pre中
InThreading(p->right);//右子树线索化
}
}
查找任意结点p的中序前驱
若p的ltag为1,则p的left为线索,指向p的前驱;
若p的ltag为0,则p的前驱为p的左子树的“最右结点”。
TBTNode *InPreNode(TBTNode *p)
{
//在中序线索二叉树上寻找结点p的中序前驱结点
TBTNode *pre;
pre=p->left;
if(p->ltag!=1)
while(pre->rtag==0)
pre=pre->right;
return pre;
}
查找任意结点p的中序后继
若p的rtag为1,则p的right为线索,指向p的后继;
若p的rtag为0,则p的后继为p的右子树的“最左结点”
TBTNode *InPreNode(TBTNode *p)
{
//在中序线索二叉树上寻找结点p的中序后继结点
TBTNode *post;
post=p->right;
if(p->rtag!=1)
while(pre->ltag==0)
pre=pre->left;
return post;
}
11.哈夫曼树
哈夫曼树的定义
结点的权:将树中结点赋予一个有某种意义的数值。
设二叉树具有n个带权值的叶子结点,从根结点到各个叶子结点的路径长度li与相应结点权值wi的乘积之和,称为二叉树的带权路径长度:
带权路径长度WPL最小的二叉树称为哈夫曼树(最优二叉树)。
示例:
WPL=2*3+4*3+5*2+7*1=35
构造哈夫曼树
根据哈夫曼树的定义,二叉树要使WPL值最小,则:
1、权值越大的叶子结点越靠近根结点;
2、而权值越小的叶子结点越远离根结点。
给定权值w=(7,2,8,4),构造哈夫曼树的方法如下:
1、由给定的n个权值{W1,W2,...,Wn}构造n棵只有一个叶子结点的二叉树,从而得到一个二叉树的集合F={T1,T2,...,Tn};
2、在F中选取根结点的权值最小的和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,新的二叉树根结点的权值为其左、右子树根结点权值之和;
3、在集合F中删除作为左、右子树的两颗二叉树,并将新建立的二叉树加入到集合F中;
4、重复2、3两步,当F中只剩下一棵二叉树时,这棵二叉树即为要建立的哈夫曼树。
示例:
给定权值W={2,3,4,5},构造哈夫曼树。
12.哈夫曼编码及应用
编码:进行快速远距离通信时,需将传送的文字转换成由二进制的字符0和1组成的字符串。
不等长编码:若需要电文总长尽可能短,可对每个字符设计长度不等的编码,且让出现次数较多的字符采用尽可能短的编码,若要设计不等长编码,则必须是任一个字符的编码都不能是另一个字符的编码的前缀。
利用哈夫曼树设计最短电文编码方案
以电文中出现的字符作为叶子结点,以该字符在电文中出现的频率作为该叶子的权值,构造哈夫曼树。
规定哈夫曼树中的左分支为0,右分支为1.
从根结点到每个叶结点所经过的分支对应的0和1组成的序列便为该结点对应字符的哈夫曼编码。
哈夫曼编码的特点:权值越大的字符编码越短,反之越长。
示例:
写出电文“ATTSTATADT”中各字符的哈夫曼编码。
1、字符集合{T,A,D,S}
2、出现的频率集合W={5,3,1,1}
3、构造哈夫曼树
4、哈夫曼编码:T:0 A:11 D:100 S:101
5、电文“ATTSTATADT”经过编码后得到:11 00 101 0 11 0 11 100 0