二叉树

时间:2021-07-18
本文章向大家介绍二叉树,主要包括二叉树使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
目录

二叉树定义:

树相关术语:

  • 节点的度:一个节点含有的子树的个数称为该节点的度;

  • 树的度:一棵树中,最大的节点的度称为树的度;

  • 叶节点或终端节点:度为零的节点;

  • 父亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;

  • 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;

  • 兄弟节点:具有相同父节点的节点互称为兄弟节点;

  • 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;

  • 树的高度或深度:树中节点的最大层次;

  • 堂兄弟节点:父节点在同一层的节点互为堂兄弟;

  • 节点的祖先:从根到该节点所经分支上的所有节点;

  • 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。

  • 森林:由m(m>=0)棵互不相交的树的集合称为森林;

二叉树存储

  • 二叉树既可以用链式存储,也可以用数组顺序存储。

    • 数组顺序存储的方式比较适合完全二叉树

    • 其他类型的二叉树用数组存储会比较浪费存储空间,一般用链式存储

  • 基于指针或引用的链式存储

  • 基于数组的顺序存储法

    • 如果节点X存储在数组中下标为i的位置:

      • 下标为2 * i 的位置存储的就是左子节点,

      • 下标为2 * i + 1的位置存储的就是右子节点。

      • 反过来,下标为i/2的位置存储就是它的父节点。

      • 通过这种方式,我们只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为1的位置),这样就可以通过下标计算,把整棵树都串起来。

常见二叉树类别

  • 满二叉树

    • 解释1:除了最后一层无任何子结点外,每一层上的所有结点都有两个子结点的二叉树。

    • 解释2:叶子节点全都在最底层,并且除了叶子节点之外,每个节点都有左右两个子节点

  • 完全二叉树

    • 叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最⼤

    • 堆其实就是一种完全二叉树,最常见的存储方式就是数组

    • 数组顺序存储的方式比较适合完全二叉树

  • 二叉搜索(查找)树(Binary Search Tree)

    • 二叉查找树要求在树中的任意一个节点:(左子节点和它的子孙< 节点< 右子节点和它的子孙)

      • 其左子树中的每个节点的值,都要小于这个节点的值,

      • 而右子树节点的值都大于这个节点的值。

    • 二叉查找树最大的特点就是,支持动态数据集合的快速插入、删除、查找操作

    • 二叉查找树是为了实现快速查找而生的。不过,它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据

  • 平衡二叉查找树(ALV树)

    • 平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:

      • 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
    • 平衡二叉搜索树是二叉搜索树和平衡二叉树的结合

  • 最后一棵不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。

二叉树链表存储代码表示:

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) {}
};
  • 叶子节点:左右子树都为空
if(root->left == nullptr && root->right == nullptr)  // 当前节点为叶子节点

二叉树的遍历方式

L102 二叉树的层次遍历(二叉树+队列)

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        // 二叉树的层序遍历使用队列辅助
        // 刚开始根节点先进队列
        // 正式遍历:
        // 先记录当前队列长度(该层节点个数),并将该长度作为遍历条件进行遍历取出该层的所有节点:
            // 先将一个节点(指针)从队列中出队,然后将它的左节点和右节点进队
            // 遍历完后队列的长度为下一层的节点个数
        vector<vector<int>> result;
        queue<TreeNode*> que;
        if(root) que.push(root); // 根节点不为空

        // 遍历每层
        while(!que.empty()){
            int size = que.size();  // 该层节点数
            vector<int> vec; // 记录该层节点值

            // 逐个取出该层节点,同时将该节点的左右子节点加入队列
            while(size--){
                TreeNode* node = que.front();
                que.pop();
                vec.push_back(node->val);
                if(node->left) que.push(node->left);
                if(node->right) que.push(node->right);
            }
            result.push_back(vec);
        }
        return result;
    }
};

JO22 从上往下打印二叉树

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :val(x), left(NULL), right(NULL) {}
};*/
class Solution {
public:
    vector<int> PrintFromTopToBottom(TreeNode* root) {
        vector<int> result;
        if(root == nullptr) return result;
        queue<TreeNode*> que;
        que.push(root);
        TreeNode* node;  // 临时节点
        while(!que.empty()){
            node = que.front();
            que.pop();
            result.push_back(node->val);
            
            if(node->left) que.push(node->left);
            if(node->right) que.push(node->right);
        }
        return result;
    }
};

L199 二叉树的右视图

