代码随想录C++ Day13 | 二叉树递归遍历,迭代遍历,统一迭代,层序遍历

二叉树递归遍历

class Solution {
public:
    // 前序遍历
    void traversal(TreeNode* cur, vector<int>& vec) {
        if (cur == NULL) return;
        vec.push_back(cur->val);    // 中
        traversal(cur->left, vec);  // 左
        traversal(cur->right, vec); // 右
    }
    // 中序遍历
    void traversal(TreeNode* cur, vector<int>& vec) {
        if (cur == NULL) return;
        traversal(cur->left, vec);  // 左
        vec.push_back(cur->val);    // 中
        traversal(cur->right, vec); // 右
    }
    后序遍历
    void traversal(TreeNode* cur, vector<int>& vec) {
        if (cur == NULL) return;
        traversal(cur->left, vec);  // 左
        traversal(cur->right, vec); // 右
        vec.push_back(cur->val);    // 中
    }

    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        traversal(root, result);
        return result;
    }
};

二叉树迭代遍历

使用栈来实现迭代遍历,栈的优势是可以记录之前访问到的元素,满足条件才弹出

前序遍历 

前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。

为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。

使用栈来记录之前访问到的节点:由于每次只能弹出一个节点,当父节点同时有左右节点时,只有左节点会被弹出,这样右节点就被保存了下来。达到最大树深之后,回退的路径上,右节点才被弹出。

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        stack<TreeNode*> st; // 每次弹出栈顶元素时,需要继续访问栈顶元素的左右孩子,因此这里要保存完整节点
        vector<int> result;
        if (root == nullptr) return result; // 根节点无效直接返回
        // 根节点先入栈
        st.push(root);
        while (!st.empty()) {
            TreeNode* node = st.top();
            st.pop();
            result.push_back(node->val); // 记录弹出元素值
            if (node->right) st.push(node->right); // 右孩子先入栈,空节点不入栈
            if (node->left) st.push(node->left); // 左孩子后入栈,先弹出,空节点不入栈
        }
        return result;
    }
};

后序遍历 

先序遍历是中左右,后序遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> result;
        if (root == nullptr) return result;
        st.push(root);
        while(!st.empty()) {
            TreeNode* node = st.top();
            st.pop();
            result.push_back(node->val);
            if (node->left) st.push(node->left); // 修改下入栈顺序
            if (node->right) st.push(node->right); 
        }
        reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了
        return result;
    }
};

中序遍历

为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作:

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

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

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

那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。

动画如下:

