深入理解中序遍历二叉树

起因

邓俊辉的中序遍历二叉树采用了和前序遍历不同的循环检查方式,在前序遍历时使用stack非空检查,而在中序遍历时采用了while1检查。不便于迁移学习。

分析

视频地址:
https://www.bilibili.com/video/BV1jt4y117KR?p=173
中序遍历时,根据规律每次进入一颗子树也需要先遍历左侧分支。由于左侧分支的发现顺序和访问顺序是逆序的,因此会想到借助stack来实现。
当进入一颗子树时,先遍历左侧分支发现节点并push stack,左侧遍历到空节点时,访问栈top节点,(左侧没有节点了,该访问当前根)。左子树为空和左子树访问完都应该访问当前根节点。
之后,如果有右子树则进入右子树,这里要思考的是右子树的根节点push stack还是直接赋值。
第一种方案,将右子树根节点push stack.
因此一开始可能写下这样的代码:

    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> ret;
        if (!root) return ret;
        vector<TreeNode *> stack = {root};
        while (!stack.empty()) {
            auto node = stack.back();
                while (node->left) {
                    stack.push_back(node->left);
                    node = node->left;
                }
            node = stack.back();
            ret.push_back(node->val);
            stack.pop_back();
            if (node->right) {
                stack.push_back(node->right);
            }
        }
        return ret;

但对于[1,null,2,3,null]的树形结构,在push节点2后,stack状态为[2],检查stack非空,进入以2为根的右子树,再开始遍历左侧分支。发现3时,stack为[2,3],访问3且3没有右子树,检查stack非空,又回到以2为根的子树,又开始了左侧分支遍历,循环不能终止。注意:我们这时通过stack非空回到以2为根的子树,开始遍历2的左侧分支。遍历左侧分支这个操作是第一次进入一棵子树(发现根节点)的操作,而不应该是从一个子树根节点的左子树访问完成回来时的操作。
因此,需要一个从左侧分支返回当前根节点的标志位。
此外,还应该思考的是,每次检查stack非空的含义是什么?
比如第一次进入整颗树的时候,要检查一次树的根节点。进入以2为根的子树时,也检查了stack非空。从3访问结束返回2的时候也检查了stack非空。所以,第一次进入一颗树的时候检查了stack的状态,而从左子树返回的时候也检查了stack的状态。这两种条件下,后者不需要遍历左侧分支的操作,因此还是印证了我们的想法:需要一个从左侧分支返回当前根节点的标志位
因此,改写后的代码如下:

    vector<int> inorderTraversal(TreeNode* root) {

        vector<int> ret;
        if (!root) return ret;
        vector<TreeNode *> stack = {root};
        int lefthasvisited= 0;
        while (!stack.empty()) {
            auto node = stack.back();
            if (!lefthasvisited) { // 只有是第一次进入一颗树的时候才进行左侧分支遍历
                while (node->left) {
                    stack.push_back(node->left);
                    node = node->left;
                }
            }
            node = stack.back();
            ret.push_back(node->val);
            stack.pop_back();
            lefthasvisited = 1;
            if (node->right) {
                stack.push_back(node->right);
                lefthasvisited = 0; // 发现有右子树的时候,需要将左子树访问完成的标志位重置
            }
                
            
        }
        return ret;

再比如下面这种方法,起初的思路是每次取stack top节点,判断应不应该访问。中序遍历时,从左子树返回或左子树为空时,应该访问当前根节点(第一个if的判断逻辑)。为了存储是否从左子树返回,需要每次设置return_from_left的状态,默认为false。在进入第一个if时,当前节点应被访问,同时,如果有右孩子,应把右孩子push stack以便在下一轮的iter时,可以考虑已右孩子为根的子树,同时将return_from_left设为false(因为下一轮将要访问右孩子为根的子树,还不能返回到上一层)。而如果没有右子树的话,当前这个子树已经访问完了,我们回到上一层时肯定是从左子树返回,即使真的是从右子树返回,当前这颗子树也都访问完了,因为根节点先于右子树访问,所以就不会影响当前这个子树根节点的判定。

    typedef TreeNode Node;
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> ret;
        if (!root) return ret;
        Node *node = nullptr;
        bool return_from_left = false;
        vector<Node *> stack(1, root);
        while (!stack.empty()) {
            node = stack.back();
            if (!node->left \
               || node->left && return_from_left) {
                ret.push_back(node->val);
                stack.pop_back();
                if (node->right) {
                    stack.push_back(node->right);
                    return_from_left = false;
                }
                else
                    return_from_left = true;
            } else {
                stack.push_back(node->left);
                return_from_left = false;
            }
        }
        return ret;
    }
posted @ 2021-10-25 09:20  ijpq  阅读(200)  评论(0编辑  收藏  举报