树与二叉树
1、树的基本概念
树的节点代表【集合】,树的边代表【关系】
树的其他表示方法
2、二叉树
定义:
(1)二叉树每个节点至多只有两棵子树(二叉树中不存在度大于2的节点);
(2)二叉树的子树有左右之分,其次序不能任意颠倒。
特殊种类:
性质:
-
在二叉树的第i层上至多有2i−1 (i≥1)个节点。
-
深度为k的二叉树至多有2k−1(k≥1)个节点。
-
对任何一棵二叉树T,如果其终端节点数为n0,度为2的节点数为n2,则n0 = n2 + 1。
-
具有n个节点的完全二叉树的深度为[log2n] + 1(向下取整)
作用:
4、二叉树的相关操作
4.1链式存储结构
二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法,我们称这样的链表叫做二叉链表。
typedef struct BiTNode{
TElemType data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
4.2二叉树的递归遍历
4.2.1二叉树的先序遍历
【算法思路】
- 若二叉树为空,则什么也不做,否则,
1)访问根结点;
2)先序遍历左子树;
3)先序遍历右子树。
void PreOrder(BiTree T){
if(T!=NULL)
{
printf("%d",T->data);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
4.2.2二叉树的中序遍历
【算法思路】
- 若二叉树为空,则什么也不做,否则,
1)中序遍历左子树;
2)访问根结点;
3)中序遍历右子树。
void InOrder(BiTree T)
{
if(T!=NULL)
{
InOrder(T->lchild);
printf("%d",T->data);
InOrder(T->rchild);
}
}
4.2.3二叉树的后序遍历
【算法思路】
- 若二叉树为空,则什么也不做,否则,
1)后序遍历左子树;
2)后序遍历右子树;
3)访问根结点。
void PostOrder(BiTree T)
{
if(T!=NULL)
{
PostOrder(T->lchild);
PostOrder(T->rchild);
printf("%d",T->data);
}
}
【注意】在递归遍历中,递归工作栈的栈深恰好为树的深度,所以在最坏情况下,二叉树是有n个结点且深度为n的单支树,遍历算法的空间复杂度为O(n)。不管采用哪种遍历算法,每个结点都访问一次且仅访问一次,故时间复杂度都是O(n)。
4.3二叉树的非递归遍历
在递归的方式里面,我们借助递归的函数自身调用自身的特点,在二叉树里无限向下挖,直至挖到递归出口,先序遍历里面我们发现了最后一个节点的左孩子或者是右孩子不存在,中序遍历里面我们发现了最后一个节点的左孩子不存在,后序遍历里我们发现了它的左孩子不存在。发现了递归出口之后,我们不得不重新回到上一层递归,继续判断上一层递归的孩子们还有没有没有遍历到的,之后继续遍历下去。
对此这种特殊的结构算法,我们发现可以使用非递归的方式进行遍历。
4.3.1中序遍历
【算法思路】
借助栈,我们来分析中序遍历的访问过程:
- 沿着根的左孩子,依次入栈,直到左孩子为空,说明已找到可以输出的结点,输出此节点。
- 栈顶元素出栈并访问:若其右孩子为空,继续执行步骤2;若其右孩子不空,将右子树转执行步骤1。
void PreOrder2(BiTree T)
{
InitStack(S);
BiTree p=T;
while(p||!IsEmpty(S))
{
if(p)
{
Push(S,p);
p=p->lchild;
}
else
{
Pop(S,p);
visit(p);//访问弹出的节点。
p=p->rchild;
}
}
}
4.3.2先序遍历
【算法思路】
和中序遍历类似,我们只需要把访问节点操作放在入栈操作前即可。
void PreOrder2(BiTree T)
{
IniStack(S);
BiTree p=T;
while(p||IsEmpty(S))
{
if(p)
{
visit(p);//访问节点。
Push(S,p);
p=p->lchild;
}
else
{
Pop(S,p);
p=p->rchild;
}
}
}
4.3.3后序遍历
在后序遍历里存在特殊情况,由于是左右根,弹出栈的第一个元素是最左边的元素,之后判断其双亲有没有右孩子,如果有那么就入栈,之后判断右孩子有没有孩子(还是先遍历到最左的节点)。第二次到达双亲的时候,我们要判断这个回是从左孩子回的还是从右孩子回的,于是我们需要设定一个辅助指针r,指向最近访问的节点,帮助我们遍历。
void PostOrder(BiTree T) {
InitStack(s);
BiTree *p = T, *r = NULL;
while (p != NULL || !IsEmpty(s)) {
if (p != NULL) {
push(s, p);
p = p->lchild;
} else {
GetTop(s, p);
if (p->rchild && p->rchild != r) {
p = p->rchild;
push(s, p);
p = p->lchild;
} else {
pop(s, p);
visit(p->data);
r = p;
p = NULL;
}
}
}
}
4.4二叉树的基本操作
4.4.1建立二叉链表
【算法步骤】
① 查找字符序列,读入字符ch。
② 如果ch是一个“@”字符,则表明该二叉树为空树,即T为NULL;否则执行以下操作:
-
申请一个节点空间T;
-
将ch赋给T->data;
-
递归创建T的左子树;
-
递归创建T的右子树。
BinTree CreatBinTree()
{
ElementType ch;
BinTree T;
scanf("%c",&ch);
if(ch == '@')
T = NULL;
else {
T = (BinTree)malloc(sizeof(struct TNode));
T->Data = ch;
T->Left = CreatBinTree();
T->Right = CreatBinTree();
}
return T;
}
【注意】这里演示的是先序遍历的顺序建立二叉链表,如果是中序或后序改变else中的顺序即可。
4.4.2计算二叉树的深度
【算法步骤】
如果是空树,递归结束,深度为0,否则执行以下操作:
-
递归计算左子树的深度记为m;
-
递归计算右子树的深度记为n;
-
如果m大于n,二叉树的深度为m+1,否则为n+1。
int Depth(BiTree T)
{
int m=0,n=0;
if(T==NULL) return 0;
else
{
m=Depth(T->lchild);
n=Depth(T->rchild);
if(m>n)
return m+1;
else
return n+1;
}
}
4.4.3计算二叉树的节点个数
直接计算做法:
int NodeCount(BiTree T)
{
if(T==NULL) return 0;
else return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}
通过度数进行计算:
int deg_two=0,deg_one=0,deg_leaf=0;
int NodeCount(BiTree T)
{
if(T==NULL) return;
if(T->lchild==NULL&&T->rchild==NULL)
deg_leaf++;
if((T->lchild!=NULL&&T->rchild==NULL)||(T->rchild!=NULL&&T->lchild==NULL))
deg_one++;
if(T->lchild&&T->rchild)
deg_two++;
NodeCount(T->lchild);
NodeCount(T->rchild);
}
【注意】这里也可以通过n0=n2+1进行计算。
4.4.4复制二叉树
【算法步骤】
如果是空树,递归结束,否则执行以下操作:
-
申请一个新节点空间,复制根节点;
-
递归复制左子树;
-
递归复制右子树。
void Copy(BiTree T,BiTree *NewT)
{
if(T==NULL){
NewT=NULL;
return ;
}
else
{
BiTNode NewT=(BiTNode*)malloc(sizeof(BiTNode));
NewT->data=T->data;
Copy(T->lchild,NewT->lchild);
Copy(T->rchild,NewT->rchild);
}
}
4.5线索二叉树
4.5.1线索二叉树的储存结构
typedef struct BiThrNode
{
TElemType data;
struct BiThrNode *lchild,*rchild;
int LTag,RTag;
}BiThrNode,*BiThrTree ;
4.5.2线索二叉树的建立
【算法步骤】
① 如果p非空,左子树递归线索化。
② 如果p的左孩子为空,则给p加上左线索,将其LTag置为1,让p的左孩子指针指向pre(前驱);否则将p的LTag置为0。
③ 如果pre的右孩子为空,则给pre加上右线索,将其RTag置为1,让pre的右孩子指针指向p(后继);否则将pre的RTag置为0。
④ 将pre指向刚访问过的节点p,即pre = p。
⑤ 右子树递归线索化。
void InThreading(BiThrTree p)
{
if(p)
{
InThreading(p->lchild);
if(!p->lchild)
{
p->Ltag=1;
p->lchild=pre;
}
else p->LTag=0;
if(!pre->rchild)
{
pre->Rtag=1;
pre->rchild=p;
}
else pre->Rtag=0;
pre=p;
InThreading(p->rchild);
}
}
4.5.3遍历线索二叉树
以中序遍历为例。
【算法步骤】
① 指针p指向根节点。
② p为非空树或遍历未结束时,循环执行以下操作:
-
沿左孩子向下,到达最左下节点*p,它是中序的第一个节点;
-
访问*p;
-
沿右线索反复查找当前节点*p的后继节点并访问后继节点,直至右线索为0或者遍历结束;
-
转向p的右子树。
void InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p=T->lchild;
while(p!=T)
{
while(p->Ltag==0) p=p->lchild;
printf("%d",p->data);
while(p->Rtag==1&&p->rchild!=T)
{
p=p->rchild;
printf("%d",p->data);
}
p=p->rchild;
}
}