class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        // 右视图就是层次遍历中的每层的最后一个元素
        queue<TreeNode*> que;
        vector<int> result;
        if(root) que.push(root);
        while(!que.empty()){
            int size = que.size();
            while(size--){
                TreeNode* node = que.front();
                que.pop();
                if(size==0){
                    result.push_back(node->val);
                }
                if(node->left) que.push(node->left);
                if(node->right) que.push(node->right);
            }
        }
        return result;
    }
};

L103 二叉树的锯齿形层序遍历

class Solution {
public:
    vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
        // 其实就是层次遍历,不过使用一个标签来决定本层是从左到右还是从右到左
        queue<TreeNode*> que;
        vector<vector<int>> result;
        if(root) que.push(root);
        bool flag = true; // flag为true时从左到右,否则从右到左
        // 逐层遍历
        while(!que.empty()){
            int size =  que.size();  // 当前层节点数
            vector<int> vec(size, 0);  // 记录该层节点值
            // 从队列中逐个取出该层节点、对该节点进行处理、往队列中加入该层节点的子节点
            while(size--){
                root = que.front();
                que.pop();
                vec[flag ? vec.size() - size -1 : size] = root->val;
                if(root->left) que.push(root->left);
                if(root->right) que.push(root->right);
            }
            result.push_back(move(vec));
            flag = !flag;
        }
        return result;
    }
};

L637 二叉树的层平均值

二叉树的修改与构造

JS27 JO18 二叉树的镜像/翻转二叉树(二叉树递归遍历DFS 或二叉树层次遍历BFS)





  • 方法1:
class Solution {
public:
    TreeNode* mirrorTree(TreeNode* root){
        if (root == NULL) return root;
        swap(root->left, root->right);  // 中
        mirrorTree(root->left);         // 左
        mirrorTree(root->right);        // 右
        return root;
    }
}
  • 方法2:树的递归遍历,递归法O(n) O(n)
class Solution {
public:
// 实际上就是交换左右子节点 
/*
根据二叉树镜像的定义,考虑递归遍历(dfs)二叉树,交换每个节点的左 / 右子节点,即可生成二叉树的镜像。

递归解析:
(1)终止条件: 当节点 root 为空时(即越过叶节点),则返回 null ;
(2)递推工作:
    1)初始化节点 tmp ,用于暂存 root的左子节点;
    2)开启递归 右子节点 mirrorTree(root.right),并将返回值作为 root 的 左子节点 。
    3)开启递归 左子节点 mirrorTree(tmp) ,并将返回值作为 root 的 右子节点 。
(3)返回值: 返回当前节点 root 
*/
    TreeNode* mirrorTree(TreeNode* root) {
        if(root == nullptr) return nullptr;
        TreeNode* tmp = root->left;
        root->left = mirrorTree(root->right);
        root->right = mirrorTree(tmp);
        return root;
    }

}
  • 方法3:树的层次遍历,辅助栈(或队列)O(n) O(n)**
/*
利用栈(或队列)遍历树的所有节点 node ,并交换每个 node 的左 / 右子节点。
算法流程:
    1特例处理: 当 root 为空时,直接返回 null ;
    2初始化: 栈(或队列),本文用栈,并加入根节点 root 。
    3循环交换: 当栈 stack 为空时跳出;
        出栈: 记为 node ;
        添加子节点: 将 node 左和右子节点入栈;
        交换: 交换 node 的左 / 右子节点。
    4返回值: 返回根节点 root 
注意:左右子树哪个先入栈或进队都可以
*/
class Solution {
public:
    // (1)使用辅助栈:前序遍历之迭代 + 栈
    TreeNode* mirrorTree(TreeNode* root){
        if(root == nullptr) return nullptr;
        stack<TreeNode*> stack;

        stack.push(root);  // 先将头结点压栈,以便进入循环
        while(!stack.empty()){
            // 取出当前遍历头节点
            TreeNode* node = stack.top();
            stack.pop();

            // 将左右子树压栈(不压空节点),压栈先左后右还是先右后左都一样
            if(node->left != nullptr) stack.push(node->left);
            if(node->right != nullptr) stack.push(node->right);
            // 交换当前头结点的左右子树
            TreeNode* tmp = node->left;
            node->left = node->right;
            node->right = tmp;

            /* 先交换后压栈和先压栈后交换效果一样
            // 交换当前头结点的左右子树
            swap(node->left, node->right);  // 交换地址

            // 将交换后的左右子树压栈(不压空节点)
            if(node->right) stack.push(node->right);
            if(node->left) stack.push(node->left);
            */
        }
        return root;
    }
}



