在树中通常将数据元素称为结点(node)。
树(tree)是n(n≥0)个结点的有限集合。当n=0时,称为空树;任意一棵非空树满足以下条件:
(1)有且仅有一个特定的称为根(root)的结点;
(2)当n>1时,除根结点之外的其余结点被分成m(m>0) 个互不相交(结点不能属于多个子树,子树之间不能有关系)的有限集合,T1,T2, … ,Tm,其中每个集合又是一棵树,并称为这个根结点的子树(subtree)。
(1)结点的度、树的度
某结点所拥有的子树的个数称为该结点的度(degree);树中各结点度的最大值称为该树的度。
对于一个拥有n个结点的树,其所有度的和为n-1。
(2)叶子结点、分支结点
度为0的结点称为叶子结点(leaf node),也称为终端结点;度不为0的结点称为分支结点(branch node),也称为非终端结点。
(3)孩子结点、双亲结点、兄弟结点
某结点的子树的根结点称为该结点的孩子结点(child node);反之,该结点称为其孩子结点的双亲结点(parent node);具有同一个双亲的孩子结点互称为兄弟结点(brothernode)。
(4)路径、路径长度
如果树的结点序列n1,n2…nk满足如下关系:结点:是结点ni+1的双亲(1≤i
(5)祖先、子孙
如果从结点x到结点y有一条路径,那么x就称为y的祖先(ancestor),y称为x的子孙(descendant)。显然,以某结点为根的子树中的任一结点都是该结点的子孙。
(6)结点的层数、树的深度(高度)、树的宽度
规定根结点的层数(level)为1,对其余任何结点,若某结点在第k层,则其孩子结点在第k十1层;树中所有结点的最大层数称为树的深度(depth),也称为树的高度:树中每一层结点个数的最大值称为树的宽度(breadth)。
(7)结点所在层数:根结点的层数为 1;对其余结点,若某结点在第 k 层,则其孩子结点在第 k+1 层
树的深度(高度):树中所有结点的最大层数
树的宽度:树中每一层结点个数的最大值
练习题
已知一颗度为4的树中,度为i(i>=1)的结点个数有i个,该树中有21个叶子结点。度总数为44+33+2*2=29,树结点总数为29+1,叶子结点为30-4-3-2=21.
ADT Tree
DataModel
树由一个根结点和若干棵子树构成,树中结点具有层次关系
Operation
InitTree:初始化一棵树
DestroyTree:销毁一棵树
PreOrder:前序遍历树
PostOrder:后序遍历树
LeverOrder:层序遍历树
endADT
存储结构:数据元素及其逻辑关系在存储器中的表示
顺序存储:完全二叉树和满二叉树适用
template <typename DataType>
struct PNode
{
DataType data;
int parent;
};
struct CTNode //孩子结点
{
int child;
CTNode* next;
}; //存放孩子链表的表头的数组
template <typename DataType>
struct CBNode //表头结点
{
DataType data;
CTNode* firstChild; //指向孩子链表的头指针
};
template <typename DataType>
struct TNode
{
DataType data;
TNode<DataType> *firstChild, *rightSib;
};
左斜树:所有结点都只有左子树的二叉树
右斜树:所有结点都只有右子树的二叉树
斜树:左斜树和右斜树的统称
斜树有什么特点呢?
(1)每一层只有一个结点
(2)结点个数与其深度相同
斜树是树结构的特例,是从树结构退化成了线性结构
所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上的二叉树
可以使用顺序存储
满二叉树有什么特点呢?
(1)叶子只能出现在最下一层
(2)只有度为0和度为2的结点
(3)在同样深度的二叉树中结点个数最多
(4)在同样深度的二叉树中叶子结点个数最多
满二叉树是树结构的特例,是最丰满的二叉树
适用所有二叉树的性质
适用完全二叉树的性质
从根结点出发,按照某种次序(前序(根)、后序(根)和层序(次)等访问)树中所有结点,并且每个结点仅被访问一次
树的前序遍历操作定义:
若二叉树为空,则空操作返回;否则:
(1)访问根结点
(2)前序遍历根结点的左子树
(3)前序遍历根结点的右子树
A B D E H I F C G
树的中序遍历:
若二叉树为空,则空操作返回;否则:
(1)中序遍历根结点的左子树
(2)访问根结点
(3)中序遍历根结点的右子树
树的后序遍历操作定义:
若二叉树为空,则空操作返回;否则:
(1)后序遍历根结点的左子树
(2)后序遍历根结点的右子树
(3)访问根结点
D H I E F B G C A
树的层序遍历操作定义:
从二叉树的根结点开始,从上至下逐层遍历,在同一层中,则按从左到右的顺序对结点逐个访问
A B C D E F G H I
方法1:LeafCount为全局变量,调用前初始化值为0
int LeafCount=0;{
void LeafNum (BiTree root)
{if (root!=NULL)
{
if(root->lchild==NULL&&root->rchild==NULL)
LeafCount++;
LeafNum(root->lchild);
LeafNum(root->rchild);
}
方法2:采用函数返回值
int leaf(BiTree root){
int LeafCount;
if(root==NULL)
LeafCount =0;
else if((root->lchild==NULL)&&(root->rchild==NULL))
LeafCount =1;
else
LeafCount =leaf(root->lchild)+leaf(root->rchild);
return LeafCount;
}
int depth=0;
void PreTreeDepth(BiTree root,int h)
{
if(root-NULL)
if(h>depth)
depth=h;
PreTreeDepth(root->lchild,h+1);
PreTreeDepth(root->rchild,h+1);
}
int PostTreeDepth(BiTree root)
{
int hl,hr,max;
if(root!=NULL)
{ hl=PostTreeDepth(root->lchild);
hr=PostTreeDepth(root->rchild);
max=hl>hr?hl:hr;
return max+1;
}
else
return 0;
}
二叉树的顺序存储结构是用一维数组存储二叉树的结点,结点的存储位置(下标)应能体现结点之间的逻辑关系——父子关系——而完全二叉树中结点的编号可以唯一地反映结点之间的逻辑关系
二叉树的顺序存储结构一般仅存储完全二叉树
template <typename DataType> struct BiNode
{
DataType data;
BiNode< DataType > *lchild, *rchild;
};
template <typename DataType>
class BiTree
{
public:
BiTree( ){root = Creat(root);}
~BiTree( ){Release(root);}
void PreOrder( ){PreOrder(root);}
void InOrder( ){InOrder(root);}
void PostOrder( ){PostOrder(root);}
void LeverOrder( );
private:
BiNode<DataType> *Creat(BiNode<DataType> *bt);
void Release(BiNode<DataType> *bt);
void PreOrder(BiNode<DataType> *bt);
void InOrder(BiNode<DataType> *bt);
void PostOrder(BiNode<DataType> *bt);
BiNode<DataType> *root;
};
template <typename DataType>
void BiTree<DataType> :: PreOrder(BiNode<DataType> *bt)
{
if (bt == null) return; //递归调用的结束条件
else {
cout << bt->data; //访问根结点bt的数据域
PreOrder(bt->lchild); //前序递归遍历bt的左子树
PreOrder(bt->rchild); //前序递归遍历bt的右子树
}
}
PreOrder(bt->lchild); //中序递归遍历bt的左子树
cout << bt->data; //访问根结点bt的数据域
PreOrder(bt->rchild); //中序递归遍历bt的右子树
PreOrder(bt->lchild); //后序递归遍历bt的左子树
PreOrder(bt->rchild); //后序递归遍历bt的右子树
cout << bt->data; //访问根结点bt的数据域
1.队列Q初始化;
2.如果二叉树非空,将根指针入队:
3.循环直到队列Q为空
3.1q=队列Q的队头元素出队:
3.2访问结点q的数据域;
3.3若结点q存在左孩子,则将左孩子指针入队;
3.4若结点q存在右孩子,则将右孩子指针入队;
template <typename DataType>
void BiTree<DataType> :: LeverOrder( )
{
BiNode<DataType> *Q[100], *q = null;
int front = -1, rear = -1;
if (root == null) return;
Q[++rear] = root;
while (front != rear)
{
q = Q[++front]; cout << q->data;
if (q->lchild != null) Q[++rear] = q->lchild;
if (q->rchild != null) Q[++rear] = q->rchild;
}
}
template <typename DataType>
BiNode<DataType> *BiTree<DataType> :: Creat(BiNode<DataType> *bt)
{
char ch;
cin >> ch; //输入结点的数据信息,假设为字符
if (ch == ‘#’) bt = null; //建立一棵空树
else {
bt = new BiNode<DataType>; bt->data = ch;
bt->lchild = Creat(bt->lchild); //递归建立左子树
bt->rchild = Creat(bt->rchild); //递归建立右子树
}
return bt;
}
template <typename DataType>
void BiTree<DataType> :: Release(BiNode<DataType> *bt)
{
if (bt == null) return;
else{
Release(bt->lchild); //释放左子树
Release(bt->rchild); //释放右子树
delete bt; //释放根结点
}
}
m(m≥0)棵互不相交的树的集合
树转换为二叉树
(1)加线——树中所有相邻兄弟之间加一条连线
(2)去线——对树中的每个结点,只保留它与第一个孩子结点之间的连线,删去它与其它孩子结点之间的连线。
(3)层次调整——以根结点为轴心,将树顺时针转动一定的角度,使之层次分明。
树的前序遍历等价于二叉树的前序遍历!ABEFCDG
树的后序遍历等价于二叉树的中序遍历!EFBCGDA
树的根结点没有兄弟->二叉树根结点的右子树必为空
森林转换为二叉树
将一个森林转换为二叉树的方法是
(1)将森林中的每棵树转换为二叉树
(2)将每棵树的根结点视为兄弟,在所有根结点之间加上连线
(3)按照二叉树结点之间的关系进行层次调整
ⅰ加线:树中所有相邻兄弟之间加一条连线各棵树的根结点间也视为兄弟);
ⅱ删线:每一个结点,仅保留其与第一个孩子之间的连线,删去与其他孩子结点之间的连线;
ⅱ.旋转:以第一棵二叉树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构清晰,左右子树分明。
二叉树转换为树(森林)
将一棵二叉树还原为树或森林,具体转换方法是
(1)加线——若某结点 x 是其双亲 y 的左孩子,则把结点 x 的右孩子、右孩子的右孩子、……,与结点 y 连线
(2)去线——删去所有双亲结点与右孩子结点的连线
(3)层次调整——整理由(1)、(2)两步所得到的树(森林),使之层次分明。
void RootFirst(CSTree root)
if (root!=NULL)
Visit(root->data); /*访问根结点*/
p=root->FCh;
while (p!=NULL)
{ RootFirst(p) /*访问p子树*/
p=p->Nsib;}
}
}
void RootFirst(CSTree root)
{ if (root!=NULL)
{ Visit (root ->data); /*访问根结点*/
RootFirst (root->FCh); /*先根遍历首子树*/
RootFirst (root->Nsib); /*先根遍历兄弟树*/
}
}
优点:简洁、可读性强,而且其正确性容易得到证明,这给程序的编写和调试带来很大的方便。
不足:运行效率低,消耗的时间、空间资源都较多
建栈,并初始化;
若遍历结点存在或者栈不为空
①若结点存在:结点入栈,指向其左孩子
②否则:出栈,访问结点,指向其右孩子
否则,遍历结束
void InOrder(BiTree root)
{ SqStack S;
InitStack(S);
BiNode *p;
p=root;
while(p||!StackEmpty(S)){
if(p){
Push(S,p); /*保存根结点*/
p=p->lchild; /*遍历左子树*/
}
else{
Pop(S,p); /*弹出根结点*/
visit(p->data); /*访问根结点*/
p=p->rchild; /*遍历右子树*/
}
}
}
if(p){
visit(p->data); /*访问根结点*/
Push(S,p); /*保存根结点*/
p=p->lchild; /*遍历左子树*/
}
else{
Pop(S,p); /*弹出根结点*/
p=p->rchild; /*遍历右子树*/
}
后序遍历的非递归算法比前序中序复杂
目的:利用他的空孩子指针,指向它的遍历前驱和后继,即把空指针改为线索,以便实现不需要递归和栈,就能够完成对二叉树的遍历
在2n个指针域中只有n-1个指针域用来存储孩子结点的地址,存在n+1个空指针域,可以利用这些空指针指向该结点在某种遍历序列中的前驱和后继结点。这些指向遍历序列前驱、后继结点的指针称为线索;
把空指针修改为线索的过程称为线索化;
经过线索化的二叉树称为线索二叉树;
含有线索的二叉链表称为线索链表。
template <typename DataType>
Struct ThrNode{
DataType data;
int ltag,rtag;
ThrNode<DataType>*lchild,*rchild;
};
void Inthread(BiTree p)
if (p!=NULL)
{ Inthread(p->LChild);
/*遍历左子树*/
if (p->LChild==NULL)
p->LChild=pre;p->Ltag=1;
if (pre!=NULL&&pre->RChild==NULL)
pre->RChild=p;pre->Rtag=1;
pre=p;
Inthread(p->RChild);/*遍历右子树*/
}
}
BiTNode InFirst(BiTree Bt)
{
BiTNode *p=Bt;
if (Ip)
return(NULL)方
while(p->Ltag==0)
p=p->LChild;
return p;
BiTNode *InNext(BiTNode *p)
{ if(p->Rtag==1)Next=p->RChild;
else
{ for(q=p->RChild;q->Ltag==0;q=q->LChild);
Next=q;
}
return(Next);
}
void TinOrder(BiTree bt)
{
BiTNode *p;
p=InFirst(bt);
while (p){
visit(p);
p=InNext(p);
}
}
在使用哈夫曼树进行编码和译码时,既要用结点的双亲信息,又要用结点的孩子信息,所以采用静态三叉链表存储哈夫曼树。
采用数组还是链表呢?
须存储哪些关系呢?
struct ElemType
{
int weight; /*假定权值为整数*/
int parent, lchild, rchild;
};
函数原型:void HuffmanTree(ElemType huffTree[ ], int w[ ], int n )
void HuffmanTree(ElemType huffTree[ ], int w[ ], int n )
{
int i, k, i1, i2;
for (i = 0; i < 2*n-1; i++) /*初始化,所有结点均没有双亲和孩子*/
{
huffTree[i].parent = -1; huffTree[i].lchild = -1; huffTree[i].rchild = -1;
}
for (k = n; k < 2*n-1; k++) /*n-1次合并*/
{
Select(huffTree, i1, i2); /*权值最小的两个根结点下标为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;
}
}
前缀编码:在一组编码中,任一编码都不是其它任何编码的前缀
前缀(无歧义)编码保证了在解码时不会有多种可能
例:一组字符{A, B, C, D, E, F, G}出现的频率分别是{9, 11, 5, 7, 8, 2, 3},设计最经济的编码方案
哈夫曼编码方案:
A:00
B:10
C:010
D:110
E:111
F:0110
G:0111
等长编码的长度:3
哈夫曼编码的平均长度:
(2×9+3×5+4×2+4×3+2×11+3×7+3×8)/45
= 2.67
存储定义:
typedef char* Huffmancode [n+1];//Huffmancode 为char*[n+1]
char* cd;
int start;
# hc为Huffmancode指针数组
CrtHuffmanCode(HuffmanTree ht,Huffmancode hc,int n)
{ cd=(char* )malloc(n *sizeof(char));
cd[n-l]='0';
for(i=1;i<=n;i++)
{start=n-1;c=i;p=htcl.parent;
while( p!=0)
if(ht[p].Lchild ==c)cd[--start]='0';
else cd[--start] ='1';
c=p;p=ht[c].parent;}
hc[i]=(char *)malloc((n-start)*sizeof(char));
strcpy(hc[i],&cd[start]);}
free(cd);
}