树
树的基本概念
树的递归式定义: 树Tree 是n(n>=0)个节点的有限集。n=0称为空树。在任意一棵非空树中:(1)有且仅有一个特定的结点,被称为 根(root) ;(2)当n>1时,其余节点可以分为m(m>0)个互不相交的有限集T1、T2 、Tm,其中每一个集合本身又是一棵树,并且称为根的 子树(SubTree) 。
结点的分类: 结点拥有的子树个数称为结点的度(Degree) 度为0的结点称为叶结点(Leaf)或终端结点。否则称为非终端结点或分支结点,除了根结点外,分支结点称为内部结点。树的度是树内各结点度最大值。
结点之间的关系:结点的子树的根称为Child,结点称为子树根的Parent,同一个Parent的Child之间互称Sibling。结点的祖先:从根到该结点分支上的所有结点。同理,以结点为根的子树中的任意结点称为其子孙。
结点的层次Level:根为第一层,跟的直系Child为第二次,若某内部结点在第m层,则其子树的根就在m+1层。同一层但不是Sibling互称为堂兄弟。树中结点的最大Level称为树的深度Depth或高度
**森林(Forest)**是m棵互不相交的树的集合。对于树中的每个结点而言,其子树的集合即为森林。
特点总结:根结点无双亲且唯一,叶结点无孩子,中间结点一个双亲1个以上的孩子。线性表和树的对比如下。
树的存储结构
①双亲表示法:除了跟结点,每个结点都有且仅有一个双亲。
其中data是数据域,parent是指针域,存储该结点双亲在数组中的下标。
/*树的双亲表示法*/
#define MAX_TREE_SIZE 100
typedef int TElemType;
typedef struct PTNode
{
TElemType data;/*结点数据*/
int parent;/*双亲位置*/
}PTNode;
typedef struct
{
PTNode nodes[MAX_TREE_SIZE];/*实例化结构数组*/
int r,n;/*跟的位置和结点数*/
}PTree;
约定根结点位置域设置为-1
②孩子表示法:数组每个元素的指针域指向一个链表,链表里存储着其孩子结点。
/*树的孩子表示法*/
#define MAX_TREE_SIZE 100
typedef struct CTNode/*孩子结点*/
{
int child;/*存储孩子的下标*/
struct CTNode *next;
}CTNode,*ChildPtr;
typedef struct
{
TElemType data;
ChildPtr firstchild;
}CTBox;
typedef struct
{
CTBox nodes[MAX_TREE_SIZE];
int r,n;
}CTree;
③双亲孩子表示法:
④孩子兄弟表示法:
分为数据域和最左侧孩子域以及右侧兄弟域。
此方法用链表实现,结构图比较形象地还原了树的结构
此方法将树变成了二叉树
二叉树
二叉树的递归式定义:非空二叉树由一个根结点和两棵互不相交的、分别成为根结点的左子树和右子树的两棵二叉树组成。
二叉树的特点:不存在度大于2的结点,度为0和1都是可以的。左子树和右子树是有次序的,不可以颠倒。即使一个结点只有一棵非空子树,也要区分其左右。
二叉树的五种基本形态:
1.空二叉树。
2.只有一个根结点。
3.根结点只有左子树。
4.根结点只有右子树。
5.根结点左右子树都有。
二叉树相比于普通树而言,需要区分左右。
满二叉树:所有分支结点都存在左子树和右子树,并且所有叶子都在同一层。
完全二叉树:对二叉树按层序编号,其中结点的编号与同样深度的满二叉树对应位置的结点编号完全相同。[可以理解为在满二叉树的弹匣上填子弹,只是没有填满,因此具有满二叉树的雏形]
如下图按层序编号会发现不连续的情况,由于编号不能跳过,所以会和同样深度的满二叉树不同,因此下述情况不是完全二叉树。
二叉树的性质
第i层之多有2i-1(i$\geq$1)个结点
深度为k的二叉树至多有2k-1个结点
二叉树T,终端结点数为n0,度2的结点数为n2,有如下关系n0=n2+1
具有n个结点的完全二叉树深度为 [ l o g 2 n ] + 1 [log_2n]+1 [log2n]+1,[ ]表示向下取整
按层序编号的完全二叉树:
1.若编号i=1,则为二叉树的根。若i>1,则其双亲结点编号为[i/2]
2.如果2i>n,则结点i无左孩子。否则有左孩子,是结点2i
3.如果2i+1>n,则结点i无右孩子。否则有右孩子,是结点2i+1
二叉树的顺序存储结构
由于完全二叉树的定义严格,因此使用顺序结构可以表现完全二叉树的结构(依据层序编号)
对于一般的二叉树,可以将其按照完全二叉树进行编号,只不过把不存在的结点设置为“^”。
顺序结构一般用于完全二叉树,否则会或多或少造成存储的浪费。
二叉链表
二叉树由一个数据域和两个指针域,这样的链表称作二叉链表。
/*二叉链表的结点结构*/
typedef struct BiTNode
{
TElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
结构示意图如下图
如果需要,可以增加指向双亲的指针域,形成三叉链表。
遍历二叉树(traversing binary tree)
前序遍历(递归式定义):若二叉树为空,则空操作返回,否则先访问根结点,然后依次前序遍历左子树,再前序遍历右子树。[结点-左子树-右子树]
使用递归方式实现前序遍历
/*二叉树的前序遍历*/
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return;
print("%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);
}
后续遍历:定义可以从前序遍历和中序遍历中进行引申。[左子树-右子树-根]
层序遍历:若树为空,则空操作返回。否则从树的第一层,从上而下,从左至右逐个访问。
层序遍历的方法:借助队列先入先出的原则,依次将结点压入队列,并在弹出时将其左右孩子(如果非空),加入队列末尾,直至队列全部弹出为空。
//PTA 数据结构6-9
/*****队列部分的定义和函数*******/
typedef struct QNode1{
BinTree Data;//队列用于存放二叉树结点地址
struct QNode1 *next;
}QNode1,*QueuePtr;
typedef struct Queue1{
QueuePtr first,rear;
}Queue1,*LinkQueue;
void QueueAdd(LinkQueue Q,BinTree Data);
BinTree QueuePop(LinkQueue Q);
void QueueAdd(LinkQueue Q,BinTree Data){
if(Q->rear!=NULL){//非空队列,则在尾部加入
QueuePtr AddPtr;
AddPtr=(QueuePtr)malloc(sizeof(QNode1));//分配空间填入数据
AddPtr->Data=Data;
AddPtr->next=NULL;
Q->rear->next=AddPtr;//连接
Q->rear=AddPtr;
}
else{//空队列,则先创建
QueuePtr AddPtr;
AddPtr=(QueuePtr)malloc(sizeof(QNode1));//分配空间填入数据
AddPtr->Data=Data;
AddPtr->next=NULL;
Q->first=AddPtr;
Q->rear=AddPtr;
}
return;
}
BinTree QueuePop(LinkQueue Q){//假设是非空,主程序使用此函数前要判断非空
//队列前部出列,分情况,只有1个元素时,需要同时改变first和rear
BinTree pushdata;
pushdata=Q->first->Data;
printf(" %c",pushdata->Data);
if(Q->first->next==NULL){//队列仅含一个元素时
Q->first=NULL;
Q->rear=NULL;
return pushdata;
}
else{//队列含有多个元素时
Q->first=Q->first->next;
return pushdata;
}
}
/*****遍历部分的函数*******/
void InorderTraversal( BinTree BT ){
if(BT==NULL)
return;
InorderTraversal(BT->Left);
printf(" %c",BT->Data);
InorderTraversal(BT->Right);
}
void PreorderTraversal( BinTree BT ){
if(BT==NULL)
return;
printf(" %c",BT->Data);
PreorderTraversal(BT->Left);
PreorderTraversal(BT->Right);
}
void PostorderTraversal( BinTree BT ){
if(BT==NULL)
return;
PostorderTraversal(BT->Left);
PostorderTraversal(BT->Right);
printf(" %c",BT->Data);
}
void LevelorderTraversal( BinTree BT ){
//创建辅助队列
LinkQueue Qaiss;
Qaiss=(LinkQueue)malloc(sizeof(Queue1));
BinTree BTpop;
Qaiss->first=NULL;
Qaiss->rear=NULL;
//
//将二叉树根进入队列
if(BT!=NULL){
QueueAdd(Qaiss,BT);
while(Qaiss->first!=NULL){//当队列不为空时
BTpop=QueuePop(Qaiss);
if(BTpop->Left!=NULL)
QueueAdd(Qaiss,BTpop->Left);
if(BTpop->Right!=NULL)
QueueAdd(Qaiss,BTpop->Right);
}
}
}
遍历的作用是将树按照一定顺序转化为线性序列。
根据前序和中序或者后序和中序可以唯一确定一棵二叉树。
二叉树的建立
扩展二叉树:在每个结点的空指针处引出一个虚结点,如下图
扩展二叉树的好处是可以根据遍历序列唯一确定一棵二叉树。如上图的前序遍历序列为AB#D##C##
//递归方式根据扩展二叉树建立二叉树的二叉链表
void CreateBiTree(BiTree *T)
//这里给的是*T相当于多了一层可以把*T看成整体,T是指针的指针
{
TElemType ch;
scanf("%c",&ch);
if(ch=='#')
*T=NULL;
else
{
*T=(BiTree)malloc(sizeof(BiTNode));
if(!*T)
exit(OVERFLOW);
(*T)->data=ch;
CreateBinTree(& (*T)->lchild);//->优先级为1,&优先级为2
CreateBinTree(& (*T)->rchild);
}
}
线索二叉树(Thread)
指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,构成的二叉树称为线索二叉树。
线索二叉树把一棵二叉树转变成了一个双向链表。
为了区分指向的是孩子还是前驱,需要改变二叉链表结点的数据结构,增加标志位域来加以区分。
标志位为0则表明指向左孩子(右孩子),标志位为1则表明指向前驱(后继)
//线索二叉树的定义,线索二叉链表
typedef enum{Link,Thread} PointerTag;
typedef struct BiThrNode
{
TElemType data;
struct BiThrNode *lchild,*rchild;
PointerTag LTag;
PointerTag RTag;
}BiThrNode,*BiThrTree;
线索化就是在遍历的过程中修改空指针的过程。
代码如下
//中序遍历二叉树的线索化
BiThrTree pre;//全局变量,始终指向刚刚访问过的结点
void InThreading(BiThrTree p)
{
if(p)
{
Intreading(p->ichild);//左子树线索化
if(!p->ichild)//没有左孩子
{
p->LTag=Thread;
p->ichild=pre;//前驱
}
if(!pre->rchild)//前驱没有右孩子
{
pre->RTag=Thread;
pre->rchild=p;
}
pre=p;//在指向下一个结点之前将前驱设置为本结点
InThreading(p->rchild);
}
}
线索化之后可以直接根据标志对其循环中序遍历,这样可以比递归中序遍历更快,具体见《大话数据结构p193》。适合于需要经常遍历或者查找前驱和后继时使用。
树、森林与二叉树的转换
1.树转换二叉树(孩子兄弟法)
注意原来的长子是后来的左孩子,兄弟是右孩子。
2.森林转换为二叉树
先把森林中的树转换为二叉树。森林中的树直接的关系可以理解为兄弟,因此按照处理兄弟的方法,将后树的根结点作为前树根结点的右孩子。
3.二叉树转换为树
4.二叉树转换为森林
森林的前序遍历和对应二叉树的前序遍历结果相同,森林的后序遍历和对应二叉树的中序遍历结果相同。
Huffman树的构造[Huffman算法]:《大话数据结构》p200