/**
 * 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<int> inorderTraversal(TreeNode* root) {
        stack<TreeNode*> st; // 栈用来存储之前访问过的节点
        vector<int> result; // 记录中序遍历的节点值
        TreeNode* cur = root;      // 用来控制节点的入栈和出栈
        while(cur != nullptr || !st.empty()) { // cur != nullptr对应cur=root, 分析可知,cur=root出现两次,最开始一次,遍历中一次,此时st都是空的,但循环仍要继续
            if (cur != nullptr) {
                st.push(cur);     // 访问到的节点入栈
                cur = cur->left;  // 如果当前节点有左节点就一直访问到最底层
            } else {
                cur = st.top(); // 指针为空,说明访问到了叶子节点的下一层,栈弹出的是左叶子节点
                st.pop();
                result.push_back(cur->val); // 记录弹出的节点值
                cur = cur->right; // 左叶子节点没有右节点,下一轮循环将父节点弹出,
                // 再下一轮指针就指向了右叶子节点,将右叶子节点弹出
                // 可以看到最左下角的遍历顺序就是左中右
                // 左下角遍历完了之后,指针为空,再弹出的就是父父节点了,然后访问右孩子
                // 右孩子看作一个小二叉树,访问完之后,再弹出就是父父父节点了
            }
        }
        return result;
    }
};

二叉树统一迭代法 

迭代法实现的先中后序,其实风格也不是那么统一,除了先序和后序,有关联,中序完全就是另一个风格了,一会用栈遍历,一会又用指针来遍历。
其实针对三种遍历方式,使用迭代法是可以写出统一风格的代码!

以中序遍历为例,上一种迭代法中说使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。
那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。

/**
 * 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<int> inorderTraversal(TreeNode* root) {
        stack<TreeNode*>st; // 使用栈记录访问过的所有节点,在中间节点后加入nullptr作为标记
        vector<int> result;
        if (root != nullptr) st.push(root);

        while(!st.empty()) {
            // 每次迭代取出栈顶元素
            TreeNode* node = st.top();
            // 栈顶不空,先将栈顶元素弹出,再将栈顶元素的右、自身、null、右节点入栈
            // 当然栈顶元素也可能没有右节点或左节点,有哪个添加哪个
            // 由于左节点后入栈,每次先弹出的就是左节点
            // 下一轮对左节点继续上述操作,而右节点只有等到左节点,中节点都处理完了才能重复上述操作
            // 试试看,如果栈顶元素是左叶子节点,它没有左右节点,下边的操作就是节点重新入栈加nullptr
            // 下一轮循环时,进入else块,空节点和这个节点就被弹出了
            // 再下一轮,栈顶元素nullptr, 次一个是中间节点,下一轮进入else两个同时被弹出
            // 再下一轮,栈顶元素是右叶子节点,没有左右节点,重新入栈和nullptr入栈,下一轮进入else两个同时被弹出
            // 再下一轮,栈顶元素就是父父节点后的nullptr了,下一轮进入else两个同时被弹出
            // 再下一轮,栈顶元素是父父节点的右节点
            // 将该右节点开始的树当作一个子树,对这个子树的遍历就是上面过程的重复
            if (node != nullptr) {
                st.pop(); // 先将栈顶元素弹出,再根据右中左顺序将右孩子,父节点,左孩子入栈
                if (node->right) st.push(node->right); // 添加右节点(空节点不入栈)
                st.push(node); // 中间节点入栈
                st.push(nullptr); // 在中间节点后添加nullptr标记
                if (node->left) st.push(node->left); // 添加左节点(空节点不入栈)
            } else { // 栈顶元素为nullptr, 说明当前节点的左节点已处理,接下来就处理自身
                st.pop(); //        // 空节点弹出
                node = st.top(); // 取出栈顶元素
                st.pop();        // 这个元素要被处理了,所以这里也要弹出
                result.push_back(node->val);
            }
        }
        return result;
    }
};

对于前序遍历,入栈的顺序变成了右,左,中,nullptr(nullptr的位置为什么总是在“中”后面,因为“中”是我们要处理的对象,遍历的分类也是根据中间节点被遍历到的顺序)

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> result;

        if (root != nullptr) st.push(root); // 当前节点先入栈,在下面的代码中要再次弹出,并根据右,左,中,nullptr的顺序再次入栈
        while (!st.empty()) {
            TreeNode* node = st.top();
            if (node != nullptr) {
                st.pop();
                if (node->right) st.push(node->right); // 右
                if (node->left) st.push(node->left);   // 左
                st.push(node);                         // 中
                st.push(nullptr);
            } else {
                st.pop();
                node = st.top();
                st.pop();
                result.push_back(node->val);
            }
        }
        return  result;
    }
};

对于后序遍历,入栈的顺序变成了中、nullptr、右、左

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*> st;
        vector<int> result;

        if (root != nullptr) st.push(root);
        while(!st.empty()) {
            TreeNode* node = st.top();
            if (node != nullptr) {
                // st.pop();
                // st.push(node);
                st.push(nullptr); // 中
                if (node->right) st.push(node->right); // 右
                if (node->left) st.push(node->left);   // 左
            } else {
                st.pop();
                node = st.top();
                st.pop();
                result.push_back(node->val);
            }
        }
        return result;
    }
};

 二叉树的层序遍历

102.二叉树的层序遍历

层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。
需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。
使用队列实现二叉树广度优先遍历,动画如下:

 

 层序遍历的思想很简单,就是每一层的元素,从左往右依次出队,同时呢,一个元素出队时,将他下层的左右节点入队,这样下层的节点也都是按照从左往右的顺序入队的

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> que;
        vector<vector<int>> result;
        if (root != nullptr) que.push(root);
        while (!que.empty()) {
            // 每次循环,开始时que保存的总是当前层的元素
            int size = que.size();
            vector<int> vec;
            // 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
            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;
    }
};

也可以用递归法实现,采用类似前序递归遍历的思路,只是需要有个depth变量记录每次递归到的层数,然后按照层数,将对应的结果记录到相应层数的数组里。但这种方法耗时且耗空间,并且递归法只能从上到小从左往右层序遍历,试试中序或后序递归遍历,顺序就乱了,不推荐

class Solution {
public:
    vector<vector<int>> order(TreeNode* cur, vector<vector<int>>& result, int depth) {
        if (cur == nullptr) return result;
        if (depth == result.size()) result.push_back(vector<int>()); // 如果递归到了result中没记录到的层,先开辟记录这层结果的空间
        result[depth].push_back(cur->val);    // 中
        order(cur->left, result, depth + 1);  // 左
        order(cur->right, result, depth + 1); // 右
        return result;
    }
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> result;
        int depth = 0; // depth从0计数,便于和result.size()比较
        order(root, result, depth);
        return result;
    }
};

107.二叉树的层序遍历II

相对于102.只需要将最后的result数组翻转一下就行 

class Solution {
public:
    vector<vector<int>> levelOrderBottom(TreeNode* root) {
        queue<TreeNode*> que;
        vector<vector<int>> result;
        if (root != nullptr) 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);
        }
        reverse(result.begin(), result.end()); // 只需将最后的result数组翻转一下
        return result;
    }
};

 199.二叉树的右视图

层序遍历的时候,判断是否遍历到单层的最后面的元素,如果是,就放进result数组中,随后返回result就可以了。 

class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        queue<TreeNode*> que;
        vector<int> result;
        if (root != nullptr) que.push(root);

        while(!que.empty()) {
            int size = que.size();
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                // 只有遍历到当前层的最后一个元素才记录
                if (i == size - 1) result.push_back(node->val);
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
        }
        return result;
    }
};

637.二叉树的层平均值

本题就是层序遍历的时候把一层求个总和再取一个均值。 

class Solution {
public:
    vector<double> averageOfLevels(TreeNode* root) {
        queue<TreeNode*> que;
        vector<double> result;

        if (root != nullptr) que.push(root);
        while(!que.empty()) {
            int size = que.size();
            double sum = 0; // 用于统计每层的数值和
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                sum += node->val;
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
            // 一层遍历完成,求平均值并记录在结果数组中
            result.push_back(sum / size);
        }
        return result;
    }
};

429.N叉树的层序遍历

/*
// Definition for a Node.
class Node {
public:
    int val;
    vector<Node*> children;

    Node() {}

    Node(int _val) {
        val = _val;
    }

    Node(int _val, vector<Node*> _children) {
        val = _val;
        children = _children;
    }
};
*/

