[LeetCode] 树的总结(二叉树和N叉树)

写在前面

树的考察点归根结底是对树的前序、中序、后序和层序的考察,树的前、中、后序遍历的递归写法是图的深度优先搜索的子集,因为带有递归特点,所以往往可以使用将其改写成非递归写法,树的层序遍历是图的广度优先搜索的子集。

树的遍历

树的递归遍历难度非常小,一般不会直接考察,而选择考察它的非递归写法,之前专门为二叉树和N叉树的遍历写过总结博客。由于此知识点过于重要,而且在面试手撕代码中是高频代码,因此重新再整理一次。

LeetCode中相关的题如下:

  • 94. 二叉树的中序遍历
  • 144. 二叉树的前序遍历
  • 173. 二叉搜索树迭代器
  • 145. 二叉树的后序遍历

前序遍历

144. 二叉树的前序遍历

解题思路: 对着题解代码我们很好理解前序遍历的非递归解法,但是为什么这么写?我们有没有想过,虽然网上有很多非递归写法的模板,但是弄懂它为什么这么写会有利于我们做变体题,OK,这里我们愚笨地探讨一下1+1问题。首先我们想一下前序遍历的递归写法是怎么写的,先访问根节点,然后递归访问左子树,再递归访问右子树,既然都说递归的本质是栈,递归进入的过程是入栈,递归退出的过程是出栈,栈是先入后出的,因此若我们想在递归时先访问左子树后访问右子树,那么在入栈时应该先压栈右子树后压栈左子树。【边入栈边出栈】—>前序遍历栈不是很深。

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        if (!root) return {};
        vector<int> res;
        stack<TreeNode*> st{{root}};
        while (!st.empty()) {
            TreeNode *p = st.top(); st.pop();
            res.push_back(p->val);
            if (p->right) st.push(p->right);
            if (p->left) st.push(p->left);
        }
        return res;
    }
};

中序遍历

解题思路: 按照前序遍历中的思维引导,对于中序遍历,我们可以这么思考为什么这么写,中序遍历的递归写法是,先遍历左子树,再访问根节点,后遍历右子树,这里我们做进一步解释,遍历左子树意味着在访问根节点之前需要将整个左子树压入栈中,而在访问完根节点后,我们会将访问的对象切换到右子树根节点,但是并不访问它,而是按照左-根-右访问右子树,因此做指针调转动作后,依然压右子树的左子树入栈。【压左子树至顶,再切右子树】—>中序遍历栈非常深。

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> st;
        TreeNode *p = root;
        while (!st.empty() || p) {
            while (p) {
                st.push(p);
                p = p->left;
            }
            p = st.top(); st.pop();
            res.push_back(p->val);
            p = p->right;
        }
        return res;
    }
};

后序遍历

解题思路: 依然按照前序遍历的思维引导,后序遍历是左-右-根,我们可以得出,若根节点访问,则左或者右子树(若有)均已被访问过,那么我们在用栈模拟递归时,是否仍需要像中序将左子树全压栈?或许这个结论我们换一个更明确的表达,因为前序是根-左-右,边访问边出栈,因此严格意义上讲递归的深度并不大,对应的栈的深度也不会太大,确切的说至多为2,而对于中序和后序,因为要向左至底(i.e.,访问左左左…左子树节点不存在)时,才开始递归回退,因此栈的深度很大,OK,讨论完这个问题后,我们依然采取左、右子树依次压栈(而不需要一直压左子树),只有当访问节点为叶子节点或者访问节点的左右子树均被访问过,那么访问该节点并出栈,那么如果表示访问节点(A)的左右子树均被访问过呢?最简单粗暴的访问是,用hash记录所有被访问过的节点,然后当访问到A时,查找A的左右子节点是否被访问过即可,但是显然这并不是最佳的方案,我们分析一下,假设节点A是7,当访问3时,左左子树和左右子树两分支已经被访问过,那么接下来会访问3,而当准备访问7时,6恰好是前一个被访问,OK,我们可以得出一个结论,在考虑到6可能为NULL时,若访问7时,前一个访问的节点是3或者6,则可以认为7的左、右子树已经被访问过,OK,请看下面代码。
[LeetCode] 树的总结(二叉树和N叉树)_第1张图片

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        if (!root) return {};
        vector<int> res;
        stack<TreeNode*> st{{root}};
        unordered_set<TreeNode*> us;
        us.insert(NULL);
        TreeNode *head = root;
        while (!st.empty()) {
            TreeNode *p = st.top(); 
            if ((!p->left && !p->right) || (head == p->left || head == p->right)) {
                res.push_back(p->val);
                st.pop();
                us.insert(p);
                head = p;
            } else {
                if (p->right) st.push(p->right);
                if (p->left) st.push(p->left);
            }
        }
        return res;
    }
};