class Solution {
public:
    // (2)使用辅助队列
    TreeNode* mirrorTree(TreeNode* root){
        if(root == nullptr) return nullptr;
        queue<TreeNode*> que;
        que.push(root);
        while(!que.empty()){
            TreeNode* node = que.front();
            que.pop();
            // 进队是先左后右
            if(node->left != nullptr) que.push(node->left);
            if(node->right != nullptr) que.push(node->right);
            TreeNode* tmp = node->left;
            node->left = node->right;
            node->right = tmp;
        }
        return root;
    }
}

JS7 JO4 重建二叉树

首先要知道一个结论,前序/后序+中序序列可以唯一确定一棵二叉树,所以自然而然可以用来建树。
看一下中序和后序有什么特点,中序[9,3,15,20,7] ,后序[9,15,7,20,3];

有如下特征:

  1. 后序中右起第一位3肯定是根结点,我们可以据此找到中序中根结点的位置rootin;

  2. 中序中根结点左边就是左子树结点,右边就是右子树结点,即[左子树结点,根结点,右子树结点],我们就可以得出左子树结点个数为int left = rootin - leftin;

  3. 后序中结点分布应该是:[左子树结点,右子树结点,根结点];

  4. 根据前一步确定的左子树个数,可以确定后序中左子树结点和右子树结点的范围;

  5. 如果我们要前序遍历生成二叉树的话,下一层递归应该是:

    • 左子树:root->left = pre_order(中序左子树范围,后序左子树范围,中序序列,后序序列);

    • 右子树:root->right = pre_order(中序右子树范围,后序右子树范围,中序序列,后序序列);

  6. 每一层递归都要返回当前根结点root;

前序遍历序列:[ 根节点 | 左子树 | 右子树 ]

中序遍历序列:[ 左子树 | 根节点 | 右子树 ]

后序遍历序列:[ 左子树 | 右子树 | 根节点 ]

前序/后序+中序序列可以唯一确定一棵二叉树

(1)前序遍历的首元素 为 树的根节点 node 的值。

(2)在中序遍历中搜索根节点 node 的索引 ,可将 中序遍历 划分为 [左子树 | 根节点 | 右子树]

(3)根据中序遍历中的左(右)子树的节点数量,可将 前序遍历 划分为 [根节点 | 左子树 | 右子树]

(4)重复(1)-(3)

class Solution {
public:
    /*
    单层递归逻辑:
    (1)取前序数组第一个元素作为当前头节点
    (2)在中序数组中找到该元素位置作为切割点,将中序数组切割成中序左数组和中序右数组
    (3)根据中序的结果大小将前序数组切割成前序左数组和前序右数组
    */
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder){
         if(preorder.size() == 0 || inorder.size()==0) return NULL;

        // 取前序数组第一个元素作为当前根节点
        int rootvalue = preorder[0];
        TreeNode* root = new TreeNode(rootvalue);

        // 如果刚创建出来的节点是叶子节点(数组元素长度为1),直接返回提前结束当前递归
        if(preorder.size() == 1) return root;

        // 寻找中序数组的切割点即根节点所在位置
        int index;
        for(index = 0; index < inorder.size(); ++index){
            if(inorder[index]==rootvalue) break;
        }

        // 切割中序数组:利用切割点(根节点)
        // 左中序数组:左闭右开区间,[0, index)
        vector<int> leftInorder(inorder.begin(), inorder.begin()+index);
        // 右中序数组:[index+1,end]
        vector<int> rightInorder(inorder.begin()+index+1, inorder.end());
        
        // 切割前序数组
        // 注意:中序数组⼤⼩⼀定是和前序数组的⼤⼩相同的(这是必然)。
        // 中序数组我们都切成了左中序数组和右中序数组了,那么前序数组就可以按照左中序数组的⼤⼩来切割,切成左前序数组和右前序数组。
        // 左前序数组:[1, leftInorder.size+1)
        vector<int> leftPreorder(preorder.begin()+1, preorder.begin()+leftInorder.size()+1);
        // 右前序数组:[leftInorder.size+1,end)
        vector<int> rightPreorder(preorder.begin()+leftInorder.size()+1, preorder.end());  

        // 找当前节点的左节点
        root->left = buildTree(leftPreorder,leftInorder);
        // 找当前节点的右节点
        root->right = buildTree(rightPreorder,rightInorder);
        // 返回根节点
        return root;
    }
};

