数据结构之二叉树(C/C++实现)
二叉树的特点
特点
- 每个结点最多有两颗子树,所以二叉树不存在度大于2的结点。
- 左子树和右子树是有顺序的,次序不能任意颠倒
- 即使树中只有一个子树,也要区分它是左子树和右子树
二叉树的五种基本形态
1.空二叉树
2.只有一个根节点
3.根节点只有左子树
4.根节点只有右子树
5.根节点只有左子树和右子树
特殊二叉树
1.斜树
所有节点只有左子树的二叉树叫左斜树
所有节点只有右子树的二叉树叫右叉树
2.满二叉树
在一颗二叉树中,如果所有分支节点都存在左子树和右子树,并且所有叶子节点都在同一层上,这样的二叉树称为满二叉树。
3.完全二叉树
对一颗具有n个节点的二叉树按层序编码,如果编码为i的节点与同样深度的满二叉树中编码为i的节点在二叉树中的位置完全相同,则这棵二叉树称为完全二叉树。
二叉树的性质
性质1:
在二叉树的第i层上至多有2^(i-1)个节点(i>=1)。
性质2:
深度为k的二叉树至多有2^k-1个节点。
性质3:
对任何一棵二叉树T,如果其终端结点数为n0,度为2的节点数为n2,则n0=n2+1
性质4:
具有n个节点的完全二叉树的深度为[log2N]([X]表示不大于x的最大整数)
性质5:
如果对一棵有n个结点的完全二叉树(其深度)的结点按层序编号(从第1层到最后一层)对任一结点有:
- 如果i=1,则结点i是二叉树的根,无双亲;如果i>1 ,则其双亲是结点[i/2]
- 如果2i>n,则结点i无左孩子(即节点i为叶子结点)
- 如果2i+1>n,则结点i无右孩子。
二叉树的存储结构
顺序存储结构
对于完全二叉树,由于其严格的定义,所以顺序存储结构也可以表现出二叉树的结构。
如下图所示:
对于一般的二叉树,尽管层序编号不能反映其逻辑关系,但可以将其按完全二叉树编号,只不过把不存在的节点设置为# 或者 其他符号。
如下图所示
链式存储结构
二叉链表的结构定义链表:
typedef struct BiTNode //节点结构
{
TElemType data; //节点数据
struct BiTNode *lchild, * rchild; //左右孩子指针
} BiTNode,*BiTree;
遍历二叉树
- 前序遍历: 规则是若二叉树为空,则空操作返回。否则先访问根节点,然后前序遍历左子树,再前序遍历右子树。
- 中序遍历: 规则是若二叉树为空,空操作返回。否则从根节点开始(注意不是先访问根节点),中序遍历根节点的左子树,然后访问根节点,然后中序遍历右子树。
- 后序遍历: 规则是若树为空,则空操作返回,否则从左到右先叶子后节点的方式遍历访问左右子树,最后是访问根节点。
- 层序遍历: 规则是若树为空,则空操作返回,否则从树的第一层,也就是根节点开始访问,从上而下逐层遍历,在同一层,按从左到右的顺序对节点逐个访问。
前序遍历算法
/*二叉树的前序遍历递归算法*/
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return;
printf("%c",T->data); /*显示结点数据,可以更改为其他对结点数据*/
PreOrderTraverse(T->lchild); /*再先序遍历左子树*/
PreOrderTraverse(T->rchild); /*最后先序遍历右子树*/
}
中序遍历算法
/*二叉树的中序遍历递归算法*/
void InOrderTraverse(BiTree T)
{
if(T==NULL)
return
InOrderTraverse(T->lchild); /*中序遍历左子树*/
printf("%c",T->data); /*显示节点数据,可以更改为其他对节点的操作*/
InOrderTraverse(T->rchild); /*最后中序遍历右子树*/
}
后序遍历算法
/*二叉树的后序遍历递归算法*/
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
printf("%c",T->data);
}
推导遍历结果
问:如果已知前序遍历序列为ABCDEF,中序遍历序列为CBAEDF
则该棵树的后序遍历结果是什么?
(1)由于前序遍历第一个字母是A,所以它是根节点。
对应于中序遍历的A,可以知道,CB是左子树,EDF是右子树。
(2)再看CB,由于前序遍历中,先打印B,所以C是B的孩子。而中序遍历中C先打印出来,B在其后。故C是B的左孩子。
(3)再看EDF,EDF为右子树,在前序遍历中,D先打印出来,所以D是E和F的祖先。在看中序遍历,E在D的左侧,F在D的右侧,因此E,F分别为D的左右孩子。
最终得到的二叉树为
后序遍历结果为后序遍历结果是:
CBEFDA
二叉树的遍历性质:
已知前序遍历序列和中序遍历序列,可以唯一确定一颗二叉树
已知后序遍历序列和中序遍历序列,可以唯一确定一颗二叉树
二叉树的建立
方法一:采用扩展二叉树的方法:
也就是将二叉树的每个结点的空指针引出一个虚节点。其值为一特地值,比如“#”。我们称这样处理后的二叉树称为原二叉树的扩展二叉树。
像下图这样:
实现算法如下:
/*按前序输入二叉树中节点的值(一个字符)*/
/*#表示空树,构造二叉链表表示二叉树T*/
void CreateBiTree(BiTree *T)
{
TElemType ch;
scanf("%c",&ch);
if(ch=='#')
*T=nullptr;
else
{
*T=(BiTree)malloc(sizeof (BiTNode));
if(!*T)
overflow_error("内存溢出");
(*T)->data=ch;
CreateBiTree(&(*T)->lchild); /*构造左子树*/
CreateBiTree(&(*T)->rchild); /*构造右子树*/
}
}
总结:其实建立二叉树和遍历二叉树原理是一样的,只不过在打印节点的地方变成了生成节点而已。
你完全可以用中序和后序遍历的方式实现二叉树的建立。
线索二叉树
原理参照下面文章:
https://www.jianshu.com/p/3965a6e424f5
线索二叉树的结构实现:
typedef struct BiThrNode
{
TElemType data; //结点数据
struct BiThrNode *lchild,*rchild; //左右孩子
PointerTag LTag;
PointerTag RTag; //左右标志
}BiThrNode,*BiThrTree;
中序遍历线索化实现:
BiThrTree pre; /*全局变量,始终指向刚刚访问过的结点*/
/*中序遍历进行中序线索化*/
void InThreading(BiThrTree p)
{
if(p)
{
InThreading(p->lchild); //递归左子树线索化
/*以下是线索化的功能*/
if(!p->lchild) //没有左孩子
{
p->LTag=Thread; //前驱线索
p->lchild=pre; //左孩子指针指向前驱
}
if(!pre->rchild) //前驱没有右孩子
{
pre->RTag=Thread; //后继线索
pre->rchild=p; //前驱右孩子指针指向后继。
}
pre=p;
InThreading(p->rchild);
}
}
总结:观察上述代码,你会发现,和二叉树中序遍历的递归代码几乎完全一样,只不过将本是打印代码的功能改成了线索化的功能。
另外我们可以发现,线索二叉树本质上就可以看作一个双向链表。
对已经线索化的二叉树进行遍历
/*T 指向头节点,头节点左链lchild指向根节点,头节点右链rchild指向中序遍历的最后
一个节点,中序遍历二叉线索链表表示二叉树*/
Status InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p;
p=T->lchild; //p指向根节点
while (p!=T) //空树或遍历结束时,p==T
{
while (p->LTag==Link) /*当LTag==0时循环到中序序列第一个节点*/
p=p->lchild;
printf("%c",p->data); /*显示节点数据,可以更改为其他对节点操作*/
while (p->RTag==Thread&&p->rchild!=T)
{
p=p->rchild;
printf("%c",p->data);
}
p=p->rchild; //p进至其右子根树
}
return OK;
}
线索二叉树由于充分利用了空指针域的空间(这等于节省了空间),又保证创建时的一次遍历就可以终生受用前驱和后继的信息,(这意味着节省了时间),所以在实际问题中,如果所用的二叉树需要经常遍历或查找节点时需要某种遍历序列中的前驱和后继,那么采用线索二叉树是非常不错的选择
树,森林与二叉树的转换
参考文章:
https://blog.csdn.net/jiashuai94/article/details/80760041
赫夫曼树及其应用
最基本的压缩编码方法:赫夫曼编码
参考文章:
https://www.cnblogs.com/ciyeer/p/9045897.html