Day15|二叉树part02:102.二叉树的层序遍历、 226.翻转二叉树、101.对称二叉树

102.二叉树的层次遍历

leetcode链接:力扣题目链接

视频链接:讲透二叉树的层序遍历 | 广度优先搜索 | LeetCode:102.二叉树的层序遍历 (opens new window)

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

示例 1:


输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]

image

之前使用迭代法进行前中后序遍历时,借助的数据结构是,也就是DFS的思想。而层序遍历就是BFS的思想,需要借助队列

代码如下:

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> q;
        vector<vector<int>> res;
        if(root == nullptr){
            return res;
        }
        q.push(root);
        while(!q.empty()){
            vector<int> res1;
            unsigned int sz = q.size();
            for(int i = 0; i < sz; i++){
                TreeNode* cur = q.front();
                q.pop();
                res1.push_back(cur->val);
                if(cur->left != nullptr){
                    q.push(cur->left);
                }
                if(cur->right != nullptr) {
                    q.push(cur->right);
                }
            }
            res.push_back(res1);
        }
        return res;
    }
};

这里有以下几个需要注意的点:

  • C++中要先用q.front()取队首元素,再用q.pop()出队;
  • 在最开始的时候要用变量sz记录队列长度,因为运行过程中队列长度会变;
  • while的每次循环控制的是每一层,for循环则控制的该层的每个节点。

这就是层序遍历的框架,里面的res.push_back就是我们根据需要修改的内容。

用这个方法还可以解决111. 二叉树的最小深度问题

给定一个二叉树,找出其最小深度。

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

说明:叶子节点是指没有子节点的节点。

示例 1:

输入:root = [3,9,20,null,null,15,7]
输出:2
示例 2:

输入:root = [2,null,3,null,4,null,5,null,6]
输出:5

image

class Solution {
public:
    int minDepth(TreeNode* root) {
        queue<TreeNode*> q;
        int depth = 1;
        vector<vector<int>> res;
        if(root == nullptr){
            return 0;
        }
        q.push(root);
        while(!q.empty()){
            unsigned int sz = q.size();
            for(int i = 0; i < sz; i++){
                TreeNode* cur = q.front();
                q.pop();
                if(cur->left == nullptr &&  cur->right == nullptr){
                    return depth;
                }
                if(cur->left != nullptr){
                    q.push(cur->left);
                }
                if(cur->right != nullptr) {
                    q.push(cur->right);
                }

            }
            depth++;
        }
       return depth;
    }
};

相对于上面的res.push_back(),定义depth,初始化为1(因为记录的是节点数),然后判断叶子结点,就是左右孩子都是空就是叶子结点,遍历一次depth加1,第一次走到叶子结点就是目标depth并返回。

这道题直接使用BFS效率更高,因为可以更早找到目标并返回。BFS适合求最短路径的题目。比如求二叉树的最大深度,则用DFS更好些。

比如说下面的求最大深度的题目:

class Solution {
public:
    int maxDepth(TreeNode* root) {
        if (root == nullptr) {
            return 0;
        }
        // 利用定义,计算左右子树的最大深度
        int leftMax = maxDepth(root->left);
        int rightMax = maxDepth(root->right);
        // 整棵树的最大深度等于左右子树的最大深度取最大值,
        // 然后再加上根节点自己
        int res = max(leftMax, rightMax) + 1;

        return res;
    }
};

使用递归的算法,代码简介易懂,BFS追根溯源往往能找到最深的结果,而BFS找到的是最短的。

实际上用BFS也是可以完成的,层序遍历的次数就是最大深度。

class Solution {
public:
    int maxDepth(TreeNode* root) {
        if (root == NULL) return 0;
        int depth = 0;
        queue<TreeNode*> que;
        que.push(root);
        while(!que.empty()) {
            int size = que.size();
            depth++; // 记录深度
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
        }
        return depth;
    }
};

1161. 最大层元素和、637. 二叉树的层平均值、515. 每行的最大值

这三题都是类似的,在for循环里面改,while最后收集。以515为例:

class Solution {
public:
    vector<int> largestValues(TreeNode* root) {
        queue<TreeNode*> q;
        vector<int> res;
        if(root == nullptr){
            return res;
        }
        q.push(root);
        while(!q.empty()){
            int sz = q.size();
            int max = INT32_MIN;
            cout<< max;
            for(int i = 0; i < sz; i++){
                TreeNode* cur = q.front();
                if(cur->val > max){
                    max = cur->val;
                }
                q.pop();
                if(cur->left != nullptr){
                    q.push(cur->left);
                }
                if(cur->right != nullptr){
                    q.push(cur->right);
                }
            }
            // cout<< max;
            res.push_back(max);
        }
        return res;
    }
};

这里需要注意的是int max = INT32_MIN; ,不需要使用-INT_MAX

429. N叉树的层序遍历

N叉树的数据结构如下:

class Node {
public:
    int val;
    vector<Node*> children;

    Node() {}

    Node(int _val) {
        val = _val;
    }

