【C++】二叉树4种遍历方式的递归和迭代实现

前序遍历:根左右

递归

class Solution {
    vector<int> res;
public:
    vector<int> preorderTraversal(TreeNode* root) {
        if (!root) return {};
        // ------ 前序操作
        res.push_back(root->val);
        // ------
        preorderTraversal(root->left); // 左
        preorderTraversal(root->right); // 右
        return res;
    }
};

迭代1:仅限前序遍历

这种迭代方式和递归的思路和代码结构完全相同,但是也仅仅只限于前序遍历能这么做。其原因在前序遍历首先输出的是根节点的值,不需要考虑各种栈中暂存的问题。注意,因为栈先进后出的特点,需要先加入右节点,后加入左节点

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        stack<TreeNode*> s;
        vector<int> res;
        if (root) s.push(root);
        while (!s.empty()) {
            TreeNode* cur = s.top(); s.pop();
            // ------ 前序操作
            res.push_back(root->val);
            // ------
            if (cur->right) s.push(cur->right); // 右
            if (cur->left) s.push(cur->left); // 左
        }
        return res;
    }
};

迭代2:仅限前序和中序遍历

为了解释清楚为什么中序遍历不能使用前序遍历里的迭代1的形式,这里首先说明一下在迭代的过程中,其实我们有两个操作:

  1. 处理:将元素放进result数组中;
  2. 访问:遍历节点。

为什么迭代1不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点

那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的

那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。栈中储存的节点的特点为:已遍历过左节点,但还没遍历过右节点。

前序遍历和中序遍历都可以使用这种方法解决。区别是,前序遍历在入栈时输出到结果,而中序遍历是在出栈时输出到结果。后序遍历需要在左右节点均已输出后,该节点才能输出,没有上述出栈入栈先后顺序这一规律,所以得使用其他思路。

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        stack<TreeNode*> s;
        vector<int> res;
        TreeNode* cur = root;
        while (!s.empty() || cur) {
            while (cur) { // 往左走到底
                s.push(cur);
                // ------ 前序操作
                res.push_back(cur->val);
                // ------
                cur = cur->left; // 左
            }
            cur = s.top(); s.pop();
            cur = cur->right; // 右
        }
        return res;
    }
};

迭代3:前序、中序和后序通用

在中序和后序遍历中,都存在需要需要暂存节点的各种问题。我们需要使用一个“标志”来对“处理”和“访问”操作进行区分。以前序遍历为例,我们取出栈顶元素并出栈,当栈推出的节点不为NULL时,按遍历顺序存入其左右节点后,再将该节点存入栈中,并加入NULL节点作为输出的标志。当栈推出的节点为NULL时,将当前的栈顶节点加入结果中。前序可以使用这样的模版,虽然没什么必要,pop出来再push进去再pop出来像是在耍杂技。

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> s;
        if (root) s.push(root);
        while (!s.empty()) {
            TreeNode* cur = s.top(); s.pop();
            if (cur) { // 遍历阶段
                if (cur->right) s.push(cur->right); // 右
                if (cur->left) s.push(cur->left); // 左
                s.push(cur); // 根
                s.push(NULL); // 输出标志
            } else { // 输出阶段
                cur = s.top(); s.pop();
                // ------ 前序操作
                res.push_back(cur->val);
                // ------
            }
        }
        return res;
    }
};

中序遍历:左根右

递归

class Solution {
    vector<int> res;
public:
    vector<int> inorderTraversal(TreeNode* root) {
        if (!root) return {};
        inorderTraversal(root->left); // 左
        // ------ 中序操作
        res.push_back(root->val);
        // ------
        inorderTraversal(root->right); // 右
        return res;
    }
};

迭代1:仅限前序和中序遍历

详细讨论见前序遍历迭代2算法。

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> s;
        TreeNode* cur = root;
        while (!s.empty() || cur) {
            while (cur) { // 往左走到底
                s.push(cur); // 根
                cur = cur->left; // 左
            }
            cur = s.top(); s.pop();
            // ------ 中序操作
            res.push_back(cur->val);
            // ------
            cur = cur->right; // 右
        }
        return res;
    }
};

迭代2:前序、中序和后序通用

在中序和后序遍历中,都存在需要需要暂存节点的各种问题。我们需要使用一个“标志”来对“处理”和“访问”操作进行区分。以中序遍历为例,我们取出栈顶元素并出栈,当栈推出的节点不为NULL时,先存入右节点,再将该节点存入栈中,并加入NULL节点作为输出的标志,最后存入左节点。当栈推出的节点为NULL时,将当前的栈顶节点加入结果中。

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> s;
        if (root) s.push(root);
        while (!s.empty()) {
            TreeNode* cur = s.top(); s.pop();
            if (cur) { // 遍历阶段
                if (cur->right) s.push(cur->right); // 右
                s.push(cur); // 根
                s.push(NULL); // 输出标志
                if (cur->left) s.push(cur->left); // 左
            } else { // 输出阶段
                cur = s.top(); s.pop();
                // ------ 中序操作
                res.push_back(cur->val);
                // ------
            }
        }
        return res;
    }
};

