考研数据结构:从入门到精通(树)——迈向算法高手之路
树的代码题在各个自命题学校的考研题里面几乎是必考题,所以对树的代码题在考研复习过程中显得尤为重要。本篇文章主要详细分析树代码题的不同考法,在刷完该题单后,对树的相关代码题将会有一个质的飞跃。
通过对树章节代码题的深入学习,将能够更好地应对考研中的数据结构题目,迈向算法高手的行列。下面,就让我们开启这段从入门到精通的树代码题学习之旅。
注意:下面标题中加※,表示题目难度较大,适合于考92的考生掌握,普通双非的考生可以不用重点复习
文章目录
树的结构体写法
链式存储的写法,适用范围较广
// C
typedef struct BTNode{
char data; // 如果定义为char类型,表示编号,比如A,B,C,D这种
// int data; // 如果定义为int类型,表示权值,比如1,2,3,4
struct BTNode *lchild, *rchild;
}BTNode, *BiTree;
// cpp
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) {}
};
一、树的基本操作
一想到基本操作,联想crud即可,也就是增删改查。
增加结点,除了二叉排序树的创建,很少会出这种题
删除结点,经典题目,删除值为x的结点
修改结点,经典题目,交换二叉树的左右结点
查找结点,经典题目,查找key的结点,查找key的双亲…
所以树的基本操作里面大致包括遍历(其他的基础), 查找,删除这几种常考题目。
除了层序遍历,大部分题目以考察递归形式居多,但不排除部分会考非递归的写法。
1. 先序遍历
递归写法:
void PreOrder(BiTree T){
if(T != NULL){
visit(T); // 访问根节点
PreOrder(T -> lchild); // 递归遍历左子树
PreOrder(T -> rchild); // 递归遍历右子树
}
}
非递归写法(栈):
void preorderTraversal(BiTree bt) {
BiTree S[maxsize];
int top = -1;
while(bt || top != -1){
if(bt){
printf("%d", bt->data); // 遍历根结点
S[++top] = bt; // 根结点入栈2
bt = bt -> lchild; // 优先入左孩子 null
}else{
bt = S[top --]; // 2
bt = bt -> rchild; //
}
}
}
2. 中序遍历
递归写法:
void InOrder(BiTree T){
if(T != NULL){
InOrder(T -> lchild); // 递归遍历左子树
visit(T); // 访问根节点
InOrder(T -> rchild); // 递归遍历右子树
}
}
非递归写法:
void inorderTraversal(BiTree bt) {
BiTree S[maxsize];
int top = -1;
while(bt || top != -1){
if(bt){
S[ ++top ] = bt;
bt = bt -> lchild;
}else{
bt = S[top --];
printf("%d", bt->data);
bt = bt -> rchild;
}
}
}
3. 后序遍历
递归写法:
void PostOrder(BiTree T){
if(T != NULL){
PostOrder(T -> lchild); // 递归遍历左子树
PostOrder(T -> rchild); // 递归遍历右子树
visit(T); // 访问根节点
}
}
非递归写法:
void postorderTraversal(BiTree bt) {
BiTree S[maxsize], nowp, tag = NULL;
int top = -1;
while(bt || top != -1){
if(bt){
S[++top] = bt;
bt = bt -> lchild;
}else{
nowp = S[top];
if(nowp -> rchild && nowp -> rchild == tag){
bt = nowp -> rchild;
}else{
nowp = S[top --];
printf("%d", nowp -> data);
tag = nowp;
}
}
}
}
4. 查找类问题
4.1 在二叉树上找元素值为x的结点
由于要求的结果是结点,所以定义的函数返回值是结点类型
TreeNode* searchBTree(TreeNode* root, int x) {
if(!root) return nullptr; // 如果空子树,返回空
if(root->val == x) return root; // 如果该节点的值等于x,返回root
TreeNode* node_left = searchBTree(root->left, x); // 找左子树的结果
TreeNode* node_right = searchBTree(root->right, x); // 找右子树的结果
if(node_left != nullptr) return node_left; // 如果左子树的结果不为空,则返回结果
if(node_right != nullptr) return node_right; // 如果右子树的结果不为空,则返回结果
return nullptr; // 最后没找到满足条件的结点,返回null
}
4.2 在二叉树上找元素值为x的双亲结点
TreeNode* searchfa(TreeNode* root, int x){
if(!root) return nullptr;
// 如果左子树存在,且左孩子的值等于x,则证明root是其双亲,返回结果
if(root -> left && root -> left -> val == x){
return root;
}
// 如果右子树存在,且右子树的值等于x,则证明root是其双亲,返回结果
if(root -> right && root-> right -> val == x){
return root;
}
TreeNode* left_ans = searchfa(root -> left, x); // 找左子树的结果
TreeNode* right_ans = searchfa(root -> right, x); // 找右子树的结果
if(left_ans != nullptr) return left_ans;
if(right_ans != nullptr) return right_ans;
return nullptr;
}
5. 修改类问题
5.1 二叉树交换左右孩子
以二叉链表作为二叉树的存储结构,交换二叉树每个结点的左孩子和右孩子。 – 2015年重庆理工 2020大连理工
&T表示传引用地址, 作用于函数内部修改结构体的属性
void changeLR(BiTree &T){
if(T == NULL) return ; // 如果空结点不用改
// 如果左右子树均为空,不用改
if(T -> lchild == NULL && T -> rchild == NULL) return ;
// 如果左右子树均存在,交换左右子树的值
if(T -> lchild && T-> rchlid){
swap(T->lchild, T->rchild);
}
// 如果仅左子树存在,递归执行交换左子树的函数
else if(T -> lchild){
changeLR(T -> lchild);
}
// 如果仅右子树存在,递归执行交换右子树的函数
else{
changeLR(T -> rchild);
}
}
6. 删除类问题
6.1 删除值x为根节点的子树
以二叉链表为存储结构,在二叉树中删除以值x为根节点的子树。 – 2020沈阳工业
void Delete_TreeNode(BiTree &root){
if(!root)return ;
Delete_TreeNode(root -> lchild); // 要删除root,先删掉root的左子树
Delete_TreeNode(root -> rchild); // 要删除root,先删掉root的右子树
root = null; // 左右子树均删完之后,把当前节点赋值为null
}
void DeleteX(BiTree &root, int x){
if(!root) return; // 空子树,直接返回即可
// 如果当前结点值为x,则调用删除删除结点的函数进行删除
if(root -> data == x){
Delete_TreeNode(root);
return ;
}
DeleteX(root -> lchlid, x); // 看左子树里面是否有值x的结点,如果有,执行递归函数删除
DeleteX(root -> rchlid, x); // 看右子树里面是否有值x的结点,如果有,执行递归函数删除
}
二、 层序遍历的相关问题(需要会改模板)
树的层序遍历主要解决那些需要逐层处理树节点、涉及节点间的横向关系以及需要统计树结构信息的问题。
1. 层序遍历(万能模板)
99% 涉及到层序遍历的问题,都可以解决
void level(BiTree bt){
BiTree que[N]; // 创建一个队列,队列存储都是树的结点
int front = 0, rear = 0; // 队列的 队首指针和队尾指针
int level = 0; // 遍历时的层数
if(bt != NULL){
que[++ rear] = bt; // 入队 根节点入队
while( front != rear){ // 队列为空
level ++; // 把层级更新一下
int size = (rear - front + N) % N; // 计算队列的长度,当前层的结点个数
for(int i = 0; i < size; i ++){
bt = que[++ front]; // 出队的,队首指针后移
printf("%d", bt -> data); // 改代码 都是这个地方
if(bt -> lchild)que[++ rear] = bt -> lchild; // 当前结点左孩子加入到队列末尾
if(bt -> rchild)que[++ rear] = bt -> rchild; // 当前结点右孩子加入到队列末尾
}
}
}
}
2. 求第k层结点个数
2.1 求第k层的结点个数
int KlevelCount(BiTree bt, int k){
BiTree que[N]; // 创建一个队列,队列存储都是树的结点
int front = 0, rear = 0; // 队列的 队首指针和队尾指针
int level = 0; // 遍历时的层数
int count = 0; // 叶结点总数
if(bt != NULL){
que[++ rear] = bt; // 入队 根节点入队
while( front != rear){ // 队列为空
level ++; // 把层级更新一下
int size = (rear - front + N) % N; // 计算队列的长度,当前层的结点个数
for(int i = 0; i < size; i ++){
bt = que[++ front]; // 出队的,队首指针后移
// 如果当前结点是第k层
if(level == k) count ++;
// printf("%d", bt -> data); // 改代码 都是这个地方
if(bt -> lchild)que[++ rear] = bt -> lchild; // 当前结点左孩子加入到队列末尾
if(bt -> rchild)que[++ rear] = bt -> rchild; // ...
}
}
}
return count;
}
2.2 求第k层度为1的结点个数
找到第k层中度为1的节点个数。 --2022东北大学
int KlevelCount(BiTree bt, int k){
BiTree que[N]; // 创建一个队列,队列存储都是树的结点
int front = 0, rear = 0; // 队列的 队首指针和队尾指针
int level = 0; // 遍历时的层数
int count = 0; // 叶结点总数
if(bt != NULL){
que[++ rear] = bt; // 入队 根节点入队
while( front != rear){ // 队列为空
level ++; // 把层级更新一下
int size = (rear - front + N) % N; // 计算队列的长度,当前层的结点个数
for(int i = 0; i < size; i ++){
bt = que[++ front]; // 出队的,队首指针后移
// 如果当前遍历的第k层结点
if(level == k){
// 度为1的判定
if(bt -> lchild && !bt -> rchild) count ++;
else if(!bt -> lchild && bt -> rchild) count ++;
}
// printf("%d", bt -> data); // 改代码 都是这个地方
if(bt -> lchild)que[++ rear] = bt -> lchild; // 当前结点左孩子加入到队列末尾
if(bt -> rchild)que[++ rear] = bt -> rchild; // ...
}
}
}
return count;
}
2.3 求第k层的叶节点个数
int KlevelLeaf(BiTree bt, int k){
BiTree que[N]; // 创建一个队列,队列存储都是树的结点
int front = 0, rear = 0; // 队列的 队首指针和队尾指针
int level = 0; // 遍历时的层数
int count = 0; // 叶结点总数
if(bt != NULL){
que[++ rear] = bt; // 入队 根节点入队
while( front != rear){ // 队列为空
level ++; // 把层级更新一下
int size = (rear - front + N) % N; // 计算队列的长度,当前层的结点个数
for(int i = 0; i < size; i ++){
bt = que[++ front]; // 出队的,队首指针后移
if(level == k && bt -> lchild == NULL && bt -> rchild == NULL) count ++;
//printf("%d", bt -> data); // 改代码 都是这个地方
if(bt -> lchild)que[++ rear] = bt -> lchild; // 当前结点左孩子加入到队列末尾
if(bt -> rchild)que[++ rear] = bt -> rchild; // ...
}
}
}
return count;
}
3. 求第k层的特殊值
比如求解第k层结点个数的最大值,最小值,极差,和,平均值;all in one,根据题目适当选择需要的值。
int KlevelMathVal(BiTree bt, int k){
BiTree que[N]; // 创建一个队列,队列存储都是树的结点
int front = 0, rear = 0; // 队列的 队首指针和队尾指针
int level = 0; // 遍历时的层数
int sum = 0; // 结点之和
int maxs = -1e9, mins = 1e9; // 定义最大和最小值
int count = 0; // 结点个数
float average = 0; // 平均值
int rang = 0; // 极差
if(bt != NULL){
que[++ rear] = bt; // 入队 根节点入队
while( front != rear){ // 队列为空
level ++; // 把层级更新一下
int size = (rear - front + N) % N; // 计算队列的长度,当前层的结点个数
for(int i = 0; i < size; i ++){
bt = que[++ front]; // 出队的,队首指针后移
// 如果当前遍历的第k层结点
if(level == k){
count ++; // 结点个数 + 1
sum += bt -> data; // 结点总和 + 当前结点值
maxs = max(maxs, bt -> data); // 更新最大值
mins = mins(mins, bt-> data); //更新最小值
}
// printf("%d", bt -> data); // 改代码 都是这个地方
if(bt -> lchild)que[++ rear] = bt -> lchild; // 当前结点左孩子加入到队列末尾
if(bt -> rchild)que[++ rear] = bt -> rchild; // ...
}
}
}
rang = maxs - mins; // 得到极差
average = sum * 1.0 / count; // 结点之和 ÷ 结点个数
// return average; 平均数....
// return rang; 极差
// return maxs; // 返回最大值
// return mins; // 返回最小值
return 0;
}
4. 求二叉树的高度
像上述模板中level最后统计的结果也就是高度
4.1 非递归算法求二叉树的高度
int level(BiTree bt){
BiTree que[N]; // 创建一个队列,队列存储都是树的结点
int front = 0, rear = 0; // 队列的 队首指针和队尾指针
int level = 0; // 遍历时的层数
if(bt != NULL){
que[++ rear] = bt; // 入队 根节点入队
while( front != rear){ // 队列为空
level ++; // 把层级更新一下
int size = (rear - front + N) % N; // 计算队列的长度,当前层的结点个数
for(int i = 0; i < size; i ++){
bt = que[++ front]; // 出队的,队首指针后移
printf("%d", bt -> data); // 改代码 都是这个地方
if(bt -> lchild)que[++ rear] = bt -> lchild; // 当前结点左孩子加入到队列末尾
if(bt -> rchild)que[++ rear] = bt -> rchild; // 当前结点右孩子加入到队列末尾
}
}
}
return level;
}
4.2 求二叉树的带权路径长度
二叉树的带权路径长度(WPL)是二叉树中所有叶结点的带权路径长度之和,也就是每个叶结点的深度与权值之积的总和。
给定一棵二叉树 T,请你计算并输出它的 WPL。
注意,根节点的深度为 0。
– 2014年408真题 2016年北京交通
const int N = 100010;
int TreeWeightSum(BiTree bt){
BiTree que[N]; // 创建一个队列,队列存储都是树的结点
int front = 0, rear = 0; // 队列的 队首指针和队尾指针
int level = 0; // 遍历时的层数
int sum = 0;
if(bt != NULL){
que[++ rear] = bt; // 入队 根节点入队
while( front != rear){ // 队列为空
level ++; // 把层级更新一下
int size = (rear - front + N) % N; // 计算队列的长度,当前层的结点个数
for(int i = 0; i < size; i ++){
bt = que[++ front]; // 出队的,队首指针后移
// printf("%d", bt -> data); // 改代码 都是这个地方
if(bt -> lchild)que[++ rear] = bt -> left; // 当前结点左孩子加入到队列末尾
if(bt -> rchild)que[++ rear] = bt -> right; // ...
// 如果当前结点是叶节点,则sum累加 该节点的权值 * 层数
// (层数需要 - 1,因为根结点的层数是0)
if(bt -> lchild == NULL && bt -> rchild == NULL){
sum += (bt -> data * (level - 1));
}
}
}
}
return sum;
}
4.3 求二叉树的宽度
二叉树的宽度(即具有结点数最多的那一层上的结点个数)。–多张试卷考过
要求解宽度,对每层的size 取一个max即可
// 求二叉树的宽度
int TreeWidth(BiTree bt){
BiTree que[N]; // 创建一个队列,队列存储都是树的结点
int front = 0, rear = 0; // 队列的 队首指针和队尾指针
int level = 0; // 遍历时的层数
int width = 0; // 表示返回值
if(bt != NULL){
que[++ rear] = bt; // 入队 根节点入队
while( front != rear){ // 队列为空
level ++; // 把层级更新一下
int size = (rear - front + N) % N; // 计算队列的长度,当前层的结点个数
width = max(width, size);
for(int i = 0; i < size; i ++){
bt = que[++ front]; // 出队的,队首指针后移
// printf("%d", bt -> data); // 改代码 都是这个地方
if(bt -> lchild)que[++ rear] = bt -> lchild; // 当前结点左孩子加入到队列末尾
if(bt -> rchild)que[++ rear] = bt -> rchild; // ...
}
}
}
return width;
}
4.4 求二叉树的繁茂度(※)
一颗二叉树的繁茂程度 为 该二叉树的宽度与高度的乘积。
–数据结构1800 2024重邮
将4.1求高度和4.3求宽度的代码融合到一个函数里面即可
// 求二叉树的繁茂度
int TreeWidth(BiTree bt){
BiTree que[N]; // 创建一个队列,队列存储都是树的结点
int front = 0, rear = 0; // 队列的 队首指针和队尾指针
int level = 0; // 遍历时的层数
int width = 0; // 二叉树的宽度
if(bt != NULL){
que[++ rear] = bt; // 入队 根节点入队
while( front != rear){ // 队列为空
level ++; // 把层级更新一下
int size = (rear - front + N) % N; // 计算队列的长度,当前层的结点个数
width = max(width, size);
for(int i = 0; i < size; i ++){
bt = que[++ front]; // 出队的,队首指针后移
// printf("%d", bt -> data); // 改代码 都是这个地方
if(bt -> lchild)que[++ rear] = bt -> lchild; // 当前结点左孩子加入到队列末尾
if(bt -> rchild)que[++ rear] = bt -> rchild; // ...
}
}
}
return width * level; // 繁茂度 = 宽度 * 高度
}
5. 找指定结点所在层次
求出指定节点key在二叉树中的层次 --多张试卷考过
int findXlevel(BiTree bt, BiTree key){
BiTree que[N]; // 创建一个队列,队列存储都是树的结点
int front = 0, rear = 0; // 队列的 队首指针和队尾指针
int level = 0; // 遍历时的层数
if(bt != NULL){
que[++ rear] = bt; // 入队 根节点入队
while( front != rear){ // 队列为空
level ++; // 把层级更新一下
int size = (rear - front + N) % N; // 计算队列的长度,当前层的结点个数
for(int i = 0; i < size; i ++){
bt = que[++ front]; // 出队的,队首指针后移
if(bt == key) return level;
// printf("%d", bt -> data); // 改代码 都是这个地方
if(bt -> lchild)que[++ rear] = bt -> lchild; // 当前结点左孩子加入到队列末尾
if(bt -> rchild)que[++ rear] = bt -> rchild; // ...
}
}
}
return -1; // 表示没有找到结果
}
求出节点值x在二叉树中的层次
int findXlevel(BiTree bt, int x){
BiTree que[N]; // 创建一个队列,队列存储都是树的结点
int front = 0, rear = 0; // 队列的 队首指针和队尾指针
int level = 0; // 遍历时的层数
if(bt != NULL){
que[++ rear] = bt; // 入队 根节点入队
while( front != rear){ // 队列为空
level ++; // 把层级更新一下
int size = (rear - front + N) % N; // 计算队列的长度,当前层的结点个数
for(int i = 0; i < size; i ++){
bt = que[++ front]; // 出队的,队首指针后移
if(bt -> data == x) return level;
// printf("%d", bt -> data); // 改代码 都是这个地方
if(bt -> lchild)que[++ rear] = bt -> lchild; // 当前结点左孩子加入到队列末尾
if(bt -> rchild)que[++ rear] = bt -> rchild; // ...
}
}
}
return -1; // 表示没有找到结果
}
6. 其他问题
注意:以下标记 ※ 的适合于考92学校的作为练手题目
6.1 反转二叉树的奇数层 (※)
题目难度: 1431(1600以下为easy, 1600-2000的区间为mid)
题目链接:https://leetcode.cn/problems/reverse-odd-levels-of-binary-tree/description/
题目描述:
给你一棵 二叉树的根节点 root
,请你反转这棵树中每个 奇数 层的节点值。
- 例如,假设第 3 层的节点值是
[2,1,3,4,7,11,29,18]
,那么反转后它应该变成[18,29,11,7,4,3,1,2]
。
反转后,返回树的根节点。
(节点的 层数 等于该节点到根节点之间的边数, 也就是根节点是第0层)
题目思路:
在层序遍历的同时,开一个数组来记录一下奇数层的值,对该层的节点值进行一个反转
const int N = 100010;
void reverseTree(TreeNode *bt){
TreeNode *stk[N];// 保留奇数层的结点
int top = 0; // 栈顶指针
TreeNode *que[N]; // 创建一个队列,队列存储都是树的结点
int front = 0, rear = 0; // 队列的 队首指针和队尾指针
int level = -1; // 遍历时的层数
if(bt != NULL){
que[++ rear] = bt; // 入队 根节点入队
while( front != rear){ // 队列为空
level ++; // 把层级更新一下
top = 0; // 清空栈
int size = (rear - front + N) % N; // 计算队列的长度,当前层的结点个数
for(int i = 0; i < size; i ++){
bt = que[++ front]; // 出队的,队首指针后移
// 如果当前遍历的是奇数层
if(level % 2 == 1){
stk[top ++] = bt;
}
if(bt -> left)que[++ rear] = bt -> left; // 当前结点左孩子加入到队列末尾
if(bt -> right)que[++ rear] = bt -> right; // ...
}
if(level % 2 == 1){
for(int i = 0; i < size / 2; i ++){
// 交换首尾元素
swap(stk[i]->val, stk[top - 1 - i]-> val);
}
}
}
}
}
TreeNode* reverseOddLevels(TreeNode* root) {
reverseTree(root);
return root;
}
6.2 二叉树的右视图(※)
题目来源: 2020年杭电 lc 199
题目提交链接:https://leetcode.cn/problems/binary-tree-right-side-view/description/
题目思路: 使用层序遍历,每层出队的末尾元素也就是二叉树的右视图
TreeNode *q[N];
vector<int> rightSideView(TreeNode* root) {
vector<int> ans;
if(root == nullptr) return ans;
int front = 0, rear = 0;
int level = 0;
q[rear ++] = root;
while(front != rear){
int size = (rear - front + N) % N;
for(int i = 0; i < size;i ++){
TreeNode *tt = q[front ++];
if(i == size - 1) ans.push_back(tt -> val);
if(tt -> left) q[rear ++] = tt -> left;
if(tt -> right) q[rear ++] = tt -> right;
}
}
return ans;
}
6.3 二叉树中的第k大层和(※)
题目来源: lc 335
题目难度: mid => 可以放部分985学校的压轴题(思路不难,但有一定代码量)
题目提交链接:https://leetcode.cn/problems/kth-largest-sum-in-a-binary-tree/description/
题目思路: 层序遍历统计所有层数的和 + 快速排序的选择算法求第k大
如果对层序遍历后的结果序列,直接排序取第k大,那么整体的时间复杂度就变为O(nlogn)
层序遍历On,快速排序的选择第k大算法也是On,则整体On
6.4 层数最深叶子节点的和 (※)
题目来源: lc 1302
题目难度: 1388
题目提交链接:https://leetcode.cn/problems/deepest-leaves-sum/
题目思路: 层序遍历统计每层的节点和sum,用ans保存最新遍历层的节点和,那么最后一层也就是全是叶节点,ans 就是我们计算的结果
const int N = 100010;
TreeNode *q[N];
int deepestLeavesSum(TreeNode* root) {
if(root == nullptr) return 0;
int front = 0, rear = 0;
int level = 0;
int ans = 0; // 保留结果
q[rear ++] = root;
while(front != rear){
int size = (rear - front + N) % N;
int sum = 0; // 保留第k层叶结点和
for(int i = 0; i < size;i ++){
TreeNode *tt = q[front ++];
sum += tt -> val;
if(tt -> left) q[rear ++] = tt -> left;
if(tt -> right) q[rear ++] = tt -> right;
}
ans = sum;
}
return ans;
}
三、 统计类问题(以递归为主)
递归函数(框架):
- 递归出口, 判断空子树对应函数的值
- 函数体(调用递归函数求解问题的过程)
这类问题可以归纳总结为一句话:
总子树的结果 = 左子树的结果 + 右子树的结果 + 当前结点带来的影响
可以认真体会一下这句话的含义
1. 计算二叉树的高度(⭐)
计算二叉树的高度 –考试频率极高,至少在不同的试卷见过30次以上
可以用一套框架解决此类问题:
1)先写递归出口,也就是空子树的返回值
显然空子树的高度为0, 返回0即可
int height(BiTree root){
if(!root) return 0; // 递归出口
....// 待完善
}
2)再思考,二叉树的高度怎么归纳成 => (总子树的结果 = 左子树的结果 + 右子树的结果 + 当前结点带来的影响)
根据二叉树的高度定义:二叉树中结点的最大层次
由此可以推出 h总 = max(h左, h右) + 1
注意此处的1是指根节点自身也占一层,所以可以理解为(当前结点带来的影响)
int height(BiTree root){
if(!root) return 0; // 递归出口
int left_height = height(root -> left); // 计算左子树的高度
int right_height = height(root -> right); // 计算右子树的高度
return max(left_height, right_height) + 1;
}
2. 计算叶子节点个数(⭐)
计算二叉树的叶节点个数 –考试频率极高,至少在不同的试卷见过30次以上
可以用一套框架解决此类问题:
1)先写递归出口,也就是空子树的返回值
显然空子树的结点个数为0,叶节点肯定为0, 返回0即可
int leafCount(BiTree root){
if(!root) return 0; // 递归出口
....// 待完善
}
2)再思考,二叉树的叶节点个数怎么归纳成 => (总子树的结果 = 左子树的结果 + 右子树的结果 + 当前结点带来的影响)
根据二叉树的叶节点定义:无左孩子且无右孩子的点
由此可以推出 leaf总 = leaf左 + leaf右 + (1 或 0)
注意此处的1或0,指根节点如果也是叶节点则统计结果 + 1,如果不是则不加,所以可以理解为(当前结点带来的影响)
可以看下图仅有根节点的二叉树示例:
int leafCount(BiTree bt){
if(bt == NULL) return 0;
int leaf_left = leafCount(bt -> left); // 算一下左子树的叶子节点个数
int leaf_right = leafCount(bt -> right); // 算一下右子树的叶子节点个数
// 当前点是叶节点, 则统计结果 + 1
if( bt -> left == NULL && bt-> right == NULL){
return leaf_left + leaf_right + 1;
}
return leaf_left + leaf_right; // 当前点不是叶节点,则不加
}
如果统计树(孩子链表法)转为二叉树的叶子结点个数
利用性质:二叉树上无左孩子的点即为树的叶子节点;
所以在上述代码中,改一下叶节点的判断条件即可
int leafCount(BiTree bt){
if(bt == NULL) return 0;
int leaf_left = leafCount(bt -> left); // 算一下左子树的叶子节点个数
int leaf_right = leafCount(bt -> right); // 算一下右子树的叶子节点个数
// 当前点是叶节点, 则统计结果 + 1
if( bt -> left == NULL && bt-> right == NULL){
return leaf_left + leaf_right + 1;
}
return leaf_left + leaf_right; // 当前点不是叶节点,则不加
}
3. 统计二叉树的所有结点之和
求二叉树中所有节点data之和 --2015东南大学 2015南京理工 2017中国农业
可以用一套框架解决此类问题:
1)先写递归出口,也就是空子树的返回值
显然空子树没有结点,则结点之和为0, 返回0即可
int treeSum(BiTree root){
if(!root) return 0; // 递归出口
....// 待完善
}
2)再思考,二叉树的所有结点之和怎么归纳成 => (总子树的结果 = 左子树的结果 + 右子树的结果 + 当前结点带来的影响)
根据二叉树的结点之和定义:二叉树中所有结点权值相加即可
由此可以推出 sum总 = sum左 + sum右 + 当前结点的值
注意此处的当前结点的值,可以理解为(当前结点对整个问题带来的影响)
int treeSum(BiTree root){
if(!root) return 0; // 递归出口
int left_sum = treeSum(root->left); // 计算左子树的权值和
int right_sum = treeSum(root->right); // 计算右子树的权值和
return left_sum + right_sum + root->data; // sum总 = 左子树的权值和 + 右子树的权值和 + 当前结点的值
}
4. 统计二叉树的所有结点个数
求二叉树中所有节点个数 --多张试卷出过
可以用一套框架解决此类问题:
1)先写递归出口,也就是空子树的返回值
显然空子树没有结点,则结点之和为0, 返回0即可
int treeSum(BiTree root){
if(!root) return 0; // 递归出口
....// 待完善
}
2)再思考,二叉树的所有结点之和怎么归纳成 => (总子树的结果 = 左子树的结果 + 右子树的结果 + 当前结点带来的影响)
根据二叉树的结点之和定义:二叉树中所有结点权值相加即可
由此可以推出 sum总 = sum左 + sum右 + 当前结点的值
注意此处的当前结点的值,可以理解为(当前结点对整个问题带来的影响)
int treeSum(BiTree root){
if(!root) return 0; // 递归出口
int left_sum = treeSum(root->left); // 计算左子树的权值和
int right_sum = treeSum(root->right); // 计算右子树的权值和
return left_sum + right_sum + root->data; // sum总 = 左子树的权值和 + 右子树的权值和 + 当前结点的值
}
5. 计算二叉树中的最值
利用递归,求二叉树中的结点的最大值。
(结点的值域范围在0 - 1e9内)
可以用一套框架解决此类问题:
1)先写递归出口,也就是空子树的返回值
空子树没有结点,返回结点值域的极小值0即可
注意此处不要设置其他值,影响到统计结果
int maxTreeval(TreeNode *root){
if(!root) return 0;
....// 待完善
}
2)再思考,二叉树的结点最大值怎么归纳成 => (总子树的结果 = 左子树的结果 + 右子树的结果 + 当前结点带来的影响)
根据二叉树的结点最大值定义:二叉树中所有结点中的最大的
由此可以推出 max_val总 = max( max_val左 , max_val右, 当前结点的值)
也就是在左子树最大值,右子树最大值,以及根节点选出最大值
为什么要比较当前结点的值,可以理解为(当前结点对整个问题带来的影响)
例如下图:
递归是从下往上计算的过程,也就是下面的结点计算好的值反馈给上层,上层才能统计出最终结果
int maxTreeval(TreeNode *root){
if(!root) return 0;
int left_max = maxTreeval(root -> left); // 计算出左子树最大值
int right_max = maxTreeval(root -> right); // 计算出右子树最大值
// 三个值的比较, max(当前结点, 左子树最大, 右子树最大)
return max( max(left_max, right_max), root -> val);
}
如果值域为-1e9 ~ 1e9,则递归出口返回值为-1e9
// 最大值负数
int maxTreeval(TreeNode *root){
if(!root) return -1e9;
int left_max = maxTreeval(root -> left);
int right_max = maxTreeval(root -> right);
// 三个值的比较, max(当前结点, 左子树最大, 右子树最大)
return max( max(left_max, right_max), root -> val);
}
同理,计算二叉树中结点的最小值,也就是三者取最小即可
// 最小值
int minTreeval(TreeNode *root){
if(!root) return 1e9;
int left_min = minTreeval(root -> left);
int right_min = minTreeval(root -> right);
// 三个值的比较, min(当前结点, 左子树最小, 右子树最小)
return min( min(left_min, right_min), root -> val);
}
四、善用全局变量和函数传参
此类方法虽然很简单,但到万不得已不要随便用。因为这种方法大部分情况下,使用全局变量来统计的结果,所以针对某些限制函数返回类型和传参个数的题目,这样写有可能不得分。
先说一下通用模板:
int ans; // 如果记录值就用int类型
// 通过函数传参,比如说 height
void dfs(BiTree bt, int ...){
if(bt == NULL){
// 走到空节点时更新 ans
ans = ....;
return ;
}
// 注意修改函数传参,比如height 此处就传height + 1
dfs(bt -> lchild, ....);
dfs(bt -> rchild, .....);
}
简单解释一下:这种递归方式可以理解为根节点到叶节点过程中不断更新函数传参,直到叶节点下一层也就是空指针的位置,更新全局变量。
1. 重写统计类问题
1.1 计算二叉树的高度
二叉树的高度:也就可以理解为叶子节点最深的层数。
int ans = 0;
void dfs(BiTree bt, int h){
if(bt == NULL){
ans = max(ans, h);
return ;
}
dfs(bt -> lchild, h + 1);
dfs(bt -> rchild, h + 1);
}
int height(BiTree bt){
dfs(bt, 0);
return ans;
}
1.2 计算二叉树的叶子结点个数
int cnt = 0;
void dfs(BiTree bt){
if(bt == NULL){
return ;
}
// 如果是叶节点
if(bt -> left == NULL && bt -> right == NULL){
cnt += 1;
}
dfs(bt -> lchild);
dfs(bt -> rchild);
}
int leafCount(BiTree bt){
dfs(bt);
return cnt;
}
1.3 统计二叉树的所有结点之和
int sum = 0;
void dfs(BiTree bt){
if(bt == NULL){
return ;
}
sum += bt -> data;
dfs(bt -> lchild);
dfs(bt -> rchild);
}
int treeSum(BiTree bt){
dfs(bt);
return sum;
}
1.4 统计二叉树的所有结点个数
int cnt = 0;
void dfs(BiTree bt){
if(bt == NULL){
return ;
}
cnt += 1;
dfs(bt -> lchild);
dfs(bt -> rchild);
}
int nodeCount(BiTree bt){
dfs(bt);
return cnt;
}
1.5 计算二叉树中的最大值
int ans = 0;
void dfs(BiTree bt, int maxs){
if(bt == NULL){
ans = max(ans, maxs);
return ;
}
maxs = max(maxs, bt -> data);
dfs(bt -> lchild, maxs);
dfs(bt -> rchild, maxs);
}
int maxTreeval(BiTree bt){
dfs(bt, -1e9);
return ans;
}
2. 二叉树中根节点到叶节点的问题(※)
2.1 根节点到叶节点路径的最大和
对一个二叉树,每个结点对应一个权值,写一个递归函数,返回从根节点到各叶子结点经过的权值最大的和,给出函数头:int maxF(BiTree T) --2019 北理工
int ans = 0;
void dfs(BiTree root, int maxs){
if(root == NULL){
return ;
}
// 如果是叶节点,加一下路径上的权值最大值
if(root -> lchild == NULL && root -> rchild == NULL){
ans += maxs;
return ;
}
maxs = max(maxs, root -> data);
dfs(root -> lchild, maxs);
dfs(root -> rchild, maxs);
}
int maxF(BiTree T){
dfs(T, 0);
return ans;
}
2.2 二叉树的一条最长路径
采用二叉链表存储的二叉树,在二叉树上找出一条从根节点到叶子节点的最长路径,输出路径长度和各个结点的值。 – 2014 851山大
// 注意: 树里面的路径,只能由父节点 => 子节点,只能单向的
int mx = 0;
int ans[N];
void dfs(TreeNode* root, int path[], int size){
if(!root){
// 如果目标路径的长度大于答案路径的长度
if(size > mx){
for(int i = 0; i < size; i ++)
ans[i] = path[i];
mx = size;
}
return ;
}
path[size ++] = root->val; // 路径序列插入树的节点值
dfs(root -> left, path, size);
dfs(root -> right, path, size);
}
int findLongestPath(TreeNode* root){
int p[N];
dfs(root, p, 0);
for(int i = 0; i < mx; i ++)
printf("%d ", ans[i]);
return mx;
}
2.3 是否存在路径总和等于target
题目描述:给你二叉树的根节点 root
和一个表示目标和的整数 targetSum
。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum
。如果存在,返回 true
;否则,返回 false
提交链接:https://leetcode.cn/problems/path-sum/description/
bool flag = false;
void dfs(TreeNode *root, int nowSum){
if(root == nullptr){
return ;
}
nowSum -= root -> val;
if(root -> left == nullptr && root -> right == nullptr){
if(nowSum == 0) flag = true;
return ;
}
dfs(root -> left, nowSum);
dfs(root -> right, nowSum);
}
bool hasPathSum(TreeNode* root, int targetSum) {
dfs(root, targetSum);
return flag;
}
2.4 根节点到叶节点数字之和 (※)
题目描述:给你一个二叉树的根节点 root
,树中每个节点都存放有一个 0
到 9
之间的数字。
每条从根节点到叶节点的路径都代表一个数字:
- 例如,从根节点到叶节点的路径
1 -> 2 -> 3
表示数字123
提交链接:https://leetcode.cn/problems/sum-root-to-leaf-numbers/description/
int ans = 0;
void dfs(TreeNode *root, int x) {
if (root == nullptr) {
return;
}
x = x * 10 + root->val;
// 如果当前点是叶子节点
if (root->left == nullptr && root -> right == nullptr) {
ans += x;
return;
}
dfs(root->left, x);
dfs(root->right, x);
}
int sumNumbers(TreeNode* root) {
dfs(root, 0);
return ans;
}
2.5 根节点到叶节点二进制之和 (※)
题目描述:给出一棵二叉树,其上每个结点的值都是 0
或 1
。每一条从根到叶的路径都代表一个从最高有效位开始的二进制数。
- 例如,如果路径为
0 -> 1 -> 1 -> 0 -> 1
,那么它表示二进制数01101
,也就是13
。
2.4的代码里面 x = x * 10 + root -> val; 改成 x = x * 2 + root -> val 即可,因为是二进制
提交链接:https://leetcode.cn/problems/sum-of-root-to-leaf-binary-numbers/description/
int ans = 0;
void dfs(TreeNode *root, int x) {
if (root == nullptr) {
return;
}
x = x * 2 + root->val;
// 如果当前点是叶子节点
if (root->left == nullptr && root -> right == nullptr) {
ans += x;
return;
}
dfs(root->left, x);
dfs(root->right, x);
}
int sumRootToLeaf(TreeNode* root) {
dfs(root, 0);
return ans;
}
五、特殊二叉树的判定问题
此类判定问题,可以归纳出一个框架:
1)先写递归出口,一般返回true即可
2)整个树是xx树 = 左子树是 && 右子树是 && 当前结点满足该树的定义
总树的结果 = 左子树的结果 + 右子树的结果 + 当前结点带来的影响, 这句话的延申版本
1. 二叉排序树的判定问题
写出判定一颗二叉链表存储的二叉树是否是二叉排序树的算法。 — --2020北邮 2019广东工业 2018重庆理工
提交链接:https://leetcode.cn/problems/validate-binary-search-tree/description/
首先,先纠正一下网上很多人写的错误写法,比如利用层序遍历,判定当前点是否大于左孩子,且小于右孩子;这种做法肯定是错的。
下图举例说明:
我们根据框架来做一下这题:
1)先写递归出口,一般返回true即可
typedef long long LL;
bool isBST(TreeNode *root, LL min_limit, LL max_limit){
if(root == NULL ) return true; // 空子树直接返回true
... // 待完善
}
2)总树是二叉排序树 = 左子树是 && 右子树是 && 当前结点满足二叉排序树的定义
二叉排序树的定义:根节点大于左子树的所有值,根节点大于右子树的左右值,且需要递归满足
这里面难点在于:怎么判定当前结点满足二叉排序树的定义,我们可以通过递归设置上下限的形式来做这个判定
可以看下图解释:
typedef long long LL;
bool isBST(TreeNode *root, LL min_limit, LL max_limit){
if(!root) return true; // 空子树返回true
bool is_left = isBST(root -> left, min_limit, root -> val); // 左子树是否满足二叉排序树
bool is_right = isBST(root -> right, root -> val, max_limit); // 右子树是否满足二叉排序树
bool is_sort = root -> val < max_limit && root -> val > min_limit; // 当前点是否在上下限之间
return is_left && is_right && is_sort; // 三者都满足返回true
}
// 调用的验证函数
bool isValidBST(TreeNode* root) {
if(!root) return true;
return isBST(root, -1e12, 1e12);
}
做法2:
二叉排序树的中序遍历是有序的,这个是充要条件,所以可以根据验证中序遍历的序列是否有序来进行判定
typedef long long ll;
const ll inf = 1e18;
vector<ll> ans;
void inorder(TreeNode* root){
if(!root) return ;
inorder(root->left);
ans.push_back(root->val);
inorder(root->right);
}
bool isValidBST(TreeNode* root) {
inorder(root); // 二叉搜索树的中序遍历 是有序的
if(ans.size() < 2) return true;
// 判断 数组是否有序 递增顺序
for(int i = 0; i < ans.size() - 1; i ++ ){
// 大于等于就不是递增
if(ans[i] >= ans[i + 1])return false;
}
return true;
}
2. 平衡二叉树的判定问题
2.1 求二叉树的平衡因子
设计算法,求一个二叉树根结点的平衡因子。
根据平衡因子的概念:左子树高度 和 右子树高度的差值,取绝对值,即 |h左 - h右|
那调用一下【三.1 】计算二叉树的高度函数即可快速求解
int height(BiTree root){
if(!root) return 0; // 递归出口
int left_height = height(root -> left); // 计算左子树的高度
int right_height = height(root -> right); // 计算右子树的高度
return max(left_height, right_height) + 1;
}
// 求解根节点的平衡因子
int banlanceFact(BiTree root){
if(!root) return 0;
return abs( height(root -> left) - height(root -> right));
}
2.2 判定平衡二叉树
平衡二叉树的判断 --2015浙大 2014武汉理工
提交链接:https://leetcode.cn/problems/balanced-binary-tree/description/
我们根据框架来做一下这题:
1)先写递归出口,一般返回true即可
bool isBalanced(TreeNode* root) {
if(!root) return true; // 先写递归出口
... // 待完善
}
2)总树是平衡二叉树 = 左子树是 && 右子树是 && 当前结点满足平衡二叉树的定义
平衡二叉树的定义:每个结点的平衡因子都要小于等于1,且需要递归满足
这里面难点在于:怎么判定当前结点满足平衡二叉树的定义,我们可以通过暴力求每个结点的平衡因子,检验平衡因子的值是否小于等于1,来做这个判定
int height(TreeNode *root){
if(!root) return 0;
return max(height(root->left), height(root->right)) + 1;
}
bool isBalanced(TreeNode* root) {
if(!root) return true;
// 验证左子树是否为平衡二叉树
bool left_banlance = isBalanced(root -> left);
// 验证右子树是否为平衡二叉树
bool right_banlance = isBalanced(root -> right);
// 当前结点的平衡因子是否小于等于1
bool node_valid = abs(height(root -> left) - height(root -> right)) <= 1;
return left_banlance && right_banlance && node_valid; // 三者都满足才是平衡二叉树
}
该题有On解法(一般不会要求On):
可参考自底向上的递归方式:https://leetcode.cn/problems/balanced-binary-tree/solutions/377216/ping-heng-er-cha-shu-by-leetcode-solution/
2.3 判定平衡二叉排序树 (※)
额外加一个满足二叉排序树的条件即可
3. 完全二叉树的判定问题(※)
给你一棵二叉树的根节点 root
,请你判断这棵树是否是一棵 完全二叉树 。
在一棵 完全二叉树 中,除了最后一层外,所有层都被完全填满,并且最后一层中的所有节点都尽可能靠左。
–2020南邮 2014哈工大 2019南昌大学 2016南京航空 2012 吉林大学 2016中国海洋
提交链接:https://leetcode.cn/problems/check-completeness-of-a-binary-tree/description/
3,4这两题比较特殊,不能采用上述框架来做
题目思路:
利用层序遍历,检查什么时候出现孩子为空的点,出现该点后,后面遍历的结点,孩子只能为空。
示例图如下:
const int N = 100010;
TreeNode *q[N];
bool isCompleteTree(TreeNode* root) {
int front = 0, rear = 0;
bool flag = false; // 标记是否出现空的位置
q[rear ++] = root;
while(front != rear){
int size = (rear - front + N) % N;
for(int i = 0; i < size; i ++){
TreeNode* tt = q[front ++];
if(!tt){
flag = true; // 出现空结点,标记为true
continue;
}
if(flag) return false; // 如果前方位置有空结点,且当前结点又不为空,则不是完全二叉树
q[rear ++] = tt->left;
q[rear ++] = tt->right;
}
}
return true;
}
4. 满二叉树的判定问题(※)
判定满二叉树 --大概2-3次
两种思路:
① 利用层序遍历,统计第i层的结点数量是否是 $ 2 ^ i $个
bool isCompleteTree(TreeNode* root) {
int front = 0, rear = 0;
q[rear ++] = root;
int it = 1; // 第一层存1个
while(front != rear){
int size = (rear - front + N) % N;
if(size != it) return false;
it *= 2; // 下一层必定存上一层的2倍
for(int i = 0; i < size; i ++){
TreeNode* tt = q[front ++];
if(tt -> left) q[rear ++] = tt -> left;
if(tt -> right) q[rear ++] = tt -> right;
}
}
return true;
}
② 假设是满二叉树,按照顺序存储的形式,收集每个结点的编号,比如根节点是1,则左孩子是2 * 1, 右孩子是2 * 1 + 1,第i个点的编号是i,则左孩子是2 * i ,右孩子是2 * i + 1;把编号收集到一个序列中,进行排序,检验序列里面是否是1~n的连续序列
比如高度为3的满二叉树,收集后的序列,排序后的结果一定为[1,2,3,4,5,6,7,8]
只需要验证末尾元素是2 的幂次方,且等于结点数
vector<int> ids;
void helper(TreeNode* root, int id){
if(!root)return ;
ids.push_back(id);
helper(root -> left, id * 2);
helper(root -> right, id * 2 + 1);
}
bool isCompleteTree(TreeNode* root) {
helper(root, 1);
sort(ids.begin(), ids.end());
int n = ids.size();
// n & (n - 1) 判定n是否为2的幂次
// 则 n & (n + 1)判定 n + 1 是否为2的幂次
// 验证条件;n + 1既是2的幂次方且 最后一项的id == 长度
return (n & (n + 1) == 0) && (n == ids.back());
}
5. 单值二叉树 (※)
如果二叉树每个节点都具有相同的值,那么该二叉树就是单值二叉树。
只有给定的树是单值二叉树时,才返回 true
;否则返回 false
难度分数:1178
题目提交链接:https://leetcode.cn/problems/univalued-binary-tree/
示例图:
我们根据框架来做一下这题:
1)先写递归出口,空子树一般返回true即可
bool isUnivalTree(TreeNode* root) {
if(!root) return true;
.... // 待完善
}
2)总树是单值二叉树 = 左子树是 && 右子树是 && 当前结点满足单值二叉树的定义
单值二叉树的定义:所有结点都具有相同的值
这里面难点在于:怎么判定当前结点满足单值二叉树的定义,也就是判断当前结点,左孩子(如果存在), 右孩子(如果存在),三者值相同
bool isUnivalTree(TreeNode* root) {
if(!root) return true;
bool left_valid = isUnivalTree(root -> left); // 校验左子树是否为单值二叉树
bool right_valid = isUnivalTree(root -> right); // 检验右子树是否为单值二叉树
// 检验当前点是否满足单值二叉树,
// 也就是根节点,左孩子(如果存在),右孩子(如果存在)
// 三者值需要相同
if(root -> left != nullptr){
bool flag = root -> val == root -> left -> val;
if(root -> right){
return root -> val == root -> right -> val && flag && left_valid && right_valid;
}
return flag && left_valid && right_valid;
}else if(root -> right != nullptr){
bool flag = root -> val == root -> right -> val;
return flag && left_valid && right_valid;
}
return left_valid && right_valid;
}
6. 正则二叉树(※)
正则二叉树的条件: 每个结点的度均为0或者2
换句话说,也就是每个结点的度不为1
我们根据框架来做一下这题:
1)先写递归出口,空子树一般返回true即可
bool isNormalTree(TreeNode* root) {
if(!root) return true;
.... // 待完善
}
2)总树是正则二叉树 = 左子树是 && 右子树是 && 当前结点满足正则二叉树的定义
正则二叉树的定义: 每个结点的度均为0或者2
这里面难点在于:怎么判定当前结点满足正则二叉树的定义,也就是判断当前结点的度不为1即可
bool isNormalTree(TreeNode* root) {
if(!root) return true;
bool left_valid = isNormalTree(root -> left); // 左子树是否为正则二叉树
bool right_valid = isNormalTree(root -> right); // 右子树是否为正则二叉树
bool is_strict = (root -> left == NULL && root -> right == NULL) || (root -> left && root -> right); // 检验当前结点是否满足度为0或者度为2
return left_valid && right_valid && is_strict;
}
7. 对称二叉树 (※)
给你一个二叉树的根节点 root
, 检查它是否轴对称。
提交链接:https://leetcode.cn/problems/symmetric-tree/description/
题目思路:设置两个指针p和q,p每次左移,q每次右移;如果p和q都存在,检验p和q的值是否相同;如果两者有一个不存在,则返回false
两个二叉树的相似[ 结构相同,节点值可以不同 ],判断方法类似
bool check(TreeNode *p, TreeNode *q) {
if (!p && !q) return true;
if (!p || !q) return false;
return p->val == q->val && check(p->left, q->right) && check(p->right, q->left);
}
bool isSymmetric(TreeNode* root) {
return check(root, root);
}
六、二叉排序树的相关问题
提到二叉排序树,同时可以联想到crud,也就是增删改查
增加结点,经典题目,通过逐个插入元素的方式建立二叉排序树
删除结点,经典题目,删除二叉树排序树中权值为x的点(较难)
查找结点,很多经典题,找前驱,后继,第k大,值为x的结点
1. 二叉排序树的建立
给定一个n个元素的序列,根据序列顺序建立一颗二叉排序树。
题目解释:相当于对一个空的二叉排序树,插入n个结点
// 二叉排序树中插入某节点
void insert(TreeNode* &root, int x){
if(!root) root = new TreeNode(x);
else if(x < root->val) insert(root->left, x);
else insert(root->right, x);
}
// 根据一个序列a创建二叉排序树
int a[N];
void build(TreeNode *&root, int n){
for(int i = 0; i < n; i ++){
insert(root, a[i]);
}
}
2. 二叉排序树删除结点(※)
二叉排序树删除结点的三个判断条件:
- 如果待删除点是叶节点,直接删即可(也就是赋值为null)
- 如果待删除点是单分支结点,替换左右孩子即可
- 如果待删除点是双分支结点,找到左子树的最大值进行替换
// 二叉排序树中删除某节点
void remove(TreeNode* &root, int x){
if(!root) return;
if(x < root-> val)remove(root->left, x);
else if(x > root->val) remove(root->right, x);
else{
// 等于x时删除
// 1. 叶子结点,直接删除
if(!root->left && !root->right){
root = NULL;
}
// 如果左子树为空,直接替换成右儿子即可
else if(!root -> left) root = root -> right;
// 如果右子树为空,直接替换成左儿子即可
else if(!root -> right) root = root -> left;
// 如果两个儿子都存在
else{
TreeNode* p = root -> left;
while(p->right) p = p->right;
root->val = p->val;
remove(root->left, p->val);
}
}
}
3. 二叉排序树的查找
3.1 二叉排序树查找值为x的结点
设计在二叉排序树上查找结点 X(健值等于 key)的算法。 --2021 广东外语贸大学
TreeNode* searchBST(TreeNode* root, int key) {
if(!root) return NULL;
if(root->val == key) return root;
if(root->val > key) return searchBST(root->left, val);
return searchBST(root->right, key);
}
3.2 二叉排序树找前驱和后继结点 (※)
找二叉排序树找不大于x的最大节点 --2020 电子科大 2021哈工大
可以理解为找二叉排序树值为x点的前驱
int get_pre(TreeNode* root, int x){
if(!root) return -INF;
if(root->val >= x) return get_pre(root->left, x);
return max(root->val, get_pre(root->right, x));
}
// 找后继的代码
// 二叉排序树中找不小于x的最小节点
int get_next(TreeNode* root, int x){
if(!root) return INF;
if(root->val <= x) return get_next(root->right, x);
return min(root->val, get_next(root->left, x));
}
3.3 获取二叉排序树的第k大结点(※)
最简单的思路:中序遍历拿到序列后,直接返回中序遍历的倒数第k个元素即可
当然也有On做法,不过多叙述
// 获得二叉排序第k大节点
// 思路:中序遍历得到序列,返回序列的倒数第k项
int res[N], top = 0;
void inorderTravel(TreeNode* root){
if(!root) return ;
inorderTravel(root -> left);
res[top ++] = root -> val;
inorderTravel(root -> right);
}
int kthLargest(TreeNode* root, int k) {
top = 0;
inorderTravel(root);
return res[top - k];
}
3.4 求给定结点在二叉排序树中的层次
编写算法求给定结点在二叉排序树中所在层数 – 2014南邮
int get_height(TreeNode* root, int h,int x){
if(!root) return 0;
if(root-> val == x)return h;
if(root -> val > x){
return get_height(root->left, h + 1, x);
}
return get_height(root->right, h + 1, x);
}
4. 两个二叉排序树的合并
试着编写一算法,将两颗二叉排序树合并为一颗二叉排序树。–2010 南邮
题目思路:将二叉排序树B里面的结点插入到A里面即可
void insert(TreeNode* &root, int x){
if(!root) root = new TreeNode(x);
else if(x < root->val) insert(root->left, x);
else insert(root->right, x);
}
// 二叉排序树的合并操作
void combineTree(TreeNode* &A, TreeNode* B){
if(B == NULL)return ;
insert(A, B -> val);
combineTree(A, B -> left);
combineTree(A, B -> right);
}
七、 顺序存储的相关问题(※)
这部分见的题目比较少,后续待补充。
已知一棵具有 n 个结点的完全二叉树被顺序存储在一维数组 A[n]中,试着编程一个算法输出 A[i]的结点的双亲与所有孩子–2021暨南
题目思路:这题难度较小,基本上会记公式就能做;题目说n个结点存在数组A[n]中,则证明下标在0 ~ n - 1区间内
那么,下标为i的双亲 (i - 1) / 2 下取整 ,这里需要做一个特判就是,根节点下标为0,双亲可以打印-1或者提示信息(表示不存在)
左孩子为2i + 1, 右孩子为2i + 2,这里也需要做一个特判就是,如果左右孩子的下标大于n - 1,可以打印-1或者(表示不存在)提示信息
#include<bits/stdc++.h>
const int N = 100010;
typedef long long LL;
using namespace std;
int n, T;
int A[N];
void parent(int i){
if(i == 0){
cout << "无双亲结点" << endl;
}else{
cout << A[(i - 1) / 2] << endl;
}
}
void children(int i){
int left_child = 2 * i + 1;
int right_child = 2 * i + 2;
if(left_child >= n ){
cout << "无左孩子" << endl;
}else{
cout << A[left_child] << endl;
}
if(right_child >= n){
cout << "无右孩子" << endl;
}else{
cout << A[right_child] << endl;
}
}
void solve(){
cin >> n;
for(int j = 0; j < n; j ++) cin >> A[j];
int i;
cin >> i;
parent(i);
children(i);
}
int main(){
// cin >> T;
T = 1;
while(T --){
solve();
}
return 0;
}
八、其他问题
1. 两个序列构造二叉树(※)
1.1 根据前中序构造唯一二叉树
根据二叉树的前序和中序序列建立该二叉树的二叉链表 --2009东北大学 2020华中科技大学
提交链接:https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/description/
1.2 根据中后序构造唯一二叉树
根据二叉树的后序和中序序列建立该二叉树的二叉链表 --2002华东师范
提交链接:https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/description/
1.3 只给定前序或者后序
给定一个前序序列的二叉排序树,建立该二叉树的二叉链表。
利用二叉排序树的中序遍历的有序性,对前序遍历序列进行排序得到中序序列
给定一个前序序列的满二叉树,建立该二叉树的二叉链表。
…
2. 最近公共祖先问题–LCA(※)
求二叉树中两个节点的最近公共祖先 --有点难度 部分985学校会考 王道书原题
推荐一下这个讲解视频:【二叉树的最近公共祖先】 https://www.bilibili.com/video/BV1W44y1Z7AR/
题目提交链接:https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/description/
分类讨论的图示例:
TreeNode *lowestCommonAncestor(TreeNode *root, TreeNode *p, TreeNode *q) {
if (root == nullptr || root == p || root == q) {
return root;
}
TreeNode *left = lowestCommonAncestor(root->left, p, q);
TreeNode *right = lowestCommonAncestor(root->right, p, q);
if (left && right) {
return root;
}
return left ? left : right;
}
加一点难度:一棵二叉树,求两个结点间的最小长度,即它们到各自公共祖先的长度之和。–2019哈工大 王道书原题改编
思路: 先找到最近公共祖先之后,写一个函数来计算两个结点之间的距离,跑两次该函数,第一次跑祖先节点到p的距离,第二次跑祖先节点到q的距离,两者相加即可
进阶题目(竞赛考点 ※※):
如果有m个查询任意两个结点的最近公共祖先
【D09 倍增算法 P3379【模板】最近公共祖先(LCA)】 https://www.bilibili.com/video/BV1vg41197Xh/
3. 树的直径(※)
二叉树根节点左右子树相隔最远的叶子节点之间的距离。(树的直径) --2016电子科大 2019东北大学 2018 吉林大学 2019南京大学
推荐一下这个讲解视频:【树形 DP:树的直径【基础算法精讲 23】】 https://www.bilibili.com/video/BV17o4y187h1/
提交链接:https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/description/
4. 二叉树的相似
4.1 判定二叉树是否相似
判定二叉树是否相似,指结构相同,节点值可以不同。 --2018西电,大概3-5次
bool isSilmilar(TreeNode* root1, TreeNode* root2){
if(!root1 && !root2) return true;
if(!root1 || !root2) return false;
return isSilmilar(root1 -> left, root2 -> left) && isSilmilar(root1 -> right, root2 -> right);
}
4.2 判定二叉树是否相同
判定两个树是否完全相同 --2018东北大学 2020南昌大学 √
提交链接:https://leetcode.cn/problems/same-tree/description/
在上述题目加一个值的判断
bool isSame(TreeNode* root1, TreeNode* root2){
if(!root1 && !root2) return true;
if(!root1 || !root2) return false;
if(root1 -> val != root2->val) return false;
return isSame(root1 -> left, root2 -> left) && isSame(root1 -> right, root2 -> right);
}
结语
至此,考研对树相关的代码题也就结束了,如果想要学习更多的树的算法相关的内容,可参考以下知识点:
- 树形dp (大厂笔试压轴题难度)
可观看灵神的算法入门视频,讲了三个最基础的树形dp问题:
【树形 DP:树的直径【基础算法精讲 23】
https://www.bilibili.com/video/BV17o4y187h1/
- 树上信息维护:例如树的DFS序(一般结合树状数组或者线段树做树上的区间维护),树上差分,树上倍增,树链剖分(省赛夺冠 或者 区域银 ~ 金难度)等等