考研数据结构:从入门到精通(树)——迈向算法高手之路

考研数据结构:从入门到精通(树)——迈向算法高手之路

​ 树的代码题在各个自命题学校的考研题里面几乎是必考题,所以对树的代码题在考研复习过程中显得尤为重要。本篇文章主要详细分析树代码题的不同考法,在刷完该题单后,对树的相关代码题将会有一个质的飞跃。

​ 通过对树章节代码题的深入学习,将能够更好地应对考研中的数据结构题目,迈向算法高手的行列。下面,就让我们开启这段从入门到精通的树代码题学习之旅。

注意:下面标题中加※,表示题目难度较大,适合于考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); // 递归遍历右子树
    }
}

非递归写法(栈):

模拟动画效果:https://leetcode.cn/problems/binary-tree-preorder-traversal/solutions/461821/er-cha-shu-de-qian-xu-bian-li-by-leetcode-solution/

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); // 递归遍历右子树
    }
}

非递归写法:

模拟的动画效果:https://leetcode.cn/problems/binary-tree-inorder-traversal/solutions/412886/er-cha-shu-de-zhong-xu-bian-li-by-leetcode-solutio/

  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);  // 访问根节点
    }
}

非递归写法:

模拟动画:https://leetcode.cn/problems/binary-tree-postorder-traversal/solutions/431066/er-cha-shu-de-hou-xu-bian-li-by-leetcode-solution/

  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. 递归出口, 判断空子树对应函数的值
  2. 函数体(调用递归函数求解问题的过程)

这类问题可以归纳总结为一句话:

总子树的结果 = 左子树的结果 + 右子树的结果 + 当前结点带来的影响

可以认真体会一下这句话的含义

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 ,树中每个节点都存放有一个 09 之间的数字。

每条从根节点到叶节点的路径都代表一个数字:

  • 例如,从根节点到叶节点的路径 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 根节点到叶节点二进制之和 (※)

题目描述:给出一棵二叉树,其上每个结点的值都是 01 。每一条从根到叶的路径都代表一个从最高有效位开始的二进制数。

  • 例如,如果路径为 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. 二叉排序树删除结点(※)

二叉排序树删除结点的三个判断条件:

  1. 如果待删除点是叶节点,直接删即可(也就是赋值为null)
  2. 如果待删除点是单分支结点,替换左右孩子即可
  3. 如果待删除点是双分支结点,找到左子树的最大值进行替换
// 二叉排序树中删除某节点
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序(一般结合树状数组或者线段树做树上的区间维护),树上差分,树上倍增,树链剖分(省赛夺冠 或者 区域银 ~ 金难度)等等
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力学习前端+Go的小菜鸡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值