此外在这里在介绍下完美二叉树的概念及重要性质
完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。
(1)所有的叶结点都出现在第k层或k-l层(层次最大的两层)
(2)对任一结点,如果其右子树的最大层次为L,则其左子树的最大层次为L或L+l。
仔细看这篇博客:https://blog.csdn.net/weixin_42110638/article/details/83796618
如果将层序遍历中的队列改为堆栈,是否也是一种树的遍历?可以应用这种方法改造出一种前序、中序、后序的非递归遍历吗?
参考要点:
1. 层序遍历与其他三种(前序、中序和后序遍历)的主要区别是:层次遍历通过各结点一次性获得其左右儿子结点信息并借助队列以自顶向下的顺序,形成按宽度优先方式遍历二叉树。而其他三种遍历均是各结点借助堆栈的逆序特点,按自底向上的顺序逐步处理左右儿子,形成按深度优先方式遍历二叉树。
2. 如果将层序遍历程序中的队列直接改为堆栈,同时将入栈(原来是入队)顺序改为先右儿子再左儿子,其遍历输出结果就是前序遍历。
3.对于中序遍历和后序遍历,关键是要判别左子树和右子树什么时候遍历结束。如果将层序遍历程序中的队列改为堆栈,同时仍然按照层次遍历的整体控制思路,对出入栈操作做些修改,仍然可以实现中序遍历和后序遍历。要点是:在原来程序控制过程中,结点出队后是将其左右儿子入队;现在可将控制过程改为:结点出栈后,将当前结点“加塞”在其左右儿子中再次入栈。如果“加塞”在左右儿子之间再次入栈就是中序遍历,如果“加塞”在左右儿子之前再次入栈就是后序遍历。也就是每个结点做两次出入栈处理(类似后序遍历的非递归程序):
(1) 中序遍历要点:结点出栈时,如果是第一次出栈,按照右儿子、当前结点、左儿子顺序入栈,即左儿子在栈顶,当前结点此时是第二次入栈。将来如果第二次出栈则直接输出。
(2) 后序遍历要点:过程与上述中序遍历一样,唯一区别是入栈顺序为:当前结点、右儿子、左儿子。
【例】二叉树的建立
Status CreateBiTree(BiTree &T) //&的意思是传进来节点指针的引用,括号内等价于 BiTreeNode* &T,目的是让传递进来的指针发生改变
{
char c;
cin >> c;
if(c == ' ') //当遇到 时,令树的根节点为NULL,从而结束该分支的递归
T = NULL;
else
{
if(!(T = (BiTNode*)malloc(sizeof(BiTNode))));
exit(OVERFLOW);
//T = new BiTreeNode;
T->data=c;//生成根节点
createBiTree(T->lchild);//构造左子树
createBiTree(T->rchild);//构造右子树
}
}
重点例题:已知两种遍历序列让你还原二叉树
给道例题:https://blog.csdn.net/weixin_42110638/article/details/83796077
<1>线索二叉树的原理
通过考察各种二叉链表,不管儿叉树的形态如何,空链域的个数总是多过非空链域的个数。准确的说,n各结点的二叉链表共有2n个链域,非空链域为n-1个,但其中的空链域却有n+1个。如下图所示。
因此,提出了一种方法,利用原来的空链域存放指针,指向树中其他结点。这种指针称为线索。
记ptr指向二叉链表中的一个结点,以下是建立线索的规则:
(1)如果ptr->lchild为空,则存放指向中序遍历序列中该结点的前驱结点。这个结点称为ptr的中序前驱;
(2)如果ptr->rchild为空,则存放指向中序遍历序列中该结点的后继结点。这个结点称为ptr的中序后继;
显然,在决定lchild是指向左孩子还是前驱,rchild是指向右孩子还是后继,需要一个区分标志的。因此,我们在每个结点再增设两个标志域ltag和rtag,注意ltag和rtag只是区分0或1数字的布尔型变量,其占用内存空间要小于像lchild和rchild的指针变量。结点结构如下所示。
其中:
(1)ltag为0时指向该结点的左孩子,为1时指向该结点的前驱;
(2)rtag为0时指向该结点的右孩子,为1时指向该结点的后继;
(3)因此对于上图的二叉链表图可以修改为下图的养子。
<2>线索二叉树结构实现
二叉线索树存储结构定义如下:
/* 二叉树的二叉线索存储结构定义*/
typedef enum{Link, Thread}PointerTag; //Link = 0表示指向左右孩子指针;Thread = 1表示指向前驱或后继的线索
typedef struct BitNode
{
char data; //结点数据
struct BitNode *lchild, *rchild; //左右孩子指针
PointerTag Ltag; //左右标志
PointerTag rtal;
}BitNode, *BiTree;
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继信息只有在遍历该二叉树时才能得到,所以,线索化的过程就是在遍历的过程中修改空指针的过程。
BiTree pre; //全局变量,始终指向刚刚访问过的结点
//中序遍历进行中序线索化
void InThreading(BiTree p)
{
if(p)
{
InThreading(p->lchild); //递归左子树线索化
//===
if(!p->lchild) //没有左孩子
{
p->ltag = Thread; //前驱线索
p->lchild = pre; //左孩子指针指向前驱
}
if(!pre->rchild) //没有右孩子
{
pre->rtag = Thread; //后继线索
pre->rchild = p; //前驱右孩子指针指向后继(当前结点p)
}
pre = p;
//===
InThreading(p->rchild); //递归右子树线索化
}
}
上述代码除了//===之间的代码以外,和二叉树中序遍历的递归代码机会完全一样。只不过将打印结点的功能改成了线索化的功能。
中间部分代码做了这样的事情:
因为此时p结点的后继还没有访问到,因此只能对它的前驱结点pre的右指针rchild做判断,if(!pre->rchild)表示如果为空,则p就是pre的后继,于是pre->rchild = p,并且设置pre->rtag = Thread,完成后继结点的线索化。如图:
if(!p->lchild)表示如果某结点的左指针域为空,因为其前驱结点刚刚访问过,赋值了pre,所以可以将pre赋值给p->lchild,并修改p->ltag = Thread(也就是1)以完成前驱结点的线索化。
完成前驱和后继的判断后,不要忘记当前结点p赋值给pre,以便于下一次使用。
有了线索二叉树后,对它进行遍历时,其实就等于操作一个双向链表结构。
和双向链表结点一样,在二叉树链表上添加一个头结点,如下图所示,并令其lchild域的指针指向二叉树的根结点(图中第一步),其rchild域的指针指向中序遍历访问时的最后一个结点(图中第二步)。反之,令二叉树的中序序列中第一个结点中,lchild域指针和最后一个结点的rchild域指针均指向头结点(图中第三和第四步)。这样的好处是:我们既可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。
//t指向头结点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的最后一个结点。
int InOrderThraverse_Thr(BiTree t)
{
BiTree 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;
}
说明:
(1)代码中,p = t->lchild;意思就是上图中的第一步,让p指向根结点开始遍历;
(2)while(p != t)其实意思就是循环直到图中的第四步出现,此时意味着p指向了头结点,于是与t相等(t是指向头结点的指针),结束循环,否则一直循环下去进行遍历操作;
(3)while(p-ltag == Link)这个循环,就是由A->B->D->H,此时H结点的ltag不是link(就是不等于0),所以结束此循环;
(4)然后就是打印H;
(5)while(p->rtag == Thread && p->rchild != t),由于结点H的rtag = Thread(就是等于1),且不是指向头结点。因此打印H的后继D,之后因为D的rtag是Link,因此退出循环;
(6)p=p->rchild;意味着p指向了结点D的右孩子I;
(7).....,就这样不断的循环遍历,直到打印出HDIBJEAFCG,结束遍历操作。
从这段代码可以看出,它等于是一个链表的扫描,所以时间复杂度为O(n)。
由于充分利用了空指针域的空间(等于节省了空间),又保证了创建时的一次遍历就可以终生受用后继的信息(意味着节省了时间)。所以在实际问题中,如果所用的二叉树需要经过遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。
//双亲存储的结构类型定义
typedef struct PTNode{
TElemType data; //数据域
int parent; //双亲的位置,根节点的双亲为-1
}PTNode; //双亲的节点类型
typedef struct {
PTNode *nodes; //初始化分配的结点数组
int r,nodeNum; //根的位置和结点数
}PTree; //树的双亲存储结构类型
优点: 可用parent直接找到双亲,并且很容易找到祖先。
缺点:需要查找节点的孩子及其子孙时要遍历整个结构。
//双亲孩子存储结构的类型定义
typedef struct ChildNode{
int childIndex; //孩子在结点数组的位置
struct ChildNode *nextChild; //下一个孩子
}ChildNode; //孩子链表中的节点类型
typdef struct{
TElemType data; //元素值
int parent; //双亲的位置
struct ChildNode *firstChild; //孩子链表头指针
}PCTreeNode; //双亲节点的节点类型
typedef struct{
PCTreeNode *nodes; //节点数组
int nodeNum,r; //结点元素的个数,根位置
}PCTree; //树的双亲孩子存储结构类型
//孩子兄弟链表的类型定义
typedef struct CSTNode{
TElemType data; //数据域
struct CSTNode *firstChild,*nextSibling; //最左孩子指针,右兄弟指针
}CSTnode,*CSTree,*CSForest; //孩子兄弟链表
Status InitTree(CSTree &T); //构造空树
CSTree MakeTree(TElemType e,int n....) //创建根结点为e和n颗子树的树
Status DestroyTree(CSTree &T) //销毁树
int TreeDepth(CSTree T) //返回树的深度
CSNode *Search(CSTree T,TElemType e); //查找树T中的节点e并返回其指针
Status InesertChild(CSTree &T,int i,CSTree c) //插入c为T的第i颗子树,c非空并且与T不相交
Status DeleteChild(CSTree &T,int i) //删除第i棵子树
创建树
#include// 标准头文件,提供宏va_start、va_arg和va_end,
CSTree MakeTree(TElemType e,int n....){
int i;
CSTree t,p,pi;
va_list argptr; //存放变长参数表信息的数组
t=(CSTree)malloc(sizeof(CSTNode));
if(t=NULL) return NULL;
t->data=e; //根结点的值为e;
t->firstChild=t->nextSibling=NULL;
if(n<=0) return t; //若无子树,则返回根结点
va_start(argptr,n) //令argptr 指向n后的第一个实参
p=va_arg(argptr,CSTree); //取第一棵子树的实参转化为CSTree类型
t->firstChild=p;
pi=p;
for(i=1;inextSibling=p;
pi=p;
}
va_end(argptr);
return t;
}
//可变参数的使用
使用可变参数应该有以下步骤(要加入):
1)首先在函数里定义一个va_list型的变量,这里是argptr,这个变 量是指向参数的指针.
2)然后用va_start宏初始化变量argptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数.
3)然后用va_arg返回可变的参数,并赋值给变量p(CSTree类型). va_arg的第二个 参数是你要返回的参数的类型,这里是CSTree类型.然后你就可以在函数里使用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获取各个参数.
4)最后用va_end宏结束可变参数的获取.
插入第i个子树
Status InesertChild(CSTree &T,int i,CSTree c){
int j;
CSTree p;
if(NULL==T||i<1) return ERROR;
if(i==1){ //c为第一棵插入子树
c->nextSibling=T->firstChild;
T->firstChild=c; //T成为T的第一棵子树
}else{
p=T->firstChild;
for(j=2;p!=NULL&&jnextSibling; //寻找插入位置
}
if(j==i){
c->nextSibling = p->nextSibling;
p->nextSibling = c;
}else return ERROR;
}
return OK;
}
求树的深度
int TreeDepth(CSTree T) { // 求树T的深度
int dep1, dep2, dep;
if(NULL==T) dep = 0; // 树为空,深度则为0
else {
dep1 = TreeDepth(T->firstChild); // 求T的子树森林的深度
dep2 = TreeDepth(T->nextSibling); // 求除T所在树以外的其余树的深度
dep = dep1+1>dep2 ? dep1+1 : dep2; // 树的深度
}
return dep;
}
查找树
CSTreeNode* Search(CSTree T, TElemType e) {
// 查找树T中的结点e并返回其指针
CSTreeNode* result = NULL;
if(NULL==T) return NULL; // 树为空,返回NULL
if(T->data==e) return T; // 找到结点,返回其指针
if((result = Search(T->firstChild, e))!=NULL) // 在T的子树森林查找
return result;
return Search(T->nextSibling, e);
// 在除T所在树以外的其余树构成的森林查找
}
哈夫曼树这里涉及到一些关于其他树的知识,比如二叉搜索树,二叉平衡树,堆(最好这几个知识都要学会,因为非常重要)
本文对这些知识不做详解,详细的可以参考我的这几篇博客
二叉搜索树:https://blog.csdn.net/weixin_42110638/article/details/83963764
二叉平衡树:https://blog.csdn.net/weixin_42110638/article/details/83963954
堆:https://blog.csdn.net/weixin_42110638/article/details/83982381
代码实现:
这里其实就用了堆的思想!!!
这个创建的过程最主要的就是每次选两个最小的,这里其实就是堆的思想,你把结点的权值构造出最小堆,每次取两个最小堆,并在一起,形成的新堆插进去