二叉树的三种遍历方式:前序遍历、中序遍历和后序遍历

二叉树的三种遍历方式:前序遍历、中序遍历和后序遍历

参考资料:

  • 二叉树、前序遍历、中序遍历、后序遍历 - 蓝海人 - 博客园 (cnblogs.com)
  • 二叉树 - LeetBook - 力扣(LeetCode)全球极客挚爱的技术成长平台 (leetcode-cn.com)
  • 代码随想录 (programmercarl.com)

二叉树的遍历是指不重复地访问二叉树中所有的节点,主要指非空二叉树。二叉树的遍历方式主要包括前序遍历、中序遍历和后序遍历。

// 二叉树的定义
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) {}
};

文章目录

  • 二叉树的三种遍历方式:前序遍历、中序遍历和后序遍历
    • 1.三种遍历的递归版本
    • 2.三种遍历的迭代版本

1.三种遍历的递归版本

  • 前序遍历:首先访问根节点,然后遍历左子树,最后遍历后子树 ( r o o t → l e f t → r i g h t ) (root \rightarrow left \rightarrow right) (rootleftright)

    // 递归形式
    class Solution {
    public:
        vector<int> preorderTraversal(TreeNode* root) {
            vector<int> nodes;
            preorder(root, nodes);
            return nodes;
        }
    private:
        void preorder(TreeNode* root, vector<int>& nodes) {
            if (root == nullptr)
                return;
            
            nodes.push_back(root->val); // 插入val
            preorder(root->left, nodes);
            preorder(root->right, nodes); 
        }
    };
    
  • 中序遍历:首先访问左子树,然后访问根节点,最后遍历右子树 ( l e f t → r o o t → r i g h t ) (left\rightarrow root\rightarrow right) (leftrootright)

    // 递归形式
    class Solution {
    public:
        vector<int> inorderTraversal(TreeNode* root) {
            vector<int> nodes;
            inorder(root, nodes);
            return nodes;
        }
    private:
        void inorder(TreeNode* root, vector<int>& nodes) {
            if (root == nullptr)
                return;
            
            inorder(root->left, nodes);
            nodes.push_back(root->val);  // 插入val
            inorder(root->right, nodes);
        }
    };
    
  • 后序遍历:首先遍历左子树,然后遍历右子树,最后访问根节点 ( l e f t → r i g h t → r o o t ) (left\rightarrow right\rightarrow root) (leftrightroot)

    // 递归形式
    class Solution {
    public:
        vector<int> postorderTraversal(TreeNode* root) {
            vector<int> nodes;
            postorder(root, nodes);
            return nodes;
        }
    private:
        void postorder(TreeNode* root, vector<int>& nodes) {
            if (root == nullptr)
                return;
            
            postorder(root->left, nodes);
            postorder(root->right, nodes);
            nodes.push_back(root->val);  // 插入val
        }
    };
    

可以发现,三种遍历类型的递归形式的遍历顺序其实是一样的(DFS),区别仅在于何时插入节点值。 三种遍历方式的迭代形式的时间和空间复杂度均相同。空间复杂度主要是递归过程中栈的开销,即递归次数,也就是二叉树的深度。 最好情况下为 O ( log ⁡ n ) O(\log{n}) O(logn),最坏情况下为 O ( n ) O(n) O(n)

  • 时间复杂度: O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n)

当然,这三种遍历方式除了递归形式外,还有存在迭代形式的解法。在递归形式中,栈的功能由操作系统担任;但对于非递归形式,则需要我们手动设置一个栈。

