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

leetcode 102.二叉树的层序遍历

leetcode 226.翻转二叉树

leetcode 102.二叉树层序遍历

前面讲过了二叉树的递归遍历、迭代遍历、统一迭代等遍历方式,接下来就轮到了另一种遍历方式:层序遍历。层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。

我们需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。

而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。

使用队列实现二叉树广度优先遍历,动画如下:

代码实现

/**
 * 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>> levelOrder(TreeNode* root) {
        queue<TreeNode*> que;
        vector<vector<int>> result;
        if(root != NULL) que.push(root);
        while(!que.empty()){
            int size = que.size();
            vector<int> vec;
            for(int i = 0; i < size; i++){
                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;
    }
};
  • 时间复杂度O(n)

  • 空间复杂度O(n)

细节处理

  1. 一层一层的遍历,每一次for循环以上一层循环的size为终止值,然后分别得到上一层节点的左孩子和右孩子并push进队列。

  1. 要注意在进入for循环前要固定size的大小:size = que.size(),因为que.size()是不断变化的。

  1. 时间复杂度分析:每个点进队出队各一次,故渐进时间复杂度为O(n)。

空间复杂度分析:每个点进队出队各一次,故渐进时间复杂度为O(n)。

递归法实现层序遍历

/**
 * 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:
    void order(TreeNode* cur, vector<vector<int>>& result, int depth){
        if(cur == nullptr) return;
        if(result.size() == depth) result.push_back(vector<int>());
        result[depth].push_back(cur->val);
        order(cur->left, result, depth + 1);
        order(cur->right, result, depth + 1);
    }
    vector<vector<int>> levelOrder(TreeNode* root) {
        int depth = 0;
        vector<vector<int>> result;
        order(root, result, depth);
        return result;
    }
};

leetcode 226.翻转二叉树

翻转二叉树的过程如下:

可以发现想要翻转它,其实就把每一个节点的左右孩子交换一下就可以了。

遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。关键在于遍历顺序,前中后序应该选哪一种遍历顺序?这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转两次。层序遍历同样可以做到。

递归法

来看一下递归三部曲:

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

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

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

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

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

if(root == NULL) return root;

这里root是遍历的每一个节点的代称,并非单指二叉树的根节点。

  1. 确定单层递归的逻辑

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

// 这类题对要被处理的节点进行的操作是交换其左右孩子,上个博客要进行的操作是push进result中
swap(root->left, root->right);    // 中
invertTree(root->left);           // 左
invertTree(root->right);          // 右

所以基于递归的代码如下:(前序遍历

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        if(root == NULL) return root;
        swap(root->left, root->right);
        invertTree(root->left);
        invertTree(root->right);
        return root;
    }
};
  • 时间复杂度O(n)

  • 空间复杂度O(n)

注意:这里翻转二叉树交换每个节点的左右孩子交换的是指针而非数值,这样交换了指针后原节点左右孩子也跟着交换。

时间复杂度分析:n为二叉树节点的数目。我们会遍历二叉树中的每一个节点,对每个节点而言,我们在常数时间内交换其两棵子树。故时间复杂度为O(n)。

空间复杂度分析:使用的空间由递归栈的深度决定,它等于当前节点在二叉树中的高度。在平均情况下,二叉树的高度与节点个数为对数关系,即O(logn)。而在最坏情况下,树形成链状,空间复杂度为O(n)。

后序遍历代码:

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        if(root == NULL) return root;
        invertTree(root->left);            // 左
        invertTree(root->right);           // 右
        swap(root->left, root->right);     // 中
        return root;
    }
};

中序遍历

递归的中序遍历是不行的,因为使用递归的中序遍历,某些节点的左右孩子会翻转两次。而某些节点的左右孩子根本不会翻转。因为中序遍历的顺序是左中右,假设树为深度为3的满二叉树,其先遍历根节点的左子树到底(depth为3),随后检查其左右节点为NULL,于是退回到depth为2的左侧节点,交换depth为2的左右节点,找depth为2的左侧节点的右孩子,其左右节点也为NULL,于是退回到根节点,交换根节点的左右节点(即depth为2的两个节点),这时depth为2的左侧节点就移动到了右侧,此时开始寻找根节点的右子树,就相当于又对原先depth为2的左侧节点做一次翻转操作。

代码如下:

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;
    }
};

代码虽然可以,但这毕竟不是真正的递归中序遍历了。

但使用迭代方式统一写法的中序是可以的。同理使用统一迭代写法的前序和后序也可以。

代码如下:

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        stack<TreeNode*> st;
        if(root != NULL) st.push(root);
        while(!st.empty()){
            TreeNode* node = st.top();
            if(node != NULL){
                st.pop();
                if(node->right) st.push(node->right);    // 右
                st.push(node);                           // 中 
                st.push(NULL);
                if(node->left) st.push(node->left);     // 左
            }
            else{
                st.pop();
                node = st.top();
                st.pop();
                swap(node->left, node->right);    // 对应该处理的节点使用swap
            }
        }
        return root;
    }
};

迭代法

前序代码:

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        stack<TreeNode*> st;
        if(root == NULL) return root;
        st.push(root);
        while(!st.empty()){
            TreeNode* node = st.top();
            st.pop();
            swap(node->left, node->right);
            if(node->right) st.push(node->right);   // 右
            if(node->left) st.push(node->left);     // 左
        }
        return root;
    }
};

层序遍历代码:

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        queue<TreeNode*> que;
        if(root != NULL) que.push(root);
        while(!que.empty()){
            int size = que.size();
            for(int i = 0; i < size; i++){
                TreeNode* node = que.front();
                que.pop();
                swap(node->left, node->right);
                if(node->left) que.push(node->left);
                if(node->right) que.push(node->right);
            }
        }
        return root;
    }
};

leetcode 101.对称二叉树

要判断对称二叉树,其实就是比较根节点的左右子树是不是相互翻转的,翻转二叉树的定义可见上题。所以这个题要比较的是两个树,如何比较呢?

比较的是两个子树的里侧和外侧元素是否相等,这就意味着我们必须要遍历两个树,得到其全部的元素及父子关系,才能比较两个树是否互为翻转。

递归法

本题的遍历顺序也有说法,即遍历只能使用后序遍历(左右中)。因为在递归法中我们要不断收集左右孩子的信息返回给上一个节点,再依次递归,才能得到根节点的左右孩子信息,才能进行比较。这么说可能有些难以理解,只有后序遍历才可以将下一层的孩子信息返回给上一层的节点。在前面做的题目中,递归法遍历顺序里的“中”一般是对节点进行处理的过程,这个题使用后序遍历在“中”的处理过程中就将左右孩子的信息向上一层返回了。

如果使用前序遍历(中左右),一上来就对中间节点(父节点)进行处理,我们还没有对左右节点进行翻转的操作,就无法知道它的左右子树是否互为翻转树了。

递归三部曲:

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

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

bool compare(TreeNode* left, TreeNode* right)
  1. 确定终止条件

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

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

  • 左节点为空,右节点不为空,不对称,return false

  • 左不为空,右为空,不对称 return false

  • 左右都为空,对称,返回true

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

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

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

if(left == NULL && right != NULL) return false;
else if(right == NULL && left != NULL) return false;
else if(right == NULL && left == NULL) return true;
else if(right->val != left->val) return false;
else....

那么最后剩余的情况就是左右节点均不为空且值相等的情况。

  1. 确定单层递归的逻辑

首先比较树的“外侧”是否相等,即比较左节点的左孩子和右节点的右孩子

再比较树的“内侧”是否相等,即比较左节点的右孩子和右节点的左孩子

最后如果内外均相等则返回true,否则返回false

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

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

完整代码如下:

/**
 * 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:
    bool compare(TreeNode* left, TreeNode* right){
        if(left == NULL && right == NULL) return true;
        else if(left == NULL && right != NULL) return false;
        else if(left != NULL && right == NULL) return false;
        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 false;
        return compare(root->left, root->right);
    }
};
  • 时间复杂度O(n)

  • 空间复杂度O(n)

时间空间复杂度分析:与上题相同。

迭代法

这道题目我们也可以使用迭代法,但要注意,这里的迭代法可不是前中后序的迭代写法,因为本题的本质是判断两个树是否是相互翻转的,其实已经不是所谓二叉树遍历的前中后序的关系了。

使用队列

/**
 * 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:
    bool isSymmetric(TreeNode* root) {
        if(root == NULL) return false;
        queue<TreeNode*> que;
        que.push(root->left);
        que.push(root->right);
        while(!que.empty()){
            TreeNode* leftNode = que.front();
            que.pop();
            TreeNode* rightNode = que.front();
            que.pop();
            if(leftNode == NULL && rightNode == NULL)
                continue;
            if(leftNode == NULL && rightNode != NULL)
                return false;
            else if(leftNode != NULL && rightNode == NULL)
                return false;
            else if(leftNode->val != rightNode->val)
                return false;
            que.push(leftNode->left);
            que.push(rightNode->right);
            que.push(leftNode->right);
            que.push(rightNode->left);
        }
        return true;
    }
};

使用栈

/**
 * 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:
    bool isSymmetric(TreeNode* root) {
        if(root == NULL) return false;
        stack<TreeNode*> st;
        st.push(root->left);
        st.push(root->right);
        while(!st.empty()){
            TreeNode* leftNode = st.top();
            st.pop();
            TreeNode* rightNode = st.top();
            st.pop();
            if(leftNode == NULL && rightNode == NULL)
                continue;
            if(leftNode == NULL && rightNode != NULL)
                return false;
            else if(leftNode != NULL && rightNode == NULL)
                return false;
            else if(leftNode->val != rightNode->val)
                return false;
            st.push(leftNode->left);
            st.push(rightNode->right);
            st.push(leftNode->right);
            st.push(rightNode->left);
        }
        return true;
    }
};

本题使用栈或者队列也只是一个存储作用,由于是对两两节点进行比较其的左右子树,所以对同时弹出的顺序没有要求。将队列的代码原封不动的改成栈即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值