后序遍历:左右根

递归

class Solution {
    vector<int> res;
public:
    vector<int> postorderTraversal(TreeNode* root) {
        if (!root) return {};
        postorderTraversal(root->left); // 左
        postorderTraversal(root->right); // 右
        // ------ 后序操作
        res.push_back(root->val);
        // ------ 
        return res;
    }
};

迭代1:前序遍历改

前序遍历是“根左右”,后序遍历是“左右根”。那么我们可以将前序遍历代码改为“根右左”输出后,再将最终输出结果反转,即得到“左右根”的输出。这个方法有点取巧,不推荐。

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> s;
        if (root) s.push(root);
        while (!s.empty()) {
            TreeNode* cur = s.top(); s.pop();
            // ------ 后序操作
            res.push_back(cur->val);
            // ------
            if (cur->left) s.push(cur->left); // 左
            if (cur->right) s.push(cur->right); // 右
        }
        reverse(res.begin(), res.end()); // 反转结果
        return res;
    }
};

迭代2:仅后序遍历

由于后序遍历具有根节点最后输出的特点,所以我们可以以根节点为基点,判断其左右节点是否为空或已输出,以此来决定是否输出根节点。使用lastVisit记录上次输出节点,避免左右节点重复入栈。这种方法还能够保证栈容量等于当前节点深度。

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> s;
        TreeNode* lastVisit;
        if (root) s.push(root);
        while (!s.empty()) {
            TreeNode* cur = s.top();
            if (cur->left && cur->left != lastVisit && cur->right != lastVisit)
                s.push(cur->left); // 左节点存在,且左右节点都没经过时,添加左节点
            else if (cur->right && cur->right != lastVisit)
                s.push(cur->right); // 右节点存在,且没经过右节点时,添加右节点
            else { // 否则输出该节点
                lastVisit = cur;
                s.pop();
                // ------ 后序操作
                res.push_back(cur->val);
                // ------
            }
        }
        return res;
    }
};

迭代3:前序、中序和后序通用

在中序和后序遍历中,都存在需要需要暂存节点的各种问题。我们需要使用一个“标志”来对“处理”和“访问”操作进行区分。以后序遍历为例,我们取出栈顶元素并出栈,当栈推出的节点不为NULL时,先将该节点入栈并添加NULL节点作为输出标志,再存入右节点和左节点。当栈推出的节点为NULL时,将当前的栈顶节点加入结果中。

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> s;
        if (root) s.push(root);
        while (!s.empty()) {
            TreeNode* cur = s.top(); s.pop();
            if (cur) { // 遍历阶段
                s.push(cur); // 根
                s.push(NULL); // 输出标志
                if (cur->right) s.push(cur->right); //右
                if (cur->left) s.push(cur->left); // 左
            } else { // 输出阶段
                cur = s.top(); s.pop();
                // ------ 后序操作
                res.push_back(cur->val);
                // ------
            }
        }
        return res;
    }
};

层序遍历

递归

层序遍历的递归算法有点特殊。思路是递归时传递当前节点所在的层数,然后将当前节点的值传入二维数组对应层的数组中,最后再将这个二维数组展开为一位数组。

class Solution {
public:
    vector<int> levelOrder(TreeNode* root) {
        vector<int> res;
        recur(root, 0);
        for (auto& i : ans) // 二维数组展开为一位数组
            res.insert(res.end(), i.begin(), i.end());
        return res;
    }
private:
    vector<vector<int>> ans;
    void recur(TreeNode* root, int level) {
        if (!root) return;
        if (ans.size() <= level) ans.push_back({}); // 拓展二维数组
        ans[level].push_back(root->val);
        recur(root->left, level + 1); // 递归下一层
        recur(root->right, level + 1);
    }
};

迭代

广度优先遍历非常适合树的层序遍历。代码结构和前序遍历中的迭代1算法完全一致,区别仅在于前序遍历使用的栈结构的DFS,层序遍历使用队列结构的BFS。

class Solution {
public:
    vector<int> levelOrder(TreeNode* root) {
        vector<int> res;
        queue<TreeNode*> q;
        if (root) q.push(root);
        while (!q.empty()) {
            TreeNode* cur = q.front(); q.pop();
            res.push_back(cur->val);
            if (cur->left) q.push(cur->left);
            if (cur->right) q.push(cur->right);
        }
        return res;
    }
};
posted @ 2021-02-22 16:41  tmpUser  阅读(285)  评论(0编辑  收藏  举报