L106 从中序与后序遍历序列构造二叉树

  • 第一步:如果数组大小为零的话,说明是空节点了。

  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。

  • 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点

  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)

  • 第五步:通过中序左数组和中序右数组元素个数来切割后序数组,切成后序左数组和后序右数组
    第六步:递归处理左区间和右区间

class Solution {
public:
 /*
    单层递归逻辑:
    (1)取后续数组最后一个元素
    (2)在中序数组中找到该元素位置作为切割点,将中序数组切割成中序左数组和中序右数组
    (3)根据中序的结果将后续数组切割成后续左数组和后续右数组
    */
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder){
        // 数组元素取完了
        if(inorder.size()==0 || postorder.size()==0) return NULL;

        // 取后序数组的最后一个元素作为根节点
        int rootvalue = postorder[postorder.size()-1];
        TreeNode* root = new TreeNode(rootvalue);

        // 如果刚创建出来的节点是叶子节点(数组元素长度为1),直接返回提前结束当前递归
        if(postorder.size() == 1) return root;

        // 寻找中序数组的切割点,即根节点所在位置
        int index;
        for(index = 0; index < inorder.size(); ++index){
            if(inorder[index]==rootvalue) break;
        }

        // 切割中序数组:利用切割点
        // 左中序数组:左闭右开区间,[0, index)
        vector<int> leftInorder(inorder.begin(), inorder.begin()+index);
        // 右中序数组:[index+1,end]
        vector<int> rightInorder(inorder.begin()+index+1, inorder.end());

        // 切割后序数组
        // 注意:中序数组⼤⼩⼀定是和后序数组的⼤⼩相同的(这是必然)。
        // 中序数组我们都切成了左中序数组和右中序数组了,那么后序数组就可以按照左中序数组的⼤⼩来切割,切成左后序数组和右后序数组。
        // 左后序数组:[0, leftInorder.size)
        vector<int> leftPostorder(postorder.begin(), postorder.begin()+leftInorder.size());
        // 右后序数组:[leftInorder.size,end)
        vector<int> rightPostorder(postorder.begin()+leftInorder.size(), postorder.end()-1);  // 后序数组舍弃末尾元素

        // 找当前节点的左节点
        root->left = buildTree(leftInorder, leftPostorder);
        // 找当前节点的右节点
        root->right = buildTree(rightInorder, rightPostorder);
        // 返回根节点
        return root;
    }
};

JS37 序列化二叉树(未写)

二叉树的属性

L257 二叉树的所有路径:前序遍历(DFS)+回溯+递归

详解了回溯与递归的关系,一般用vector好回溯,一个递归一个回溯,配套

class Solution {
public:
    vector<string> binaryTreePaths(TreeNode* root) {
        vector<string> result;  // 当到叶子节点时就将path中的路径转换成string形式存到result中
        vector<int> path; // 以数值形式记录当前路径,当这条路径完整时再转string

        if(root == nullptr) return result;

        traversal(root, path, result);
        return result;
    }
private:
    void traversal(TreeNode*root, vector<int>& path, vector<string>& result){

        // 将当前节点加入到path中
        path.push_back(root->val);

        // 判断当前节点是不是叶子节点,如果是叶子节点,记录当前路径
        if(root->left == nullptr && root->right == nullptr){

            // 将path记录的当前路径转换成题目要求的格式
            string spath;
            for(int i=0; i< path.size()-1; i++){
                spath += to_string(path[i]);
                spath += "->";
            }
            // 最后一个节点单独记录(叶子节点)
            spath += to_string(path[path.size()-1]);

            // 记录一个路径
            result.push_back(spath); 
        }

        // 由于前面没有判空,那么在这里递归的时候,如果为空就不进行下一层递归了。所以递归前要进行判读,判断当前递归节点否为空
        // 左节点不是空节点
        if(root->left){
            traversal(root->left, path, result);
            path.pop_back(); // 回溯,去掉的是递归进去后加入的左节点的值
        }
        // 右节点不是空节点
        if(root->right){
            traversal(root->right, path, result);
            path.pop_back();  // 回溯
        } 
    }

L112 路径总和I:判断二叉树中是否有和为某一值的路径(常考!!!!!,返回值的利用)





class Solution {
public:
    bool hasPathSum(TreeNode* root, int targetSum) {
        // 递归的时候使用递减的targetSum
        // 如果到了叶⼦节点且sum等于叶子节点的值,说明找到了⽬标和,否则就是没找到:

        // 空节点:如果前面到了叶子节点但sum不等于叶子节点的值就会到这一步
        if(!root) return false;  
        // 叶子节点:如果到了叶⼦节点且sum等于叶子节点的值,说明找到了⽬标和
        if(!root->left && !root->right && targetSum == root->val) return true;  
        
        // targetSum-root->val隐含了回溯
        // 利用或的特性,只要或前面判断为真,后面就不会进行判断
        return hasPathSum(root->left, targetSum-root->val) || hasPathSum(root->right, targetSum-root->val);  
    }
};

JS34 JO24 L113 路径总和II:返回二叉树中所有和为某一值的路径(前序遍历+路径记录+回溯)


解题思路



