第五章 树与二叉树
一、二叉树
链式存储结构
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);//递归遍历右子树
}
}
非递归版
void PreOrder2(BiTree T)
{
Stack S;
BiTree p = T;
InitStack(S);
while(p || !IsEmpty(S)) //栈不空或p不空时循环
{
if(p) //一路向左遍历
{
visit(p);//先序遍历,先访问根结点
Push(S,p);//并入栈
p = p->lchild; //左孩子不空,一直向左走
}
else //p空,出栈回溯,并转向出栈结点的右子树
{
Pop(S,p);
p = p->rchild; //转向根结点的右子树进行先序遍历
}
}
}
中序遍历
递归版
void InOrder(BiTree T)
{
if(T != NULL)
{
InOrder(T->lchild); //递归遍历左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归遍历右子树
}
}
非递归版
void InOrder2(BiTree T)
{
Stack S;
BiTree p = T;
while(p || !IsEmpty(S)) //栈不空或p不空时循环
{
if(p) //一路向左遍历
{
Push(S,p);
p = p->lchild;
}
else //p空,出栈回溯,并转向出栈结点的右子树
{
Pop(S,p);
visit(p); //访问根结点
p = p->rchild;//转向根结点的右子树进行中序遍历
}
}
}
后序遍历
递归版
void PostOrder(BiTree T)
{
if(T != NULL)
{
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
非递归版
思想:按照“根右左”的顺序遍历,将结果保存到栈中,最后依次出栈访问,恰好得到“左右根”的访问顺序。
void PostOrder(BiTree T)
{
Stack s1,s2;
BiTree p = T;
while(p || !IsEmpty(s1)) //栈不空或p不空时循环
{
if(p) //一路向右遍历
{
Push(s2,p);
Push(s1,p);
p = p->rchild;
}
else //p空,出栈回溯,并转向出栈结点的左子树
{
Pop(s1,p);
p = p->lchild;//转向根结点的左子树进行遍历
}
}
while(!IsEmpty(s2))
{
Pop(s2,p);
visit(p);//访问每个结点
}
}
层序遍历
void LevelOrder(BiTree T)
{
Queue Q;
BiTree p;
InitQueue(Q);
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);
}
}
线索化
存储结构
typedef struct ThreadNode
{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;//左右线索标志,0表示左右孩子,1表示线索
}ThreadNode,*ThreadTree;
构造(以中序为例)
void InThread(ThreadTree &p,ThreadTree &pre)
{
if(p != NULL)
{
InThread(p->lchild,pre); //递归,线索化左子树
if(p->lchild == NULL) //若左子树为空,建立前驱结点
{
p->ltag = 1;
p->lchild = pre;
}
if(p->rchild == NULL && pre != NULL) //建立上一个结点的后驱结点
{
pre->rtag = 1;
pre->rchild = p;
}
InThread(p->rchild,p);//递归,线索化右子树,更新当前结点为刚刚访问的结点
}
}
主过程算法
void CreateInThread(ThreadTree T)
{
ThreadTree pre = NULL;
if(T != NULL)
{
InThread(T,pre);
pre->rchild = NULL;//处理遍历的最后一个结点
pre->rtag = 1;
}
}
遍历(以中序为例)
求中序线索二叉树中中序序列下的第一个结点
TreeNode *Firstnode(ThreadNode *p)
{
while(p->ltag == 0) p = p->lchild; //最左下结点(不一定叶结点)
return p;
}
求中序线索二叉树中结点p的后继
TreeNode *nextnode(ThreadNode *p)
{
if(p->rtag == 0) return Firstnode(p->rchild);
else return p->rchild;
}
不含头结点的中序线索二叉树的中序遍历的算法
void Inorder(ThreadNode *T)
{
for(ThreadNode *p = Firstnode(T);p != NULL;p = nextnode(p))
{
visit(p);
}
}
二、树和森林
存储结构
双亲表示法(顺序存储)
#define MAX_TREE_SIZE 100
typedef struct{
ElemType data;
int parent;
}PTNode;
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int n;
}PTree;
孩子兄弟表示法(链式存储) 树转换成森林
typdef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;//第一个孩子和右兄弟指针
}CSNode,*CSTree;
相互转换方法
树转换成二叉树:大家变小家,双亲管老大,大孩管小孩
- 在兄弟之间加一条线
- 对每个结点,只保留它与第一个孩子的连线,与其他孩子的连线全部删去
- 以树根为轴心,顺时针旋转45°
森林转换成二叉树:先把每棵树转换成二叉树,再连接每棵二叉树的根结点
二叉树转换成树:小家变大家,连右变团圆,删除原右链
- 若某结点是其双亲的左孩子,则把该结点的右孩子、右孩子的右孩子等都与该结点的双亲结点用连线连起来。
- 删除原二叉树中所有双亲结点与右孩子结点之间的连线。
- 以根节点为轴心,逆时针转动45°,使结构层次分明。
二叉树转换成森林:断右变多个二叉树,逐一转换为树
- 断开右子树,直到无右结点为止
- 连接每个结点左孩子的右孩子,直到无右子树为止
- 删去每个结点最初存在的右链
树和森林的遍历与二叉树遍历的对应关系
树 | 森林 | 二叉树 |
---|---|---|
先序遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
三、二叉搜索树
查找
BSTNode* BST_Search(BiTree T,ElemType key)
{
while(T != NULL && T->data != key)
{
if(key < T->data) T = T->lchild;
else T = T->rchild;
}
return T;
}
插入
int BST_Insert(BiTree T,KeyType k)
{
if(T == NULL)
{
T = (BiTree)malloc(sizeof(BSTNode));
T->data = k;
T->lchild = T->rchild = NULL;
return 1;//插入成功
}
if(k == T->data) return 0;//存在相同关键字的结点,插入失败
else if(k < T->data) return BST_Insert(T->lchild,k);//插入到左子树
else return BST_Insert(T->rchild,k);//插入到右子树
}
删除
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* FindMax(TreeNode* root) //一直向右走,找到树上最大值
{
if(!root) return NULL;
TreeNode* t = root;
while(t->right)
{
t = t->right;
}
return t;
}
TreeNode* deleteNode(TreeNode* root, int key)
{
TreeNode* tmp;
if(root)
{
if(key == root->val) //找到了要删除的结点
{
if(root->left && root->right) //左右孩子存在,则用左子树最大值的结点值替换当前结点(西电答案默认前驱)
{
tmp = FindMax(root->left);
root->val = tmp->val;
root->left = deleteNode(root->left,tmp->val);//删除左子树的最大值结点
}
else //有1个孩子或叶结点
{
tmp = root;//备份当前结点用于删除
if(!root->left) root = root->right;//右孩子代替当前根结点
else if(!root->right) root = root->left;//左孩子代替当前根结点
delete(tmp);//删除先前根结点
}
}
else if(key < root->val) //递归去左子树查找并删除
{
root->left = deleteNode(root->left,key);
}
else root->right = deleteNode(root->right,key);//递归去右子树查找并删除
return root;
}
else return root;
}
};
四、平衡二叉树
插入后发生不平衡时的调整策略
基本思想
先找到离插入结点最近的平衡因子的绝对值大于1的结点A,再对以A为根的子树,在保持二叉排序树特性的前提下,调整各结点的位置关系,使之重新达到平衡。
1.LL平衡旋转
执行条件
结点A的左孩子的左子树上插入结点导致A的不平衡
执行方法:右旋
- 将A的左孩子B向右上旋转代替A成为根结点
- 将A结点向右下旋转成为B的右子树的根结点
- B的原右子树则作为A结点的左子树
1.RR平衡旋转
执行条件
结点A的右孩子的右子树上插入结点导致A的不平衡
执行方法:左旋
- 将A的右孩子B向左上旋转代替A成为根结点
- 将A结点向左下旋转成为B的左子树的根结点
- B的原左子树则作为A结点的右子树
1.LR平衡旋转
执行条件
结点A的左孩子的右子树上插入结点导致A的不平衡
执行方法:先将A结点的左子树左旋,后将A结点右旋
1.RL平衡旋转
执行条件
结点A的右孩子的左子树上插入结点导致A的不平衡
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)