——————————————

树的遍历统一解题模板

我们先列出代码,然后观察代码分析特征,然后再从理论分析角度给出合理性解释。观察下面代码,会发现解题形式非常统一,均引入辅助指针节点,在压栈顺序上也是先压左子树后压右子树(当然有时候有说法是,根据栈先入后出特征,需要先压右子树再压左子树,这样能保证出栈时先左后右,具体怎么个压法,我的建议是画图,按照递归转栈的方式,画图即可弄清楚如何左右子树压栈),不同的是,获取遍历序列的位置不同,根据先序特征,遍历到节点时即获取值,根据中序遍历特征,只有再遍历完左节点时才获取值,后序可能麻烦一些,需要记录前置遍历节点,然后代码的另一个特征是,if-else结构中if访问的是节点p左子树,else中访问的节点p右子树。当然后序遍历也可以将其转化为先序遍历解,这篇博客中就讲到这个解法。

1)先序

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        if (!root) return {};
        stack<TreeNode*> st;
        TreeNode *p = root;
        vector<int> res;
        while (!st.empty() || p) {
            if (p) {
                st.push(p);
                res.push_back(p->val);
                p = p->left;
            } else {
                p = st.top(); st.pop();
                p = p->right;
            }
        }
        return res;
    }
};

2)中序

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        if (!root) return {};
        vector<int> res;
        stack<TreeNode*> st;
        TreeNode *p = root;
        while (!st.empty() || p) {
            if (p) {
                st.push(p);
                p = p->left;
            } else {
                p = st.top(); st.pop();
                res.push_back(p->val);
                p = p->right;
            }
        }
        return res;
    }
};

3)后序

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        if (!root) return {};
        vector<int> res;
        stack<TreeNode*> st;
        TreeNode *p = root;
        TreeNode *prev = root;
        while (!st.empty() || p) {
            if (p) {
                st.push(p);
                p = p->left;
            } else {
                p = st.top();
                if (!p->right || p->right == prev) {
                    res.push_back(p->val);
                    st.pop();
                    prev = p;
                    p = NULL;
                } else {
                    p = p->right;
                }  
            }
        }
        return res;
    }
};

——————————————

  • 树的遍历(先、中、后、层序遍历)时间复杂度和空间复杂度均为O(n)

——————————————

173. 二叉搜索树迭代器

原题链接

解题思路: 从题意不难得出要求中序遍历序列,偷懒的方法是直接用递归方法将中序求出来存在一个数组里,然后每次从数组中取元素,不过一般面试的时候面试官不会轻易让你写这样的代码,会附加条件,数据不能一次加载到内存中,i.e.,不能将数据存在数组中再遍历,因此本质上此题是在考察中序遍历的非递归写法,因此将中序非递归写法稍加修改即可解此题。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class BSTIterator {
public:
    BSTIterator(TreeNode* root) {
        p = root;
    }
    
    /** @return the next smallest number */
    int next() {
        int res;
        while (p) {
            st.push(p);
            p = p->left;
        }
        p = st.top(); st.pop();
        res = p->val;
        p = p->right;
        return res;
    }
    
    /** @return whether we have a next smallest number */
    bool hasNext() {
        return (!st.empty() || p);
    }
private:
    stack<TreeNode*> st;
    TreeNode *p;
};

/**
 * Your BSTIterator object will be instantiated and called as such:
 * BSTIterator* obj = new BSTIterator(root);
 * int param_1 = obj->next();
 * bool param_2 = obj->hasNext();
 */

你可能感兴趣的:(栈,leetcode,树)