  • 方法1:
/**
 * 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:
    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
        // 比路径总和I需要多记录一下当前遍历树的数组状态
        vector<vector<int>> result;  // 记录符合要求的路径结果
        vector<int> path;  // 记录当前路径

        if(!root) return result;  // 处理空树

        dfs(root, targetSum, path, result);
        return result;
    }

private:
    void dfs(TreeNode* root, int targetSum, vector<int>& path, vector<vector<int>>& result){
        // 判空节点不在这处理了,挪到后面处理
        // if(!root) return;  // 如果前面到了叶子节点但sum不等于叶子节点的值就会到这一步

        path.push_back(root->val); // 将当前节点的值加入到路径
        
        // 如果当前节点为叶子节点且targetSum等于叶子节点的值,说明找到了⽬标路径,将路径添加到result中
        if(!root->left && !root->right && targetSum==root->val){
            result.push_back(path); 
            return; 
        }

        // 节点为空的情况在这里处理了
        // 如果左节点不为空,往左下走;如果为空,就不往左下走了
        if(root->left) {
            dfs(root->left, targetSum - root->val, path, result);  
            path.pop_back();  // 回溯
        }
        // 如果右节点不为空,往右下走;如果为空,就不往右下走了
        if(root->right) {
            dfs(root->right, targetSum - root->val, path, result);  
            path.pop_back();  // 回溯
        }
    }
};

L437 路径总和III(未写)

L124 二叉树中的最大路径和(字节常考!!!!!后序遍历)


解题思路



class Solution {
public:
    /*
     所有树的题目,都想成一颗只有根、左节点、右节点 的小树。然后一颗颗小树构成整棵大树,所以只需要考虑这颗小树即可。

    根据题意可知,一条最大的路径存在两种可能:
    1、存在一个节点,一条最大路径等于该节点的左右子树中路径较大的一颗子树,加上它自己后向其父节点回溯
    2、存在一个节点,一条最大路径包含其左右子树与其本身,不会再向父节点回溯
    */
    int ans = INT_MIN;  // 记录最大路径和

    int maxPathSum(TreeNode* root) {
        /**
        对于任意一个节点, 如果最大和路径包含该节点, 那么只可能是两种情况:
        1. 其左右子树中所构成的和路径值较大的那个加上该节点的值后向父节点回溯构成最大路径
        2. 左右子树都在最大路径中, 加上该节点的值构成了最终的最大路径
        **/
        
        dfs(root);
        return ans;
    }

    int dfs(TreeNode* root){
        if(!root) return 0; 

        // 如果子树路径和为负则应当置0表示最大路径不包括子树,因为是负收益,不要也罢
        int left = max(0, dfs(root->left));  //递归获取左子树中的最大路径和
        int right = max(0, dfs(root->right));  //递归获取右子树中的最大路径和
        
        // 判断该节点+左右子树的路径和是否大于当前路径和
        // 计算第二种情况下的路径长度,即左子树长度+右子树长度+节点本身的长度是否直接构成了最大路径
        ans = max(ans, root->val + left + right);  

        // 返回子树的最大路径和:选择左子树/右子树 + 当前节点
        // 计算第一种情况下的路径长度,取左右子树中路径较长的加上节点本身并向父节点回溯
        return max(left, right) + root->val;  
    }
};

JS55 JO38 二叉树最大深度(后序遍历DFS:左右中,知道左右子树结果才能知父节点结果时)

根节点树深度=max(左子树深度,右子树深度)+1

如果当前节点是空节点,则高度为0

求最大深度时一定是到了叶子节点的(隐式)




  • 方法1:后序遍历DFS
#include<iostream>
using namespace std;
struct TreeNode
{
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(): val(0),left(nullptr),right(nullptr){}
    TreeNode(int x): val(x), left(nullptr), right(nullptr){}
};

int maxDepth(TreeNode* root){
    // 当前节点为空节点,高度为0
    if(root == nullptr) return 0;
    return 1 + max(maxDepth(root->left), maxDepth(root->right));
}

