数据结构-树与二叉树(Tree and Binary)-定义与基本操作
一. 二叉树(Binary Tree)
1. 存储结构
1.1 顺序存储结构(完全二叉树、满二叉树)
struct TreeNode {
int value; // 结点中的数据元素
bool isEmpty; // 结点是否为空
};
1.2 链式存储结构(一般二叉树)
typedef struct BiTNode {
int data; // 数据域
struct BiTNode *lchild, *rchild; // 左、右孩子指针
}BiTNode, *BiTree;
2. 遍历
2.1 先序遍历
// 先序遍历
void PreOrder(BiTree T) {
if (T != NULL) {
visit(T); // 访问根结点
PreOrder(T->lchild); // 递归遍历左子树
PreOrder(T->rchild); // 递归遍历右子树
}
}
2.2 中序遍历
// 中序遍历
void InOrder(BiTree T) {
if (T != NULL) {
InOrder(T->lchild); // 递归遍历左子树
visit(T); // 访问根结点
InOrder(T->rchild); // 递归遍历右子树
}
}
2.3 后序遍历
// 后序遍历
void PostOrder(BiTree T) {
if (T != NULL) {
PostOrder(T->lchild); // 递归遍历左子树
PostOrder(T->rchild); // 递归遍历右子树
visit(T); // 访问根结点
}
}
2.4 层次遍历
// 层次遍历
void LevelOrder(BiTree T) {
LinkQueue Q;
InitQueue(Q); // 初始化辅助队列
BiTree p;
EnQueue(Q, T); // 根结点入队
while (!QueueEmpty(Q)) { // 队列不空则循环
DeQueue(Q, p); // 队头结点出队
visit(p); // 访问队头结点
if (p->lchild !=NULL) {
EnQueue(Q, p->lchild); // 左孩子入队
}
if (p->rchild != NULL) {
EnQueue(Q, p->rchild); // 右孩子入队
}
}
}
二. 线索二叉树(Threaded Binary Tree)
1. 存储结构
typedef struct ThreadNode {
int data; // 数据元素
struct ThreadNode *lchild, *rchild; // 左、右孩子指针
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
2. 创建
ThreadNode *pre = NULL; // 全局变量,指向当前访问结点的前驱。
// 访问结点:连接该结点与前驱结点的线索信息。
void visit(ThreadNode *q) {
if (q->lchild == NULL) { // 左子树为空,建立前驱线索。
q->lchild = pre;
q->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild = q; // 建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q; // 标记当前结点成为刚访问过的结点
}
2.1 线索化二叉树
- 先序线索二叉树
void PreThread(ThreadTree T) {
if (T != NULL) {
visit(T);
// 如果不判断ltag,那么当ltag==1时会回到pre,陷入循环。
// 在中序和后序线索化中不用判断,因为visit(q)时,q的左子树已经被遍历处理过,不会再被访问。
if (T->ltag == 0) { // lchild不是前驱线索
PreThread(T->lchild);
}
PreThread(T->rchild);
}
}
- 中序线索二叉树
void InThread(ThreadTree T) {
if (T != NULL) {
InThread(T->lchild); // 中序线索化左子树
visit(T); // 访问根结点
InThread(T->rchild); // 中序线索化右子树
}
}
- 后序线索二叉树
void PostThread(ThreadTree T) {
if (T != NULL) {
PostThread(T->lchild); // 后序线索化左子树
PostThread(T->rchild); // 后序线索化右子树
visit(T); // 访问根结点
}
}
2.2 创建线索二叉树
- 先序线索二叉树
void CreatePreThread(ThreadTree T) {
pre = NULL;
if (T != NULL) {
PreThread(T); // 通过先序遍历线索化二叉树
// 处理遍历的最后一个结点
if (pre->rchild = NULL) {
pre->rtag = 1;
}
}
}
- 中序线索二叉树
void CreateInThread(ThreadTree T) {
pre = NULL;
if (T != NULL) {
InThread(T); // 通过中序遍历线索化二叉树
// 处理遍历的最后一个结点
if (pre->rchild = NULL) {
pre->rtag = 1;
}
}
}
- 后序线索二叉树
void CreatePostThread(ThreadTree T) {
pre = NULL;
if (T != NULL) {
PostThread(T); // 通过后序遍历线索化二叉树
// 处理遍历的最后一个结点
if (pre->rchild = NULL) {
pre->rtag = 1;
}
}
}
3. 寻找后继结点和前驱结点
3.1 寻找后继结点
- 先序线索二叉树
如果 rtag == 1
,直接返回后继线索;
否则 rtag == 0
,p必有右孩子。如果p有左孩子,则左孩子为后继;如果p无左孩子,则右孩子为后继。
ThreadNode *Nextnode(ThreadNode *p) {
if (p->rtag == 1) {
return p->rchild;
}
if (p->ltag == 0) {
return p->lchild;
}
return p->rchild;
}
- 中序线索二叉树
如果 rtag == 1
,直接返回后继线索;
否则遍历右子树,找到第一个访问的结点(右子树中最左下的结点)为后继。
ThreadNode *Firstnode(ThreadNode *p) {
while (p->ltag == 0) {
p = p->lchild;
}
return p;
}
ThreadNode *Nextnode(ThreadNode *p) {
if (p->rtag == 1) {
return p->rchild;
}
return Firstnode(p->rchild);
}
- 后序线索二叉树
如果 rtag == 1
,直接返回后继线索;
否则 rtag == 0
,p必有右孩子。因为在后序遍历中,左右子树的结点只可能是根的前驱,不可能是后继,所以只能从头开始后序遍历寻找后继结点。
ThreadNode *Nextnode(ThreadNode *p) {
if (p->rtag == 1) {
return p->rchild;
}
// 从头开始后序遍历寻找后继结点
// ...
}
如果改为三叉链表,增加1个指向双亲结点的指针,则有以下4种情况:
- 可以找到p的双亲结点,且p是右孩子:p的双亲结点即为后驱。
- 可以找到p的双亲结点,且p是左孩子,其右兄弟为空:p的双亲结点即为后驱。
- 可以找到p的双亲结点,且p是左孩子,其右兄弟非空:p的后驱为右兄弟子树中第一个被后序遍历的结点。
- p是根结点,没有后驱结点。
3.2 寻找前驱结点
- 先序线索二叉树
如果 ltag == 1
,直接返回前驱线索;
否则 ltag == 0
,p必有左孩子。因为在先序遍历中,左右子树中的结点只可能是根的后继,不可能是前驱,所以只能从头开始先序遍历寻找前驱结点。(类似在后序线索二叉树中寻找后继结点)
ThreadNode *Prenode(ThreadNode *p) {
if (p->ltag == 1) {
return p->lchild;
}
// 从头开始先序遍历寻找前驱结点
// ...
}
如果改为三叉链表,增加1个指向双亲结点的指针,则有以下4种情况:
- 可以找到p的双亲结点,且p是左孩子:p的双亲结点即为前驱。
- 可以找到p的双亲结点,且p是右孩子,其左兄弟为空:p的双亲结点即为前驱。
- 可以找到p的双亲结点,且p是右孩子,其左兄弟非空:p的前驱为左兄弟子树中最后一个被先序遍历的结点。
- p是根结点,没有前驱结点。
- 中序线索二叉树
如果 ltag == 1
,直接返回前驱线索;
否则遍历左子树,找到最后一个访问的结点(左子树中最右下的结点)为前驱。
ThreadNode *Lastnode(ThreadNode *p) {
while (p->rtag == 0) {
p = p->rchild;
}
return p;
}
ThreadNode *Prenode(ThreadNode *p) {
if (p->ltag == 0) {
return Lastnode(p->lchild);
}
return p->lchild;
}
- 后序线索二叉树
如果 ltag == 1
,直接返回前驱线索;
否则 ltag == 0
,p必有左孩子。如果p有右孩子,则右孩子为后序;如果p没有右孩子,则左孩子为后序。(类似在先序线索二叉树中找后继结点)
ThreadNode *Prenode(ThreadNode *p) {
if (p->ltag == 1) {
return p->lchild;
}
if(p->rtag == 0) {
return p->rchild;
}
return p->lchild;
}
4. 遍历
4.1 正向遍历
找到第一个结点,然后依次找到结点的后继,直到其后继为空。
// 正向遍历
void Inorder(ThreadNode *T) {
for (ThreadNode *p = Firstnode(T); p != NULL; p = Nextnode(p)) {
// ...
}
}
4.2 逆向遍历
找到最后一个结点,然后依次找到结点的前驱,直到其前驱为空。
// 逆向遍历
void ReInorder(ThreadNode *T) {
for (ThreadNode *p = Lastnode(T); p != NULL; p = Prenode(p)) {
// ...
}
}
三. 树
1. 存储结构
1.1 双亲表示法
#define MAX_TREE_SIZE 100 // 树中最多结点树
typedef struct { // 树的结点定义
int data; // 数据元素
int parent; // 双亲位置域
}PTNode;
typedef struct { // 树的类型定义
PTNode nodes[MAX_TREE_SIZE];
int n; // 结点数
}PTree;
1.2 孩子表示法
struct CTNode {
int child; // 孩子结点在数组中的位置
struct CTNode *next; // 下一个孩子
};
typedef struct {
int data;
struct CTNode *firstChild; // 第一个孩子
}CTBox;
typedef struct {
CTBox nodes[MAX_TREE_SIZE];
int n; // 结点数
int r; // 根的位置
}CTree;
1.3 孩子兄弟表示法(重点)
typedef struct CSNode {
int data; // 数据域
struct CSNode *firstchild; // 第一个孩子
struct CSNode *nextsibling; // 右兄弟
}CSNode, *CSTree;