    Node(int _val, vector<Node*> _children) {
        val = _val;
        children = _children;
    }
};

可以看到其孩子并不是只有左右两个了,而是一个Node*的数组。因此修改的就是原本左右孩子的入队部分:

class Solution {
public:
    vector<vector<int>> levelOrder(Node* root) {
        queue<Node*> q;
        vector<vector<int>> res;
        if(root == nullptr){
            return res;
        }
        q.push(root);
        while(!q.empty()){
            vector<int> res1;
            unsigned int sz = q.size();
            for(int i = 0; i < sz; i++){
                Node* cur = q.front();
                q.pop();
                res1.push_back(cur->val);
//                if(cur->left != nullptr){
//                    q.push(cur->left);
//                }
//                if(cur->right != nullptr) {
//                    q.push(cur->right);
//                }
                for(auto child : cur->children){
                    if(child != nullptr){
                        q.push(child);
                    }
                }
            }
            res.push_back(res1);
        }
        return res;
    }
};

这也就是BFS算法的雏形。

199. 二叉树的右视图

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

示例 1:

输入: [1,2,3,null,5,null,4]
输出: [1,3,4]

image

这题是求每一层的最后一个元素,故在for循环里面改,只要i到最后一个,就收集进res:

class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        queue<TreeNode*> q;
        vector<int> res;
        if(root == nullptr){
            return res;
        }
        q.push(root);
        while(!q.empty()){
            unsigned int sz = q.size();
            for(int i = 0; i < sz; i++){
                TreeNode* cur = q.front();
                cout << cur->val << ' ';
                if(i == sz - 1){
                    res.push_back(cur->val);
                }

                q.pop();
                if(cur->left != nullptr){
                    q.push(cur->left);
                }
                if(cur->right != nullptr) {
                    q.push(cur->right);
                }
            }
        }
        return res;
    }
};

116. 填充每个节点的下一个右侧节点指针

给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:

struct Node {
  int val;
  Node *left;
  Node *right;
  Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。

初始状态下,所有 next 指针都被设置为 NULL。
输入:root = [1,2,3,4,5,6,7]
输出:[1,#,2,3,#,4,5,6,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化的输出按层序遍历排列,同一层节点由 next 指针连接,'#' 标志着每一层的结束。
示例 2:

输入:root = []
输出:[]

image

这题的思路跟右视图一样,找到每一层的最后一个节点将其next赋值为null,其他的为下一个也就是当前节点出队后的队首。

class Solution {
public:
    Node* connect(Node* root) {
        queue<Node*> q;
        if (root == nullptr){
            return nullptr;
        }
        q.push(root);
        while(!q.empty()){
            int sz = q.size();
            for(int i = 0; i < sz; i++){
                Node* cur = q.front();
                q.pop();
                if(i == sz - 1){//每一层到头了
                    cur->next = nullptr;
                }else{
                    cur->next = q.front();
                }
                if(cur->left != nullptr){
                    q.push(cur->left);
                }
                if(cur->right != nullptr){
                    q.push(cur->right);
                }
            }
        }
        return root;
    }
};

实际上,通过117.填充每个节点的下一个右侧节点指针II我们可以得到,即使不是完美二叉树也可使用上面的代码,因为在层序遍历中,以我们的上帝视角看,其实结构还是一样的,都处于同一层。

226. 翻转二叉树

leetcode链接:力扣题目链接(opens new window)

视频链接:听说一位巨佬面Google被拒了,因为没写出翻转二叉树 | LeetCode:226.翻转二叉树 (opens new window)

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

示例 1:



输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

image

(这里是指针做交换)

有两种方法,遍历法和分解法。

二叉树解题的思维模式分两类:

1、是否可以通过遍历一遍二叉树得到答案?如果可以,用一个 traverse 函数配合外部变量来实现,这叫「遍历」的思维模式。

2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。

无论使用哪种思维模式,你都需要思考:

如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。

遍历法(代码随想录)

这道题目使用前序和后序比较方便。

回顾一下递归三部曲:

  1. 确定递归函数的参数和返回值

参数就是要传入节点的指针,不需要其他参数了,通常此时定下来主要参数,如果在写递归的逻辑中发现还需要其他参数的时候,随时补充。

返回值的话其实也不需要,但是题目中给出的要返回root节点的指针,可以直接使用题目定义好的函数,所以就函数的返回类型为TreeNode*

TreeNode* invertTree(TreeNode* root)
  1. 确定终止条件

当前节点为空的时候,就返回

if (root == NULL) return root;
  1. 确定单层递归的逻辑

前序:中左右,中就是我们要处理的节点。

因为是先前序遍历,所以先进行交换左右孩子节点,然后反转左子树,反转右子树。

swap(root->left, root->right);//中
invertTree(root->left);//左
invertTree(root->right);//右

后序:左右中,就把上面的顺序改一下就可以了:

swap(root->left, root->right);
invertTree(root->left);
invertTree(root->right);

中序:左中右,不太方便,因为有的节点会被交换两次!!!正确的代码应该这么写:

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        if (root == NULL) return root;
        invertTree(root->left);         // 左
        swap(root->left, root->right);  // 中
        invertTree(root->left);         // 注意 这里依然要遍历左孩子,因为中间节点已经翻转了
        return root;
    }
};