int main(){
    TreeNode a(3);
    TreeNode b(9);
    TreeNode c(20);
    TreeNode d(15);
    TreeNode e(7);
    a.left = &b;
    a.right = &c;
    c.left = &d;
    c.right = &e;
    int depth = maxDepth(&a);
    cout << "maxDepth:" << depth <<endl;
}

L111 二叉树最小深度(后序遍历DFS)

根节点树深度=min(左子树深度,右子树深度)+1

如果当前节点是空节点,则高度为0

求最小深度时一定要注意显式到叶子节点(当前节点左子树为空,右子树不为空,则最小深度在右子树)


如果这么求的话,没有左孩子的分支会算为最短深度。

所以,如果左子树为空,右子树不为空,返回当前节点的最小深度是 1 + 右子树的深度。(1是节点自己)

反之,右子树为空,左子树不为空,返回当前节点的最小深度是 1 + 左子树的深度

最后如果左右子树都不为空,返回当前节点的最小深度是 左右子树深度最小值 + 1

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。」,注意是「叶子节点」。

什么是叶子节点,左右孩子都为空的节点才是叶子节点!

#include<iostream>
using namespace std;
struct TreeNode
{
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(): val(0),left(nullptr),right(nullptr){}
    TreeNode(int x): val(x), left(nullptr), right(nullptr){}
};
int minDepth(TreeNode* root){
    // 当前节点为空节点,高度为0
    if(root == nullptr) return 0;

    // 当前节点左子树为空,右不为空,最小深度继续往右子树找
    if(root->left == nullptr && root->right != nullptr) return 1 + minDepth(root->right);
    // 当前节点右子树为空,左不为空,最小深度继续往左子树找
    if(root->left != nullptr && root->right == nullptr) return 1 + minDepth(root->left);

    // 左右子树都不为空,最小深度分别往左右子树找
    return 1 + min(minDepth(root->left), minDepth(root->right));
}
int main(){
    TreeNode a(3);
    TreeNode b(9);
    TreeNode c(20);
    TreeNode d(15);
    TreeNode e(7);
    a.left = &b;
    a.right = &c;
    c.left = &d;
    c.right = &e;
    int depth = minDepth(&a);
    cout << "maxDepth:" << depth <<endl;
}

JS55 判断平衡二叉树(后序遍历DFS)

class Solution {
public:
    bool isBalanced(TreeNode* root) {
        return getDepth(root) == -1 ? false : true;
    }
private:
    // 如果以当前节点为根节点的二叉树是平衡二叉树,则返回高度,否则返回-1
    int getDepth(TreeNode* root){
        if(root == nullptr) return 0;

        // 获得左子树高度
        int leftDepth = getDepth(root->left);
        // 如果返回的高度不是-1,说明其左子树不是二叉平衡树,那以该节点为根节点的二叉树也不是二叉平衡树
        if(leftDepth == -1) return -1; 

        // 获得右子树高度
        int rightDepth = getDepth(root->right);
        // 如果返回的高度不是-1,说明其右子树不是二叉平衡树,那以该节点为根节点的二叉树也不是二叉平衡树
        if(rightDepth == -1) return -1;

        // 判断两子树高度差,如果高度差大于1,那以该节点为根节点的二叉树也不是二叉平衡树,否则返回高度
        return abs(leftDepth - rightDepth) > 1 ? -1 : 1+max(leftDepth, rightDepth);
    }
};

JS28 JO58 对称的二叉树(后序遍历DFS)

通过判断左右子树是否对称才知道当前节点二叉树是否对称

class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        if(root == NULL) return true;
        return compare(root->left, root->right);
    }
private:
    bool compare(TreeNode* left, TreeNode* right){
        // 终止条件:
        // (1)左右节点为空
        if(left == nullptr && right == nullptr) return true;
        // (2)左节点不为空,右节点为空;左节点为空,右节点不空;数值不相同
        if(left == nullptr || right == nullptr || left->val != right->val) return false;
        // 左右节点相同时,看他们的子节点情况
        // 根据两子节点的结果得到本节点结果
        return compare(left->left,right->right) && compare(left->right, right->left);
    }
};

完全二叉树节点个数(后序遍历DFS或层次遍历)

  • 递归法:后序遍历DFS
  • 迭代法:层次遍历
class Solution {
public:
    int countNodes(TreeNode* root) {
        // 层次遍历
        queue<TreeNode*> que;
        int num=0;
        if(root) que.push(root);
        while(!que.empty()){
            int size= que.size();
            num += size;
            while(size--){
                TreeNode* node = que.front();
                que.pop();
                if(node->left) que.push(node->left);
                if(node->right) que.push(node->right);
            }
        }
        return num;
    }
};