class Solution {
public:
    // 2叉数还是N叉数,使用队列来进行层序遍历时,原理都是一样的
    // 遍历当前层时,按从左往右的顺序将下一层元素加入队尾
    vector<vector<int>> levelOrder(Node* root) {
        queue<Node*> que;
        vector<vector<int>> result;

        if (root != nullptr) que.push(root);
        while(!que.empty()) {
            int size = que.size();
            vector<int> vec;
            for (int i = 0; i < size; i++) {
                Node* node = que.front();
                que.pop();
                vec.push_back(node->val);
                // 将当前节点的所有子结点加入队尾
                for (Node* ch: node->children) {
                    if (ch) que.push(ch);
                }
            }
            result.push_back(vec);
       }
       return result;
    }
};

 515.在每个树行中找最大值

层序遍历,取每一层的最大值 

class Solution {
public:
    vector<int> largestValues(TreeNode* root) {
        queue<TreeNode*> que;
        vector<int> result;

        if (root != nullptr) que.push(root);
        while(!que.empty()) {
            int size = que.size();
            int max_val = INT_MIN; // 记录每层的最大值
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                max_val = node->val > max_val? node->val: max_val; // 当前元素大于之前记录的最大值就更新最大值
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
            result.push_back(max_val);
       }
       return result;
    }
};

 116.填充每个节点的下一个右侧节点指针

这里的完美二叉树,应该指的是满二叉树。本题依然是层序遍历,只不过在单层遍历的时候使用两个指针,一个记录当前节点,一个记录当前节点的前一个节点,然后在遍历的时候让前一个节点的next指向本节点就可以了,注意第一个节点没有前一个节点

/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* left;
    Node* right;
    Node* next;

    Node() : val(0), left(NULL), right(NULL), next(NULL) {}

    Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}

    Node(int _val, Node* _left, Node* _right, Node* _next)
        : val(_val), left(_left), right(_right), next(_next) {}
};
*/

class Solution {
public:
    Node* connect(Node* root) {
        queue<Node*> que;
        if (root != nullptr) que.push(root);
        while (!que.empty()) {
            int size = que.size();

            Node* node; // 记录当前节点
            Node* nodePre = nullptr; // 记录当前节点的前一个节点,初始化为nullptr, 因为第一个节点不存在前一个节点
            for (int i = 0; i < size; i++) {
                node = que.front(); // 取出当前节点
                que.pop();
                if (nodePre != nullptr) nodePre->next = node; // 除第一个节点外,将当前节点的前一个节点指向当前节点
                nodePre = node; // 更新
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
            nodePre->next = nullptr; // 本层最后一个节点指向NULL
        }
        return root;
    }
};

117.填充每个节点的下一个右侧节点指针II 

 层序遍历的解法,与二叉树是否是满二叉树无关,因此和116的代码是一样的

104.二叉树的最大深度

使用迭代法的话,使用层序遍历是最为合适的,因为最大的深度就是二叉树的层数,和层序遍历的方式极其吻合。
在二叉树中,一层一层的来遍历二叉树,记录一下遍历的层数就是二叉树的深度 

class Solution {
public:
    int maxDepth(TreeNode* root) {
        if (root == nullptr) return 0;
        int depth = 0;
        queue<TreeNode*> que;
        que.push(root);
        while(!que.empty()) {
            int size = que.size();
            depth++; // 记录深度
            for (int i = 0; i < size; i++) {
            TreeNode* node = que.front();
            que.pop();
            if (node->left) que.push(node->left);
            if (node->right) que.push(node->right);
            }
        }
        return depth;
    }
};

111.二叉树的最小深度

class Solution {
public:
    int minDepth(TreeNode* root) {
        if (root == nullptr) return 0;
        int depth = 0;
        queue<TreeNode*> que;
        que.push(root);
        while(!que.empty()) {
            int size = que.size();
            depth++; // 记录深度
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
                // 如果左右子节点都为空,说明遍历到了叶子节点,直接返回
                if (!node->left && !node->right) return depth;
            }
        }
        return depth;
    }
};

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值