这里遍历两边root->left,因为原来的右子树已经被换到左子树去了。

附上我认为比较好的递归的统一框架,加一个traverse() 操作使目标函数与遍历函数分离:

class Solution {
public:
     void traverse(TreeNode* node){
        if(node == nullptr){
            return;
        }
        //交换每个node的左右节点。
        TreeNode* tmp = node->left;
        node->left = node->right;
        node->right = tmp;
//        delete tmp;
        traverse(node->left);
        traverse(node->right);
    }
    TreeNode* invertTree(TreeNode* root) {
        traverse(root);
        return root;
    }
};

分解法(labuladong)

// 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点
TreeNode* invertTree(TreeNode* root) {
    if (root == nullptr) {
        return nullptr;
    }
    // 利用函数定义,先翻转左右子树
    TreeNode* left = invertTree(root->left);
    TreeNode* right = invertTree(root->right);

    // 然后交换左右子节点
    root->left = right;
    root->right = left;

    // 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root
    return root;
}

然后思考,对于某一个二叉树节点 x 执行 invertTree(x),你能利用这个递归函数的定义做点啥?

我可以用 invertTree(x.left) 先把 x 的左子树翻转,再用 invertTree(x.right)x 的右子树翻转,最后把 x 的左右子树交换,这恰好完成了以 x 为根的整棵二叉树的翻转,即完成了 invertTree(x) 的定义。

这个方法还是挺难想的。先掌握遍历法!

101. 对称二叉树

leetcodel链接:力扣题目链接(opens new window)

视频链接:同时操作两个二叉树 | LeetCode:101. 对称二叉树 (opens new window)

image

遍历法

  1. 确定递归函数的参数和返回值

因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是左子树节点和右子树节点。

返回值自然是bool类型。

代码如下:

bool compare(TreeNode* left, TreeNode* right)

我总是纠结递归要不要另外写函数?这边直接定了,不管怎么样都额外写一个函数类似于traverse进行实现。

  1. 确定终止条件

要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。

节点为空的情况有:(注意我们比较的其实不是左孩子和右孩子,所以如下我称之为左节点右节点

  • 左节点为空,右节点不为空,不对称,return false
  • 左不为空,右为空,不对称 return false
  • 左右都为空,对称,返回true

此时已经排除掉了节点为空的情况,那么剩下的就是左右节点不为空:

  • 左右都不为空,比较节点数值,不相同就return false

此时左右节点不为空,且数值也不相同的情况我们也处理了。

代码如下:

if (left == NULL && right != NULL) return false;
else if (left != NULL && right == NULL) return false;
else if (left == NULL && right == NULL) return true;
else if (left->val != right->val) return false; // 注意这里我没有使用else

这边如果if都不进,就是左右节点都不为空且值相等的情况,因此这种情况是继续向下遍历,看左右孩子。就会进入单层递归

  1. 确定单层递归的逻辑

这里选用后序遍历(左右中),因为需要不断收集左右孩子的信息给上一个节点,故我们需要处理的“中”应该是最后处理的。实际上后序遍历就是回溯算法的一种表现。

单层递归的逻辑就是处理 左右节点都不为空,且数值相同的情况。

  • 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
  • 比较内侧是否对称,传入左节点的右孩子,右节点的左孩子。
  • 如果左右都对称就返回true ,有一侧不对称就返回false 。

代码如下:

bool outside = compare(left->left, right->right);   // 左子树:左、 右子树:右
bool inside = compare(left->right, right->left);    // 左子树:右、 右子树:左
bool isSame = outside && inside;                    // 左子树:中、 右子树:中(逻辑处理)
return isSame;

可以看出左子树左右中,右子树右左中,把这个遍历顺序也称之为“后序遍历”(尽管不是严格的后序遍历)。

最后的代码如下:

class Solution {
public:
    bool compare(TreeNode* left, TreeNode* right) {
        // 首先排除空节点的情况
        if (left == NULL && right != NULL) return false;
        else if (left != NULL && right == NULL) return false;
        else if (left == NULL && right == NULL) return true;
        // 排除了空节点,再排除数值不相同的情况
        else if (left->val != right->val) return false;

        // 此时就是:左右节点都不为空,且数值相同的情况
        // 此时才做递归,做下一层的判断
        bool outside = compare(left->left, right->right);   // 左子树:左、 右子树:右
        bool inside = compare(left->right, right->left);    // 左子树:右、 右子树:左
        bool isSame = outside && inside;                    // 左子树:中、 右子树:中 (逻辑处理)
        return isSame;

    }
    bool isSymmetric(TreeNode* root) {
        if (root == NULL) return true;
        return compare(root->left, root->right);
    }
};

总结

二叉树章节题目的框架意识很强。本质上解决方法两个:递归和迭代,递归注意遍历方式。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值