二叉树的公共祖先问题

JS68 二叉搜索树的最近公共祖先(前序遍历,搜索树有序)



  • 递归法:
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 每个当前节点分三种情况
        // (1)两个节点都在当前节点左子树中,继续搜索
        if(root->val > p->val && root->val > q->val){
            return lowestCommonAncestor(root->left, p, q);
        }
        // (2)两个节点都在当前节点右子树中,继续搜索
        if(root->val < p->val && root->val < q->val){
            return lowestCommonAncestor(root->right, p, q);
        }
        // (3)两个节点在当前节点左右两边或某个节点为当前节点且另一个节点在其左或右子树中,则当前节点就是这两节点的最近公共祖先
        return root; 
    }
};
  • 迭代法
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 找到一个节点,使得p和q在它左右两边,此时该节点即为p和q的最近公共祖先
        while(root){
            // (1)两个节点都在当前节点左子树中
            if(root->val > p->val && root->val > q->val){
                root = root->left;
            }

            // (2)两个节点都在当前节点右子树中
            else if(root->val < p->val && root->val < q->val){
                root = root->right;
            }

           // (3)两个节点在当前节点左右两边或某个节点为当前节点且另一个节点在其左或右子树中,则当前节点就是这两节点的最近公共祖先
           else break;
        }
        return root;
    }
};

JS68 普通二叉树的最近公共祖先(后序遍历)

后序遍历:天然的回溯过程,最先处理的一定是叶子节点

  • 如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。

  • 使用后序遍历,天然的回溯的过程,就是从低向上遍历节点,一旦发现如何这个条件的节点,就是最近公共节点了

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 终止条件:如果找到了节点p或q,或者遇到了空节点,就返回
        if(root == p || root == q || root == nullptr) return root;

        TreeNode* left = lowestCommonAncestor(root->left, p, q);
        TreeNode* right = lowestCommonAncestor(root->right, p, q);

        // 由左右节点返回的结果来决定本节点的返回结果
        // 左右节点返回值不为空,说明当前节点为最近公共节点
        if(left != nullptr && right != nullptr) return root;

        // 如果left为空,right不为空,说明目标结点是通过right返回的,反之亦然
        if(left == nullptr && right != nullptr) return right;
        if(left != nullptr && right == nullptr) return left;

        // left和right都为空
        return nullptr;
        
    }
};

二叉搜索树的遍历

二叉搜索树是一个有序树:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;

  • 它的左、右子树也分别为二叉搜索树

二叉搜索树的递归遍历

TreeNode* searchBST(TreeNode* root, int val) {
    if (root == NULL || root->val == val) return root;
    if (root->val > val) return searchBST(root->left, val);
    if (root->val < val) return searchBST(root->right, val);
    return NULL;
}

二叉搜索树的迭代遍历

class Solution {
public:
    TreeNode* searchBST(TreeNode* root, int val) {
        while (root != NULL) {
            if (root->val > val) root = root->left;
            else if (root->val < val) root = root->right;
            else return root;
        }
        return NULL;
    }
};
  • 大多二叉搜索树的题目,其实都离不开中序遍历,因为这样就是有序的。

  • **二叉搜索树的中序遍历为 递增序列 **

  • 中序遍历 为对二叉树作 “左、根、右” 顺序遍历,递归实现如下:

// 打印中序遍历
void dfs(Node* root) {
    if(root == nullptr) return;
    dfs(root->left); // 左
    cout << root->val << endl; // 根
    dfs(root->right); // 右
}

L700 二叉搜索树中的搜索

二叉搜索树与双向链表(中序遍历)




class Solution {
public:
    Node* treeToDoublyList(Node* root) {
        if(root == nullptr) return nullptr;
        dfs(root);
        // 遍历结束后将头尾相连
        head->left = pre;
        pre->right = head;
        
        return head; 
    }
private:
    Node *pre, *head; // head会一直指向链表第一个节点,即二叉搜索树的第一个前序遍历点
    void dfs(Node* cur){
        if(cur == nullptr) return;
        dfs(cur->left);

        // 当前节点的业务逻辑
        if(pre!=nullptr) pre->right = cur;  // 当 pre 不为空时: 修改双向节点引用
        else head = cur;  // 当 pre 为空时: 代表正在访问链表头节点,记为 head ;
        cur->left = pre;
        pre = cur;

        dfs(cur->right);
    }
};

