Table of Contents
C语言具体实现见:数据结构C语言实现-5—树
树
树(Tree)是 n (n >= 0)个结点的有限集。n = 0时称为空树。在任意一棵非空树中:
(1)有且仅有一个特定的称为根(Root)的结点;
(2)当 n > 1时,其余结点可分为 m(m > 0)个互不相交的有限集,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。
结点拥有的子树数称为结点的度(Degree),度为0的结点称为叶节点,树的度是树内各节点的度的最大值,如下图中树的度为3.
结点的子树的根称为该结点的孩子(Child),相应地,该节点称为孩子的双亲(Parent),同一个双亲的孩子之间互称为兄弟(Sibling),结点的祖先是从根到该结点所经分支上的所有结点。
结点的层次是从根开始定义起,根为第一层....,树中结点的最大层次称为树的深度(Depth)或高度
如果将树中结点的各子树看成从左到右是有次序的,不能交换,则称该树为有序树,否则称为无序树。
线性结构和树结构的区别
线性结构: 树结构:
一对一的关系 一对多的关系
第一个数据元素:无前驱 根结点:无双亲,唯一
最后一个数据元素:无后继 叶结点:无孩子,可以为多个
中间元素:一个前驱一个后继 中间结点:一个双亲多个孩子
树的抽象数据类型
ADT树(tree)
Data
树是由一个根节点和若干棵子树构成,树中结点具有相同数据类型及层次关系。
Operation
InitTree(*T):构造空树T;
DestroyTree(*T):销毁树T;
CreateTree(*T, definition):按 definition 中给出树的定义来构造树;
ClearTree(*T):若树 T 存在,则将树 T 清为空树;
TreeEmpty(T):若 T 为空树,返回true,否则返回false;
TreeDepth(T):返回 T 的深度
Root(T):返回 T 的根结点
Value(T, cur_e):cur_e 是树 T 中一个结点,返回此结点的值;
Assign(T, cur_e, value):给树 T 的结点 cur_e 赋值为 value;
Parent(T, cur_e):若 cur_e 是树 T 的非根结点,则返回它的双亲,否则返回空;
LeftChild(T, cur_e):若 cur_e 是树的非叶结点,则返回它的最左孩子,否则返回空;
Rightsibling(T, cur_e):若 cur_e 有右兄弟,则返回它的右兄弟,否则返回空;
InsertChild(*T, *p, i, c):其中 p 指向树 T 的某个结点,i 为所指结点 p 的度加上1,非空树 c 与 T 不相交,
操作结果为插入 c 为树 T 中 p 所指结点的第 i 棵子树;
DeleteChild(*T, *p, i):其中 p 指向树 T 的某个结点,i 为所指结点 p 的度,操作结果为删除 T 中 p 所指结点的第 i 棵子树
树的存储结构
简单的顺序存储结构,是无法满足树这种一对多的关系的,不过充分利用顺序存储和链式存储结构,完全可以实现对树的存储结构的表示
这里介绍三种表示法:双亲表示法、孩子表示法、孩子兄弟表示法
双亲表示法
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
增加一个右兄弟域,若右兄弟不存在赋值为-1
孩子表示法
孩子表示法是:把每个结点的孩子结点排列起来,以单链表作为存储结构,则 n 个结点有 n 个孩子链表,如果是叶子结点则此单链表为空,然后 n 个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中
为此设计两种结点结构:
一个是孩子链表的孩子结点:
其中 child 是数据域,用来存储某个结点在表头数组中的下标,next是指针域,用来存储指向某结点的下一个孩子结点的指针。
另一个是表头数组耳朵表头结点:
其中 data 是数据域,存储某结点的数据信息,firstchild 是头指针域,存储该结点的孩子链表的头指针。
/* 树的孩子表示法结构定义 */
#define MAX_TREE_SIZE 100
typedef struct CTNOde /* 孩子结点 */
{
int child;
struct CTNode *next;
}*ChildPtr;
typedef struct /* 表头结构 */
{
TElemType data;
ChildPtr firstchild;
}CTBox;
typedef struct /* 树结构 */
{
CTBox nodes[MAX_TREE_SIZE]; /* 结点数组 */
int r, n; /* 根的位置和结点数 */
}CTree;
双亲表示法和孩子表示法的结合(双亲孩子表示法)如下所示:
孩子兄弟表示法
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,可以设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
其中 data 是数据域,firstchild 为指针域,存储该结点的第一个孩子结点的存储地址,rightsib是指针域,存储该结点的右兄弟结点的存储地址。
/* 树的孩子兄弟表示法结构定义 */
typedef struct CSNode
{
TElemType data;
struct CSNode *firstchild, *rightsib;
}CSNode, *CSTree;
二叉树
二叉树(Binary Tree)是 n(n >= 0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根节点和两棵互不相交的、分别称为根节点的左子树和右子树的二叉树组成。
二叉树的特点有:
- 每个结点最多有两棵子树;
- 左子树和右子树是有顺序的,次序不能颠倒;
- 既是树种某结点只有一棵子树,也要区分它是左子树还是右子树。
几种特殊的二叉树:
1.斜树
左斜树:所有结点都只有左子树的二叉树
右斜树:所有结点都只有右子树的二叉树
斜树结点的个数和树的深度相同。(线性表结构就可以理解为树的一种特殊表现形式)
2.满二叉树
满二叉树:在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上。
如上图所示,满二叉树有以下特点:
- 叶子结点只能出现在最下一层;
- 非叶子结点的度一定是2
- 在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多
3.完全二叉树
对一棵具有 n 个结点的二叉树按层序编号,如果编号为 i(1 =< i <= n)的结点与同样深度的满二叉树中编号为 i 的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树
所以满二叉树一定是完全二叉树,反之不一定。
二叉树的性质
- 在二叉树的第 i 层上至多有个结点(i>=1)
- 深度为 k 的二叉树至多有个结点(k>=1)
- 对任何一棵二叉树 T,如果其终端结点数为 ,度为 2 的结点数为 ,则
- 具有 n 个结点的完全二叉树的深度为(表示不大于 x 的最大整数)
二叉树的存储结构
二叉树顺序存储结构
二叉树顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置(也就是数组的下标)要能体现结点之间的逻辑关系
例如一个完全二叉树:
二叉链表
因为二叉树每个结点最多有两个孩子,所以设计为一个数据域和两个指针域
其中 data 是数据域,lchild 和 rchild都是指针域,分别存放指向左孩子和右孩子的指针。
/* 二叉树的二叉链表结点结构定义 */
typedef struct BiTNode /* 结点结构 */
{
TelemType data; /* 结点数据 */
struct BiTNode *lchild, *rchild; /* 左右孩子指针 */
}BiTNode, *BiTree;
遍历二叉树
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。遍历其实都是在把树种的结点变成某种意义的线性序列,给程序的实现带来了好处,遍历方式主要分为四种:
1.前序遍历
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树,如下图遍历的顺序为:ABDGHCEIF
/*二叉树的前序遍历递归算法 */
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return;
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
PreOrderTraverse(T->lchild); /* 再先序遍历左子树 */
PreOrderTraverse(T->rchild); /* 最后先序遍历右子树 */
}
2.中序遍历
规则是若二叉树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树,如下图遍历的顺序为:GDHBAEICF
/*二叉树的中序遍历递归算法 */
void InOrderTraverse(BiTree T)
{
if(T==NULL)
return;
InOrderTraverse(T->lchild); /* 中序遍历左子树 */
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
InOrderTraverse(T->rchild); /* 最后中序遍历右子树 */
}
3.后序遍历
规则是若二叉树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点,如下图遍历的顺序为:GHDBIEFCA
/* 二叉树的后序遍历递归算法 */
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return;
PostOrderTraverse(T->lchild); /* 先后序遍历左子树 */
PostOrderTraverse(T->rchild); /* 再后序遍历右子树 */
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
}
4.层序遍历
规则是若二叉树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中按从左到右的顺序对结点逐个访问,如下图遍历的顺序为:ABCDEFGHI
线索二叉树
之前提高的二叉链表(如下图所示),有很多空指针域的存在,线索二叉树就是利用这些空指针域,来存放指向结点在某种遍历次序下的前驱和后继结点的地址。
指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded Binary Tree)
如下图所示,二叉树经过中序遍历后,将所有的空指针域种的 rchild,改为指向它的后继结点,从图中可以看出,H 的后继是 D,I 的后继是 B...
将这颗二叉树的所有空指针域中的lchild,改为指向当前结点的前驱。同样可看出,H 的前驱是NULL,I 的前驱是 D...
通过下图更容易看出(空心箭头实线为前驱,虚线黑箭头为后继),线索二叉树就是把一棵二叉树转变为一个双向链表,这样插入、删除、查询等操作都更方便。对二叉树以某种次序遍历使其变为线索二叉树的过程就称为线索化 。
此时仍然存在问题,我们并不知道某一结点的 lchild 是指向它的左孩子还是前驱?rchild 是指向右孩子还是后继?为此需要再增设两个标志域 ltag 和 rtag。
其中:ltag 为 0 时指向该结点的左孩子,为 1 时指向该结点的前驱;rtag 为 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, *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; /* 前驱右孩子指针指向后继(当前结点p) */
}
pre=p; /* 保持pre指向p的前驱 */
InThreading(p->rchild); /* 递归右子树线索化 */
}
}
和双向链表结构一样,在二叉树线索链表上添加一个头结点,如下图所示
/* 中序遍历二叉线索树T(头结点)的非递归算法 */
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;
}
return OK;
}