2.三种遍历的迭代版本

  • 前序遍历

    每一次循环从栈中得到栈顶节点,然后找到该栈顶节点的左右子节点。按照先放右子节点,再放左子节点的顺序入栈。若子节点不存在,则不放入。

    class Solution {
    public:
        vector<int> preorderTraversal(TreeNode* root) {
            vector<int> res;
            stack<TreeNode*> stk;
    
            if (root == nullptr)
                return res;
    
            stk.push(root);
    
            while (!stk.empty()) {
                TreeNode* node = stk.top();
                res.push_back(node->val);
                stk.pop();
    
                // 先右再左入栈
                if (node->right)    
                    stk.push(node->right);
                if (node->left)    
                    stk.push(node->left);
            }
    
            return res;
        }
    };
    
  • 后序遍历

    后序遍历的迭代版本只需要对前序遍历代码稍作修改即可。已知前序遍历的顺序为中左右,而后序遍历的顺序为左右中。因此,只需要将前序遍历的顺序改为中右左,再将结果做一个翻转即可。

    class Solution {
    public:
        vector<int> postorderTraversal(TreeNode* root) {
            vector<int> res;
            stack<TreeNode*> stk;
    
            if (root == nullptr)
                return res;
            
            stk.push(root);
    
            while (!stk.empty()) {
                TreeNode* node = stk.top();
                res.push_back(node->val);
                stk.pop();
    
                // 先左再右入栈
                if (node->left)
                    stk.push(node->left);
                if (node->right) 
                    stk.push(node->right);
            }
    
            reverse(res.begin(), res.end());
    
            return res;
        }
    };
    
  • 中序遍历

    中序遍历的非递归版本的关键点在于栈的控制以及下一个节点的确定。

    • 栈用于储存当前的节点路径;

        1
       / \
      4   5 
         /
        3
      

      比如,对于以上二叉树,若当前节点为"3",则此时栈中的数据应该为

      ["1", "5", "3"]
      
    • 如何确定下一个节点

      1. 若当前节点的右节点存在,则通过while循环依次找到右节点的所有左节点,并用栈记录经过的所有节点;

        while (curNode) {
            stack.push(curNode);
            curNode = curNode->left;
        }
        
      2. 若当前节点的右节点不存在,则在栈中删除当前节点以及所有存在右子树关系的节点。

        node = stack.top();
        stack.pop();
        while (!stack.empty() && stack.top()->right == node) {
            node = stack.top();
            stack.pop();
        }
        
    class Solution {
    public:
        vector<int> inorderTraversal(TreeNode* root) {
            stack<TreeNode*> stk;
            vector<int> res;
            
            // 1.找到最左边的节点
            findLeftestNode(root, stk);
    
            // 2.开始中序遍历
            while (!stk.empty()) {
                TreeNode* curNode = stk.top();
                res.push_back(curNode->val);
                
                // 若当前节点的右节点存在,则进入该右节点分支,找到该分支的最左边的节点
                if (curNode->right) {
                    findLeftestNode(curNode->right, stk);
                }
                // 若当前节点的右节点不存在
                else {
                    // curNode = stk.top();
                    stk.pop();
                    // 若存在右子树关节的节点,则删除
                    while (!stk.empty() && stk.top()->right == curNode) {
                        curNode = stk.top();
                        stk.pop();
                    }
                }
            }
    
            return res;
        }
        
        void findMostLeft(TreeNode* root, stack<TreeNode*>& stk) {
            while (root) {
                stk.push(root);
                root = root->left;
            }
        }
    };
    

    将以上代码进行简化,可以得到以下代码。

    • 改变了栈的功能,仍然是用于储存当前的节点路径,但不会再储存之前已被储存过的节点。

          1
         / \
        4   5 
       / \
      2   3
      

      例如以上二叉树,最开始先得到最左侧节点"2",此时栈中数据为["1","4","2"]。然后pop掉"2"后,栈同样也会pop掉"4"。若是以上未简化的代码,则会保留"4",在后面判断当右节点不存在时pop

    class Solution {
    private:
        void findMostLeft(TreeNode* root, stack<TreeNode*>& stk) {
            while (root) {
                stk.push(root);
                root = root->left;
            }
        }
    public:
        vector<int> inorderTraversal(TreeNode * root) {
            vector<int> nodes;
            if (root == nullptr)
                return nodes;
            
            stack<TreeNode*> stk;
            
            // 1.找到最左边的节点
            findMostLeft(root, stk);
    
            // 2.开始中序遍历
            TreeNode* curNode;
            while (!stk.empty()) {
                // 取出一个节点,然后就丢掉
                curNode = stk.top();
                stk.pop();
                nodes.push_back(curNode->val);
                if (curNode->right != nullptr) {
                    findMostLeft(curNode->right, stk);
                }
            }
    
            return nodes;
        }
    };
    

除了这三种常用的二叉树遍历方式外,还有一种遍历方式,被称为层序遍历(level traversal)。 这个就留到之后再学吧。

你可能感兴趣的:(数据结构与算法,leetcode,深度优先,算法)