JS33 二叉搜索树的后序遍历序列(后序遍历+分治)






匹配类二叉树

匹配类二叉树可以使用一种套路相对固定的递归函数,在周赛中和每日一题中多次出现,而第一次见到不太容易写出正确的递归解法,因此我们来总结一下。

这类题目与字符串匹配有些神似,求解过程大致分为两步:

  • 先将根节点匹配;

  • 根节点匹配后,对子树进行匹配。

而参与匹配的二叉树可以是一棵,与自身匹配;也可以是两棵,即互相匹配。如对称二叉树就是两棵树之间的匹配问题

JS26 树的子结构(前序遍历)




  • 方法1:
class Solution {
public:
    bool isSubStructure(TreeNode* A, TreeNode* B) {
        // 如果A为空或B为空
        if(A == nullptr || B==nullptr){
            return false;
        }
        dfs(A,B);
        return result;
    }
private:
    bool result = false;

    // dfs搜索找到A中节点值与B根节点相同的节点
    void dfs(TreeNode* A, TreeNode* B){
        if(A == nullptr) return;

        if(A->val == B->val && isSubTree(A,B)){
            // 找到跟B相等的结点,找到后开始递归判断两个树是否相等
            // 如果存在就立刻返回
            result = true;
            return;
        }

        dfs(A->left, B);
        dfs(A->right, B);
    }

    // 判断B是否为A的子树,并且二者根节点值相同
    bool isSubTree(TreeNode* A, TreeNode* B){
        // 如果B先遍历完,说明B已匹配完成(越过叶子节点),返回true
        if(B == nullptr) return true;

        // 如果A先遍历完,说明匹配失败
        if(A == nullptr) return false;

        return A->val == B->val && isSubTree(A->left, B->left) && isSubTree(A->right, B->right);
    }
};
  • 方法2:
class Solution {
public:
    bool isSubStructure(TreeNode* A, TreeNode* B) {
        if(A == nullptr || B == nullptr) return false;

        // 先序遍历树A中的每个节点
        // 判断树A中以当前节点为根节点的子树是否包含树B
        // 如果返回的结果是不包含即false,继续遍历下一个节点(这里利用了||的短路性质)

        return recur(A, B) || isSubStructure(A->left, B) || isSubStructure(A->right, B);
    }
private:
    bool recur(TreeNode* A, TreeNode* B){
        // 如果B先遍历完,说明B已匹配完成(越过叶子节点),返回true
        if(B == nullptr) return true;

        // 如果A先遍历完,说明匹配失败
        if(A == nullptr) return false;

        // 如果A和B的当前节点不相等,说明匹配失败
        if(A->val != B->val) return false;

        // A和B的当前节点匹配,继续比较下一个几点
        return recur(A->left, B->left) && recur(A->right, B->right);
    }
};

L572 另一个树的子树

class Solution {
public:
    bool isSubtree(TreeNode* s, TreeNode* t) {
        if(s == nullptr || t == nullptr) return false;

        // 先序遍历树s中的每个节点
        // 判断树s中以当前节点为根节点的子树是否包含树t
        // 如果返回的结果是不包含即false,继续遍历下一个节点(这里利用了||的短路性质)

        return recur(s, t) || isSubtree(s->left,t) || isSubtree(s->right, t);
    }
private:
    bool recur(TreeNode* s, TreeNode* t){
        // 如果s和t同时遍历完,说明t是s的子树
        if(s == nullptr && t==nullptr) return true;

        // 如果s或t先遍历完,说明匹配失败
        if(s == nullptr || t==nullptr) return false;

        // 如果s和t的当前节点不相等,说明匹配失败
        if(s->val != t->val) return false;

        // s和t的当前节点匹配,继续比较下一个几点
        return recur(s->left, t->left) && recur(s->right, t->right);
    }
};

L1367 二叉树中的列表


class Solution {
public:
    bool isSubPath(ListNode* head, TreeNode* root) {
        if(head == nullptr || root == nullptr) return false;
        return recur(head, root) || isSubPath(head, root->left) || isSubPath(head, root->right);
    }
private:
    bool recur(ListNode* head, TreeNode* root){
        // 如果链表先遍历完,说明最终匹配成功
        if(head == nullptr) return true;
        // 如果树一条路径先遍历完,说明匹配失败
        if(root == nullptr) return false;
        if(head->val != root->val) return false;
        return recur(head->next, root->left) || recur(head->next, root->right);
    }
};

原文地址:https://www.cnblogs.com/cmyDS/p/15009100.html