数据结构——树(2)
数据结构基础(12)
文章目录
- 数据结构基础(12)
- 二叉树的先序遍历
- 先序遍历
- 中序遍历
- 后序遍历
- 二叉树的层序遍历
- 由遍历序列构造二叉树
- 前序 + 中序遍历序列
- 后序 + 中序遍历序列
- 层序 + 中序遍历序列
- 二叉树的中序遍历(缺点):
- 中序线索二叉树
- 中序线索化
- 先序线索化
- 后序线索化
- 中序线索二叉树找中序后继
- 中序线索二叉树找中序前驱
- 中序线索二叉树找先序后继
- 先序线索二叉树找先序前驱
- 后序线索二叉树找后序前驱
二叉树的先序遍历
- 遍历:按某种次序把所有结点都访问一遍 – > 线性结构
- 层次遍历:基于树的层次特性确定的次序规则
- 二叉树的先/中/后序遍历:基于树的递归特性确定的次序规则
二叉树的递归特性
- 要么是个空二叉树
- 要么就是由“根节点 + 左子树 + 右子树”组成的二叉树
先序遍历:根左右(NLR)-- > 又叫先根遍历
中序遍历:左根右(LNR)- > 又叫中根遍历
后序遍历:左右根(LRN)- > 又叫后根遍历
基础图:
先序遍历: ABC
中序遍历:BAC
后序遍历:BCA
在加深一点:
先序遍历: A B D E C F G
中序遍历; D B E A F C G
后序遍历:D E B F G C A
对算数表达式的“分析树”:
先序遍历 — > 前缀表达式
中序遍历 — > 中缀表达式(需要加界限符)
后序遍历 — > 后缀表达式
先序遍历
先序遍历(PreOrder)的操作过程如下:
- 若二叉树为空,则什么也不做;
- 若二叉树非空:
①访问根结点;
②先序遍历左子树
③先序遍历右子树
实现代码:
typedef struct BiTNode{ElemType data;struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;//先序遍历
void PreOrder(BiTree T){if(T!=NULL){visit(T); //访问根结点PreOrder(T->lchild); //递归遍历左子树PreOrder(T->rchild); //递归遍历右子树}
}
第一次被访问的点
空间复杂度:O(h)
中序遍历
中序遍历(InOrder)的操作过程如下:
- 若二叉树为空,则什么也不做;
- 若二叉树非空:
①先序遍历左子树;
②访问根结点;
③先序遍历右子树。
实现代码:
typedef struct BiTNode{ElemType data;struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;//中序遍历
void InOrder(BiTree T){if(T!=NULL){InOrder(T->lchild); //递归遍历左子树visit(T); //访问根结点InOrder(T->rchild); //递归遍历右子树}
}
第二次被访问的点
后序遍历
后序遍历(PostOrder)的操作过程如下:
- 若二叉树为空,则什么也不做;
- 若二叉树非空:
①先序遍历左子树;
②先序遍历右子树;
③访问根结点。
实现代码:
typedef struct BiTNode{ElemType data;struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;//后序遍历
void PostOrder(BiTree T){if(T!=NULL){PostOrder(T->lchild); //递归遍历左子树PostOrder(T->rchild); //递归遍历右子树visit(T); //访问根结点}
}
第三次被访问的点
二叉树的层序遍历
算法思想:
算法思想:
- 初始化一个辅助队列
- 根结点入队
- 若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
- 重复步骤3直至队列为空
实现代码:
//二叉树的结点(链式存储)
typedef struct BiTNode{char data;struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;//链式队列结点
typedef struct LinkNode{BiTNode * data;struct LinkNode *next;
}LinkNode;typedef struct{LinkNode *front,*rear; //队头队尾
}LinkQueue;//层序遍历
void LevelOrder(BiTree T){LinkQueue Q;InitQueue(Q); //初始化辅助队列BiTree p;EnQueue(Q,T); //将根结点入队while(!IsEmpty(Q)){ //队列不空则循环DeQueue(Q, p); //队头结点出队visit(p); //访问出队结点if(p->lchild!=NULL)EnQueue(Q,p->lchild); //左孩子入队if(p->rchild!=NULL)EnQueue(Q,p->rchild); //右孩子入队}
}
存指针而不是结点
BiTNode * data;
由遍历序列构造二叉树
-
一个中序遍历序列可能对应多种二叉树形态
-
一个前序遍历序列可能对应多种二叉树形态
-
一个后序遍历序列可能对应多种二叉树形态
-
一个层序遍历序列可能对应多种二叉树形态
SO : 若只给出一颗二叉树的前/中/后/层 序遍历序列中的一种 ,不能唯一确定一颗二叉树
前序 + 中序遍历序列
- 前序遍历:根结点、前序遍历左子树、前序遍历右子树
前序遍历序列:
根结点 左子树的前序遍历序列 右子树的前序遍历序列
- 中序遍历:中序遍历左子树、根结点、中序遍历右子树
中序遍历序列:
左子树的中序遍历序列 根结点 右子树的中序遍历序列
前序遍历序列: A D B C E
中序遍历序列: B D C A E
得出的二叉树:
后序 + 中序遍历序列
- 后序遍历:前序遍历左子树、前序遍历右子树、根结点
后序遍历序列:
左子树的后序遍历序列 右子树的后序遍历序列 根结点
- 中序遍历:中序遍历左子树、根结点、中序遍历右子树
中序遍历序列
左子树的中序遍历序列 根结点 右子树的中序遍历序列
后序遍历序列: E F A H C I G B D
中序遍历序列: E A F D H C B G I
得出的二叉树:
层序 + 中序遍历序列
-
层序遍历序列
根结点 左子树的根 右子树的根
-
中序遍历序列
左子树的中序遍历序列 根结点 右子树的中序遍历序列
层序遍历序列:A B C D E
中序遍历序列:A C B E D
得出的二叉树:
KEY:找到树的根节点,并根据中序序列划分左右子树,再找到左右子树根结点
二叉树的中序遍历(缺点):
Q1:如何找到指定结点 P 在中序遍历序列中的前驱?
Q2 : 如何找到 p 的中序后继?
思路:从根节点出发,重新进行一次中序遍历,指针 q 记录当前访问的结点,指针 pre 记录上一个被访问的结点
1.当 q == p 时,pre 为前驱
2.当 pre == p 时,q 为后继
缺点:
找前驱、后继很不方便:遍历操作必须从根开始
中序线索二叉树
中序遍历序列:D G B E A F C
- 前驱线索(由左孩子指针充当 —— > 红色线
- 后继线索(由右孩子指针充当)—— > 绿色线
线索链表代码实现:
//线索二叉树结点
typedef struct ThreadNode{ElemType data;struct ThreadNode *lchild,*rchild;int ltag,rtag; //左、右线索标志
}ThreadNode,*ThreadTree;
-
tag == 0,表示指针指向孩子
tag ==1,表示指针是“线索” -
先序、后序同理
中序线索化
实现代码
// 线索二叉树结点结构定义
typedef struct ThreadNode{ElemType data; struct ThreadNode *lchild,*rchild; int ltag,rtag;
}ThreadNode,*ThreadTree;// 全局变量 pre,指向当前访问结点的前驱
ThreadNode *pre=NULL; // 中序遍历二叉树并进行线索化
void InThread(ThreadTree T){if(T!=NULL){InThread(T->lchild);visit(T); InThread(T->rchild); }
}// 访问结点函数(实际处理线索化逻辑)
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; // 更新pre为当前结点
}// 封装中序线索化的入口函数,统一初始化pre并处理最终结点
void CreateInThread(ThreadTree T){pre=NULL; // 初始化为NULLif(T!=NULL){ InThread(T); // 处理遍历的最后一个结点:若其右孩子为空,标记为线索if(pre->rchild==NULL){ pre->rtag=1; }}
}
先序线索化
- 先序线索化中,注意处理爱滴魔力转圈圈问题,当 ltag == 0时,才能对左子树先序线索化
实现代码;
// 线索二叉树结点结构定义
typedef struct ThreadNode{ElemType data;struct ThreadNode *lchild, *rchild;int ltag, rtag;
}ThreadNode, *ThreadTree;// 全局变量 pre,指向当前访问结点的前驱
ThreadNode *pre = NULL; // 先序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T){if(T != NULL){visit(T); // 先处理根节点(执行线索化核心逻辑)// lchild 不是前驱线索(即 ltag == 0 时,说明是真实孩子,递归处理左子树)if (T->ltag == 0) PreThread(T->lchild); PreThread(T->rchild); // 递归处理右子树}
}// 访问结点函数(实际进行线索化操作)
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; // 更新 pre 为当前结点,为下一个结点做准备
}// 先序线索化二叉树的入口函数,统一初始化 pre 并处理最后一个结点
void CreatePreThread(ThreadTree T){pre = NULL; // pre 初始化为 NULLif(T != NULL){ // 非空二叉树才进行线索化PreThread(T); // 调用先序线索化递归函数// 处理遍历的最后一个结点:若其右孩子为空,标记为线索if (pre->rchild == NULL) pre->rtag = 1; }
}
后序线索化
实现代码:
// 线索二叉树结点结构(需提前定义 ElemType,如 typedef int ElemType; )
typedef struct ThreadNode {ElemType data;struct ThreadNode *lchild, *rchild;int ltag, rtag; // 0: 指向孩子,1: 线索
} ThreadNode, *ThreadTree;// 全局变量:指向当前访问结点的前驱
ThreadNode *pre = NULL; // 后序遍历 + 线索化(递归逻辑:左 → 右 → 根)
void PostThread(ThreadTree T) {if (T != NULL) {PostThread(T->lchild); // 后序遍历左子树PostThread(T->rchild); // 后序遍历右子树visit(T); // 访问根节点(执行线索化操作)}
}// 访问结点函数(处理线索化逻辑)
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; // 更新前驱为当前结点
}// 后序线索化入口函数(初始化 + 处理最后结点)
void CreatePostThread(ThreadTree T) {pre = NULL; // 初始化前驱if (T != NULL) { // 非空树才线索化PostThread(T); // 调用后序线索化// 处理遍历的最后一个结点(根结点)if (pre->rchild == NULL) { pre->rtag = 1; }}
}
中序线索二叉树找中序后继
在中序线索二叉树中找到指定结点 * p 的中序后继 next
- 若 p->rtag == 1,则 next = p->rchild
- 若 p->rtag == 0
next = p 的右子树中最左下结点
实现代码:
// 找到以 p 为根的子树中,第一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p){// 循环找到最左下结点(不一定是叶结点)while(p->ltag==0) p=p->lchild; return p;
}// 在中序线索二叉树中找到结点 p 的后继结点
ThreadNode *Nextnode(ThreadNode *p){// 右子树中最左下结点if(p->rtag==0) return Firstnode(p->rchild); else return p->rchild; // rtag==1 直接返回后继线索
}
- 对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void Inorder(ThreadNode *T){for(ThreadNode *p=Firstnode(T);p!=NULL; p=Nextnode(p))visit(p);
}
空间复杂度O(1)
中序线索二叉树找中序前驱
在中序线索二叉树中找到指定结点 * p 的中序前驱 pre
- 若 p->ltag == 1,则 pre = p->lchild
- 若 p->ltag == 0
pre = p 的左子树中最右下结点
实现代码:
// 找到以 p 为根的子树中,最后一个被中序遍历的结点
ThreadNode *Lastnode(ThreadNode *p){// 循环找到最右下结点(不一定是叶结点)while(p->rtag==0) p=p->rchild; return p;
}// 在中序线索二叉树中找到结点 p 的前驱结点
ThreadNode *Prenode(ThreadNode *p){// 左子树中最右下结点if(p->ltag==0) return Lastnode(p->lchild); else return p->lchild; // ltag==1 直接返回前驱线索
}
- 对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode *T){for(ThreadNode *p=Lastnode(T);p!=NULL; p=Prenode(p))visit(p);
}
中序线索二叉树找先序后继
在先序线索二叉树中找到指定结点 * p 的中序后继 next
- 若 p->rtag == 1,则 pre = p->rchild
- 若 p->rtag == 0
假设有左孩子
- 若 p 有左孩子,则先序后继为左孩子
先序遍历 —— 根 左 右
根 (根 左 右) 右
假设没有左孩子
- 若 p 没有左孩子,则先序后继为右孩子
先序遍历 —— 根 右
根 (根 左 右)
先序线索二叉树找先序前驱
在先序线索二叉树中找到指定结点 * p 的中序前驱 pre
- 若 p->ltag == 1,则 pre = p->lchild
- 若 p->ltag == 0
在先序遍历中,左右子树的结点只可能是根的后继,不可能是前驱
后序线索二叉树找后序前驱
在先序线索二叉树中找到指定结点 * p 的中序前驱 pre
- 若 p->ltag == 1,则 pre = p->lchild
- 若 p->ltag == 0
假设有右孩子
- 若 p 有右孩子,则后序前驱为右孩子
后序遍历 —— 左 右 根
左 (左 右 根) 根
假设没有右孩子
- 若 p 没有右孩子,则后序前驱为左孩子
后序遍历 —— 左 根
(左 右 根) 根
中序线索二叉树 | 先序线索二叉树 | 后序线索二叉树 | |
---|---|---|---|
找前驱 | ✓ | ✕ | ✓ |
找后继 | ✓ | ✓ | ✕ |