二叉树的定义及基本特点
定义
二叉树(Binary Tree)是n(n≥0)个结点所构成的集合,它或为空树(n = 0);或为非空树,对于非空树T:(1)有且仅有一个称之为根的结点 (2)除根结点以外的其余结点分为两个互不相交的子集T1和T2,分别称为T的左子树和右子树,且T1和T2本身又都是二叉树。
基本特点
- 结点的度小于等于2
- 有序树(子树有序,不能颠倒)
二叉树的性质
- 在二叉树的第i层上至多有 2 i − 1 2^{i-1} 2i−1 个结点
- 深度为k的二叉树至多有 2 k − 1 2^k -1 2k−1 个结点
- 对于任何一棵二叉树,若2度的结点数有 n 2 n_2 n2个,则叶子数 n 0 n_0 n0必定为 n 2 + 1 n_2+1 n2+1 (即 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1)
- 完全二叉树:深度为k 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k 的满二叉树中编号从1至n的结点一一对应
- 满二叉树:一棵深度为k 且有
2
k
−
1
2k -1
2k−1个结点的二叉树。
(特点:每层都“充满”了结点) 满二叉树是完全二叉树的一个特例。
- 具有n个结点的完全二叉树的深度必为 ⌊ l o g 2 n ⌋ + 1 \lfloor log2n \rfloor+1 ⌊log2n⌋+1
- 对完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为 2 i 2i 2i,其右孩子编号必为 2 i + 1 2i+1 2i+1;其双亲的编号必为 i 2 \frac {i} {2} 2i
二叉树的存储结构
-
顺序存储
特点:
结点间关系蕴含在其存储位置中
浪费空间,适于存满二叉树和完全二叉树 -
链式存储
(1)二叉链表typedef struct BiNode{ TElemType data; struct BiNode *lchild,*rchild; //左右孩子指针 }BiNode,*BiTree;
在n个结点的二叉链表中,有(n+1)个空指针域
(2)三叉链表
typedef struct TriTNode{ TelemType data; struct TriTNode *lchild,*parent,*rchild;//左右孩子指针和双亲指针 }TriTNode,*TriTree;
遍历二叉树
遍历规则:
- DLR—先序遍历,即先根再左再右
- LDR—中序遍历,即先左再根再右
- LRD—后序遍历,即先左再右再根
若二叉树中各结点的值均不相同,则:
由二叉树的前序序列和中序序列,或由其后序序列和中序序列均能唯一地确定一棵二叉树,但由前序序列和后序序列却不一定能唯一地确定一棵二叉树。
算法实现:
//递归遍历二叉树
//先序
void PreOrderTraverse(BiTree T){
if(T==NULL) return ; //空二叉树
else{
cout<<T->data; //访问根结点
PreOrderTraverse(T->lchild); //递归遍历左子树
PreOrderTraverse(T->rchild); //递归遍历右子树
}
}
//中序
void InOrderTraverse(BiTree T){
if(T==NULL) return ; //空二叉树
else{
InOrderTraverse(T->lchild); //递归遍历左子树
cout<<T->data; //访问根结点
InOrderTraverse(T->rchild); //递归遍历右子树
}
}
//后序
void PostOrderTraverse(BiTree T){
if(T==NULL) return ; //空二叉树
else{
PostOrderTraverse(T->lchild); //递归遍历左子树
PostOrderTraverse(T->rchild); //递归遍历右子树
cout<<T->data; //访问根结点
}
}
//非递归遍历(中序)
void InOrderTraverse(BiTree T){
InitStack(S);
p=T;//p指向根节点
q=new BiTree;//q用来存放栈顶弹出的元素
while( p || !StackEmpty(S))
{
if(p) //p非空
{
Push(S,p); //根指针压入栈
p=p->lchild; //遍历左子树
}
else
{
Pop(S,q); //出栈
cout<<q->data; //访问根节点
p=q->rchild; //遍历右子树
}
}
}
二叉树的建立
//建立二叉树 (先序)
void CreateBiTree(BiTree &T){
cin>>ch;
if (ch=='#') T=NULL; //递归结束,建空树
else{
T=new BiTNode;
T->data=ch; //生成根结点
CreateBiTree(T->lchild); //递归创建左子树
CreateBiTree(T->rchild); //递归创建右子树
}
}
复制二叉树
//复制二叉树
void Copy(BiTree T,BiTree &NewT){
if(T==NULL)
{
NewT=NULL;
return ;
}
else
{
NewT=new BiTree;
NewT->data=T->data;
copy(T->lchild,NewT->lchild);
copy(T->rchild,NewT->rchild);
}
}
二叉树遍历算法的应用
//计算二叉树叶子节点总数
int LeadCount(BiTree T){
if(T==NULL) //如果是空树返回0
return 0;
if (T->lchild == NULL && T->rchild == NULL)
return 1; //如果是叶子结点返回1
else return LeafCount(T->lchild) + LeafCount(T->rchild);
}
//计算二叉树的深度
int Depth(BiTree T){
if(T==NULL) //如果是空树返回0
return 0;
else
{
m=Depth(T->lchild);
n=Depth(T->rchild);
if(m>n) return m+1;
else return n+1;
}
}
//计算二叉树中结点的个数
int NodeCount(BiTree T) {
if(T==NULL)
return 0;
else
return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}
线索化二叉树
利用n+1个空链域增加信息
- 若结点有左子树,则lchild指向其左孩子;否则, lchild指向其直接前驱(即线索)
- 若结点有右子树,则rchild指向其右孩子;否则, rchild指向其直接后继(即线索)
为了避免混淆,增加两个标志域
//以结点p为根的子树中序线索化
//pre是全局变量 初始化时其右孩子指针为空,便在数的最左点开始建线索
void InThreading(BiThrTree p)
{ if(p)
{
InThreading(p->lchild);//左子树递归线索化
if(!p->lchild) //没有左孩子
{
p->LTag=1; //给p加上左线索
p->lchild=pre; //左孩子指针指向前驱pre
}
else
p->LTag=0;
if(!pre->rchild)
{
pre->RTag=1;
pre->lchild=p; //p的右孩子指针指向其后继p
}
else
p->RTag=0;
pre=p; //保持pre指针p的前驱
InThreading(p->rchild); //右子树递归线索化
}
}
哈夫曼树
相关概念:
- 路径:由一结点到另一结点间的分支所构成
- 路径长度 :路径上的分支数目
- 带权路径长度 :结点到根的路径长度与结点上权的乘积
- 树的带权路径长度 :树中所有叶子结点的带权路径长度之和
- 哈夫曼树:带权路径长度最小的树
哈夫曼树的构造过程
基本思想:使权值大的结点靠近根
操作要点:对权值的合并、删除与替换,总是合并当前值最小的两个
一棵有n个叶子结点的Huffman树有 2 n − 1 2n-1 2n−1个结点,故采用顺序存储结构
结点类型定义
typedef struct
{ int weght;
int parent,lch,rch;
}*HuffmanTree;
初始化并创建哈夫曼树
void CreatHuffmanTree (HuffmanTree HT,int n){
if(n<=1)return;
m=2*n-1;
HT=new HTNode[m+1];//0号单元未用,HT[m]表示根结点
for(i=1;i<=m;++i){
HT[i].lch=0;
HT[i].rch=0;
HT[i].parent=0;}
for(i=1;i<=n;++i)
cin>>HT[i].weight;
for( i=n+1;i<=m;++i) //构造 Huffman树
{ Select(HT,i-1, s1, s2);
//在HT[k](1≤k≤i-1)中选择两个其双亲域为0,
// 且权值最小的结点,
// 并返回它们在HT中的序号s1和s2
HT[s1].parent=i; HT[s2] .parent=i;
//表示从F中删除s1,s2
HT[i].lch=s1; HT[i].rch=s2 ;
//s1,s2分别作为i的左右孩子
HT[i].weight=HT[s1].weight + HT[s2] .weight;
//i 的权值为左右孩子权值之和
}
}
void Select(HuffmanTree& HT, int j, int& d1, int& d2)//获取两个较小值的位置
{
int min[2] = { 10000,10000 };
for (int i = 1; i < j; i++)
{
if (HT[i].parent == 0)
{
if (HT[i].weight < min[0])
{
min[1] = min[0];
min[0] = HT[i].weight;
d2 = d1;
d1 = i;
}
else if (HT[i].weight < min[1])
{
min[1] = HT[i].weight;
d2 = i;
}
}
else
continue;
}
}
从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中
void CreatHuffmanCode(HuffmanTree HT, HuffmanCode &HC, int n){
//从叶子到根逆向求每个字符的赫夫曼编码,存储在编码表HC中
HC=new char *[n+1]; //分配n个字符编码的头指针矢量
cd=new char [n]; //分配临时存放编码的动态数组空间
cd[n-1]=’\0’; //编码结束符
for(i=1; i<=n; ++i){ //逐个字符求赫夫曼编码
start=n-1; c=i; f=HT[i].parent;
while(f!=0){ //从叶子结点开始向上回溯,直到根结点
--start; //回溯一次start向前指一个位置
if (HT[f].lchild= =c) cd[start]=’0’; //结点c是f的左孩子,则生成代码0
else cd[start]=’1’; //结点c是f的右孩子,则生成代码1
c=f; f=HT[f].parent; //继续向上回溯
} //求出第i个字符的编码
HC[i]= new char [n-start]; // 为第i 个字符编码分配空间
strcpy(HC[i], &cd[start]); //将求得的编码从临时空间cd复制到HC的当前行中
}
delete cd; //释放临时空间
}