目录
一、二叉树
- 二叉树:每个结点的度均不大于2的有序树。
- 斜树:所有结点都只有左子树的二叉树叫左斜树,所有结点都只有右子树的二叉树叫右斜树。统称为斜树。
- 满二叉树:所有分支结点(非终端结点)都有左右子树,且所有叶子结点(终端结点)在同一层次的二叉树。
- 完全二叉树:对于一棵具有n个结点的二叉树按层序编号,如果编号为i(1≤i≤n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵树为完全二叉树。 (直观解释:除了最后一层外其他层都是满二叉树的二叉树,且最后一层的结点从左往右按顺序分布。)
二叉树的性质
- 在二叉树的第i层至多有
个结点(i≥1)。
- 深度为K的二叉树至多有
-1个结点(k≥1)。
- 对于任意一棵二叉树,叶子结点数为
,度为2的结点数为
,则
=
+1。
推导:树中的边数为结点数n-1,即 2
+
=n-1
而 n-1=
+
+
-1
故 2
+
=
+
+
-1
- 具有n个结点的完全二叉树的深度为:⌊ logn⌋+1(⌊ logn⌋表示不大于X的最大整数)。
- 如果对一棵有n个结点的完全二叉树 (其深度为⌊ logn⌋+1)的结点按层序编号(从第1层到第⌊ logn⌋+1层,每层从左到右),对任一结点i(1≤i≤n)有:
- 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点⌊ i/2⌋。
- 如果2i>n,则结点i为叶子结点;否则其左孩子是结点2i。
- 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。
二叉树的存储结构
- 顺序储存结构: 用数组自上而下,自左至右储存完全二叉树上的结点元素,0号单元储存根结点。一般二叉树数组中一些位置填0表示不存在此结点,会占用不必要空间。故数组一般只用来存储完全二叉树。
- 链式储存结构:由二叉树的定义可知,链表中的结点数据项包括数据域和左、右指针域。链表的头指针指向根结点,这样的链表又称为二叉链表。如果需要方便找一个结点的父结点,可以定义一个三叉链表额外储存结点的父结点指针域。
二叉树的遍历
对于线性表遍历的顺序是一条直线,对于二叉树遍历需要遵循某种关系。遍历后其实就是把树中的结点变成某种有意义的线性序列,用于程序实现。
遍历二叉树:按某条搜索路径巡访树中的每个结点,使每个结点均只被访问一次。
- 前序遍历:若二叉树为空,则空操作1.访问根结点 2.前序遍历左子树 3.前序遍历右子树
- 中序遍历:若二叉树为空,则空操作1.中序遍历左子树 2.访问根结点 3.中序遍历右子树
- 后序遍历:若二叉树为空,则空操作1.后序遍历左子树 2.后序遍历右子树 3.访问根结点
- 层序遍历:若二叉树为空,则空操作,从树的第一层从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问
//前序代码,中序后序改变一下代码的顺序就行
void ProOrderTraverse(BiTree T)
{ if(T==NULL)
return;
printf("%d",T->data);//也可以是其他对结点的操作
ProOrderTraverse(T->lchild);//先序遍历左子树
ProOrderTraverse(T->rchild);//先序遍历右子树
}
已知前序、层序或者后序序列其中之一,可直观知道根结点。
再知道中序序列可以画出树的逻辑图,但只知前序、层序和后序序列不行。
二、线索二叉树的基本概念和构造
构造二叉树的二叉链表中,存在结点的左指针域、右指针域为空,浪费内存资源。n个结点共有2n个指针域,分支线数为n-1(除了根结点每个结点都有一个指针指向它),所以空指针域有n+1个。
在进行遍历二叉表后,我们能够知道遍历操作时结点的前驱和后继,但是以后每次我们想知道结点的前驱和后续都要再遍历一次,在此思路上可以在空指针域里存放某种遍历操作时结点的前驱和后继的线索。
- 指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称作线索二叉树
- 对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化。
一般用二叉树的空左孩子指针指向前驱,空右孩子指针指向后继。每个结点再增设两个标志域ltag和rtag来判断lchild指针指的是左孩子还是前驱、rchild指的是右孩子还是后继。标志域只是存放0或者1的布尔型变量,其占用内存小于指针变量。
二叉树的线索存储结构定义代码:
//二叉树的二叉线索存储结构定义
typedef enum {Link,Thread} PointerTag; /* Link=0表示指向左右孩子指针, */
/* Thread=1表示指向前驱或后继的线索 */
typedef struct BiThrNode /* 二叉线索存储结点结构 */
{
TElemType data; /* 结点数据 */
struct BiThrNode *lchild, *rchild; /* 左右孩子指针 */
PointerTag LTag;
PointerTag RTag; /* 左右标志 */
} BiThrNode;
中序遍历线索化的递归函数代码:
线索化的过程即为遍历过程中结点操作为修改该结点空指针的过程,附设一个指针pre表示刚刚访问的结点,即当前结点p的前驱。
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//前驱右孩子指针指向后继(当前结点p)
}
pre=p;//保持pre指向p的前驱
InThreading(p->rchild)//递归右子树线索化
}
}
结点操作为:
- 如果当前结点的左指针域为空,则让它指向前驱
- 如果前驱的右指针域为空,则让它指向后继
- 让pre指针跟上当前结点
根据函数递归,首先函数进入B结点对B线索化使B的左指针域指向pre后并且pre为B了,然后返回A判断pre的右指针域为空,然后pre(此时已为B)的右指针域指向后继A,然后pre为A,函数接着返回C……
在中序线索树中,对于右孩子指针不储存后继的结点,它的后继是遍历其右子树时访问的第一个结点,即右子树最左下的结点。同样对于左孩子指针不储存前驱的结点,它的前驱是遍历其左子树时最后一个访问的结点。
线索二叉树的遍历代码:
有了线索二叉树,就可以对其进行更为高效的遍历了,时间复杂度为O (n)。
//T指向头结点,头结点的lchild指向根结点,头结点的rchild指向中序遍历的最后一个结点
status InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p;
p=T->lchild;//p指向根结点
while(p!=T)//空树或遍历结束时,P==T
{
while(p->LTag==Link)//如果存在左子树,向下递归到左下角
p=p->lchild;
Visit(p->data);//访问左子树为空的结点
while(p->Rtag==Thread&&p->rchild!=T)//第8行
{
p=p->rchild;
Visit(p->data);//访问后继结点
}
p=p->rchild//第13行,P进至右子树根
}
return OK;
}
上述代码的第8行到第13行实现的是B→A(访问A)→C,或者B→C(不访问,进行后续判断)