二叉树前中后序遍历非递归(迭代)解法

对应leetcode 题目94、144、145

参考https://leetcode-cn.com/problems/binary-tree-preorder-traversal/solution/cer-cha-shu-san-chong-bian-li-qian-zhong-erk2/

持续更新中,作为自己的思路与理解笔记,借鉴“官方题解”与“史上最全遍历二叉树详解”(原作者:一个GO语言全干工程师),对其中不理解的地方做出了自己的注解,感谢大佬的题解!
此外,本文很长,不要着急沉下心慢慢走,建议吃透再去下一个部分,理解思路后代码一定要自己敲一遍!!!

递归
递归是二叉树遍历实现中最简单的,二叉树天然就具有递归的属性,对每个结点的处理完全一样,二叉树可以称得上是思路最清晰的递归题目。只要简单排布“调用左结点递归函数、调用右结点递归函数、输出本结点”这三个步骤就能完成递归操作。

前序遍历
中左右:输出本结点->调用左递归->调用右递归

C++

class Solution {
public:
vector<int> ans;
vector<int> preorderTraversal(TreeNode* root) {
// 为空则直接返回
if(root == NULL)
return ans;
ans.push_back(root->val);
preorderTraversal(root->left);
preorderTraversal(root->right);
return ans;
}
};
中序遍历
左中右:调用左递归->输出本结点->调用右递归

C++

class Solution {
public:
vector<int> ans;
vector<int> preorderTraversal(TreeNode* root) {
// 为空则直接返回
if(root == NULL)
return ans;
preorderTraversal(root->left);
ans.push_back(root->val);
preorderTraversal(root->right);
return ans;
}
};
后序遍历
左右中:调用左递归->调用右递归->输出本结点

C++

class Solution {
public:
vector<int> ans;
vector<int> preorderTraversal(TreeNode* root) {
// 为空则直接返回
if(root == NULL)
return ans;
preorderTraversal(root->left);
preorderTraversal(root->right);
ans.push_back(root->val);
return ans;
}
};
迭代
你能看到这一步说明已经掌握了二叉树的递归方法。是不是觉得这玩意儿好像也就那样,哪有那么难。别急,先让我们沉下心,真正的大boss才刚开始打。
迭代本质上是在利用栈的辅助模拟递归,搞清迭代事实上就是搞清计算机到底做了什么,以及在整个二叉树中数据的走向。因此我们的思路也可以顺着递归的路线,一步一步走。

前序遍历
思路:
由于“中左右”的访问顺序正好符合根结点寻找子节点的顺序,因此每次循环时弹栈,输出此弹栈结点并将其右结点和左结点按照叙述顺序依次入栈。至于为什么要右结点先入栈,是因为栈后进先出的特性。右结点先入栈,就会后输出右结点。
初始化:
一开始让root结点先入栈,满足循环条件
步骤:

弹栈栈顶元素,同时输出此结点
当前结点的右结点入栈
当前结点的左结点入栈
重复上述过程
结束条件:
每次弹栈根结点后入栈子结点,栈为空时则说明遍历结束。
C++

class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
stack<TreeNode*> stk;
if(root != NULL)
stk.push(root);
while(!stk.empty())
{
TreeNode* cur = stk.top();
stk.pop();
ans.push_back(cur->val);
if(cur->right != NULL)
stk.push(cur->right);
if(cur->left != NULL)
stk.push(cur->left);
}
return ans;
}
};
中序遍历
思路:
中序遍历思路相较于前序遍历有很大的改变。前序遍历遇到根结点直接输出即可,但中序遍历“左中右”需先找到此根结点的左结点,因此事实上第一个被输出的结点会是整个二叉树的最左侧结点。依据这一特性,我们每遇到一个结点,首先寻找其最左侧的子结点,同时用栈记录寻找经过的路径结点,这些是输出最左侧结点之后的返回路径。之后每次向上层父结点返回,弹栈输出上层父结点的同时判断此结点是否含有右子结点,如果存在则此右结点入栈并到达新的一轮循环,对此右结点也进行上述操作。
初始化:
curr定义为将要入栈的结点,初始化为root
top定义为栈顶的弹栈结点
步骤:

寻找当前结点的最左侧结点直到curr为空(此时栈顶结点即为最左侧结点)
弹栈栈顶结点top并输出
判断top是否具有右结点,如果存在则令curr指向右结点,并在下一轮循环入栈
重复上述过程
结束条件:
这里可以看到结束条件有两个:栈为空,curr为空。这是因为中序遍历优中后右的特性,会有一个时刻栈为空但右结点并未被遍历,因此只有在curr也为空证明右结点不存在的情况下,才能结束遍历。
C++

class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
stack<TreeNode*> stk;
TreeNode* curr = root;
while(!stk.empty() || curr != NULL)
{
// 找到节点的最左侧节点,同时记录路径入栈
while(curr != NULL)
{
stk.push(curr);
curr = curr->left;
}
// top定义是此刻的弹栈元素
TreeNode* top = stk.top();
ans.push_back(top->val);
stk.pop();
// 处理过最左侧结点后,判断其是否存在右子树
if(top->right != NULL)
curr = top->right;
}
return ans;
}
};
后序遍历
法一
思路:
后序遍历思路类似中序遍历,都是从“左”开始,只不过情况的处理更加复杂。中序遍历在寻找到最左侧结点之后就可以直接弹栈,但后序遍历不可以,因为其要先输出右子结点之后才能输出根结点。而能输出根结点的条件仅在左右子树均已处理完毕或者不存在。因此依据这些特点,我们设定curr作为当前退出栈的结点,top为待处理的根结点,通过判断top与curr的关系来断定是否已经完成子结点的遍历进入到返回上层父结点的阶段。
初始化:
curr定义为当前退出栈的结点,初始化为head(这里有一个要点,不能把curr初始化为NULL,否则在第一次弹栈之前 != NULL 与 != curr 是等价的,因此初始化为head仅是为了不让其为空,并不代表head要出栈)
top定义为栈顶待处理的根节点
步骤:

判断top的左结点是否能入栈(为空或者左右子树被处理过则说明此结点已被遍历过,不能入栈)
判断top的右结点是否能入栈(为空或者右子树被处理过则不能入栈)
如果上述两条件都不符合,说明top结点不存在左右子树或者左右子树均被遍历过,因此直接弹栈并输出,同时curr指向top,存储当前退出栈的结点信息。
重复上述过程
结束条件:
这里根结点必然是最后弹栈并输出的,因此结束条件就是栈为空即可。
C++

class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> ans;
stack<TreeNode*> stk;
if(root != NULL)
stk.push(root);
// curr存储当前退出栈的结点
TreeNode* curr = root;
while(!stk.empty())
{
TreeNode* top = stk.top();
if(top->left != NULL && top->left != curr && top->right != curr)
stk.push(top->left);
else if(top->right != NULL && top->right != curr)
stk.push(top->right);
// 当左右子树都处理过或者不存在情况下,说明此结点可以弹栈
else
{
ans.push_back(top->val);
stk.pop();
curr = top;
}
}
return ans;
}
};
法二(巧妙做法)

前序遍历的过程是中左右。
将其转化成中右左。也就是压栈的过程中优先压入左子树,再压入右子树。
在弹栈的同时将此弹栈结点压入另一个栈,完成逆序。
对新栈中的元素直接顺序弹栈并输出。
C++

class Solution {
public:
vector<int> postOrderIteration(TreeNode* root) {
vector<int> ans;
if (root == NULL)
return ans;
stack<TreeNode*> stack1;
stack<TreeNode*> stack2;
stack1.push(root);
// 栈一顺序存储
while (!stack1.empty())
{
TreeNode* node = stack1.top();
stack1.pop();
stack2.push(node);
if (node->left != NULL)
stack1.push(node->left);
if (node->right != NULL)
stack1.push(node->right);
}
// 栈二直接输出
while (!stack2.empty())
{
ans.push_back(stack2.top()->val);
stack2.pop();
}
}
Morris
芜湖~你能沉下心看到这里,说明迭代法已经完全吃透了,鼓掌鼓掌!现在开始最后的冲刺!

Morris遍历利用了二叉树结点中大量指向null的指针,由Joseph Morris于1979年发明。
时间复杂度:O(n) 额外空间复杂度:O(1)
首先说明Morris的通用解法过程:

Morris的整体思路:以某个根结点开始,找到它左子树的最右侧节点之后与这个根结点进行连接。
这么连接后,cur这个指针是可以完整的从一个节点顺着下一个节点遍历,将整棵树遍历完毕,直到7这个节点右侧没有指向。

Morris的过程可以大致分成四步:根节点的左子树建立连线(由上至下)-> 左侧到头,向右子树前进(由上至下)-> 右子树到头,返回上层并断开连线(由下至上)-> 回到根节点,对右子树做同样的处理。也就是说,建立连接阶段我们是不断向左走并对这些结点左子树的最右侧结点建立连接,大方向是向左走的;返回上层阶段我们是不断向右走,并将走过的连接断开。这其中何时输出结点,作何种特殊处理进一步区分三种遍历。

C++

class Solution {
public:
void preOrderMorris(TreeNode* root) {
if (root == NULL)
return;
TreeNode* curr = root; // 当前的结点
TreeNode* currLeft = NULL; // 当前结点的左子树
while (curr != NULL)
{
currLeft = curr->left;
// 当前结点的左子树存在即可建立连接
if (currLeft != NULL)
{
// 找到当前左子树的最右侧节点,并且不能沿着连接返回上层
while (currLeft->right != NULL && currLeft->right != curr)
currLeft = currLeft->right;
//最右侧节点的右指针没有指向根结点,创建连接并往下一个左子树的根结点进行连接操作
if (currLeft->right == NULL)
{
currLeft->right = curr;
curr = curr.left;
continue; // 这个continue很关键
}
else
//当左子树的最右侧节点有指向根结点,此时说明我们已经进入到了返回上层的阶段,不再是一开始的建立连接阶段,同时在回到根结点时我们应已处理完下层节点,直接断开连接即可。
currLeft->right = NULL;
}
// 返回上层的阶段不断向右走
curr = curr.right;
}
}
}
前序遍历
思路:
Morris建立连接时是给每个根结点寻找其左子树的最右侧结点建立连接,因此“从根结点开始”这一特性很符合前序遍历“中左右”的遍历方式,因此在给结点建立连接的同时输出此根结点即可完成前序遍历。
特殊处理:

建立连接的同时输出此根结点。
到达一些没有子节点的叶子节点,直接输出并向右走返回上层或向此节点的右子树前进。
判断出某节点已有连接,则不用输出,直接断开走过的连接后继续向右走。
C++

class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
if (root == NULL)
return ans;
TreeNode* curr = root; // 当前的结点
TreeNode* currLeft = NULL; // 当前结点的左子树
while (curr != NULL)
{
currLeft = curr->left;
// 当前结点的左子树存在即可建立连接
if (currLeft != NULL)
{
// 找到当前左子树的最右侧节点,并且不能沿着连接返回上层
while (currLeft->right != NULL && currLeft->right != curr)
currLeft = currLeft->right;
//最右侧节点的右指针没有指向根结点,创建连接并往下一个左子树的根结点进行连接操作
if (currLeft->right == NULL)
{
currLeft->right = curr;
ans.push_back(curr->val);
curr = curr->left;
continue; // 这个continue很关键
}
else
// 当左子树的最右侧节点有指向根结点,此时说明我们已经进入到了返回上层的阶段,不再是一开始的建立连接阶段,同时在回到根结点时我们应已输出过下层节点,直接断开连接即可
currLeft->right = NULL;
}
else
// 当前节点的左子树为空,说明左侧到头,直接输出
ans.push_back(curr->val);
// 返回上层的阶段不断向右走
curr = curr->right;
}
return ans;
}
};
中序遍历
思路:
类似迭代,整个二叉树中输出的第一个节点是最左侧结点,因此在建立连接的时候是不能够直接输出的,必须在建立连接阶段完成,到达最左侧结点之后返回上层的阶段,才能开始输出,此时正好符合“左中右”的遍历方式。
特殊处理:

在建立连接阶段并不输出结点。
在找到最左侧结点(即根结点的左子树为空)时,开始向右走返回上层并同时输出当前结点。
对右子树也进行同样的处理。
C++

class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
if (root == NULL)
return ans;
TreeNode* curr = root; // 当前的结点
TreeNode* currLeft = NULL; // 当前结点的左子树
while (curr != NULL)
{
currLeft = curr->left;
// 当前结点的左子树存在即可建立连接
if (currLeft != NULL)
{
// 找到当前左子树的最右侧节点,并且不能沿着连接返回上层
while (currLeft->right != NULL && currLeft->right != curr)
currLeft = currLeft->right;
//最右侧节点的右指针没有指向根结点,创建连接并往下一个左子树的根结点进行连接操作
if (currLeft->right == NULL)
{
currLeft->right = curr;
curr = curr->left;
continue; // 这个continue很关键
}
else
// 当左子树的最右侧节点有指向根结点,此时说明我们已经进入到了返回上层的阶段,不再是一开始的建立连接阶段,同时在回到根结点时我们应已输出过下层节点,直接断开连接即可
currLeft->right = NULL;
}
// 当前节点的左子树为空,说明左侧到头,直接输出并返回上层
ans.push_back(curr->val);
// 返回上层的阶段不断向右走
curr = curr->right;
}
return ans;
}
};
后序遍历
思路:
后序遍历又双叒叕是最难搞的情况。举个例子:

打印顺序:打印 4 打印 5 2 打印 6 打印 7 3 1
我们将一个节点的连续右节点当成一个单链表来看待,可以发现,输出顺序是将此单链表翻转后输出。
当我们返回上层之后,也就是将连线断开的时候,输出下层的单链表。
比如返回到 2,此时打印 4
比如返回到 1,此时打印 5 2
比如返回到 3,此时打印 6
那么我们只需要将这个单链表逆序输出即可。
note:这里不应该打印当前层,而是之前的一层,否则根结点会先与右边输出。

C++

class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> ans;
if (root == NULL)
return ans;
TreeNode* curr = root; // 当前的结点
TreeNode* currLeft = NULL; // 当前结点的左子树
while (curr != NULL) {
currLeft = curr->left;
// 当前结点的左子树存在即可建立连接
if (currLeft != NULL)
{
// 找到当前左子树的最右侧节点,并且不能沿着连接返回上层
while (currLeft->right != NULL && currLeft->right != curr)
currLeft = currLeft->right;
//最右侧节点的右指针没有指向根结点,创建连接并往下一个左子树的根结点进行连接操作
if (currLeft->right == NULL)
{
currLeft->right = curr;
curr = curr->left;
continue; // 这个continue很关键
}
// 当左子树的最右侧节点有指向根结点,此时说明我们已经进入到了返回上层的阶段,断开连接同时对之前的一层进行翻转并输出
else
{
currLeft->right = NULL;
postMorrisPrint(curr->left, ans);
}

}
// 返回上层的阶段不断向右走
curr = curr->right;
}
// 最后一轮循环结束时,从root结点引申的右结点单链表并没有输出,这里补上
postMorrisPrint(root, ans);
return ans;
}
//输出函数
void postMorrisPrint(TreeNode* head, vector<int>& ans) {
TreeNode* newhead = postMorrisReverseList(head); // newhead为翻转后的新头部
TreeNode* curr = newhead;
while (curr != NULL)
{
ans.push_back(curr->val);
curr = curr->right;
}
postMorrisReverseList(newhead); // 遍历结束后再次翻转恢复原链表
}
//翻转单链表函数
TreeNode* postMorrisReverseList(TreeNode* head) {
TreeNode* curr = head;
TreeNode* pre = NULL; // 哨兵结点
while (curr != NULL)
{
TreeNode* next = curr->right;
curr->right = pre;
pre = curr;
curr = next;
}
return pre;
}
}
层序遍历
层序遍历与上述都不相同,这是广度优先搜索,我们需要使用队列来作为辅助工具。并且有一点很重要,层序遍历往往是递归使用迭代实现的主要方法之一。
简单层序遍历(不需要分层):
每出队一个结点,就把这个结点的左右子结点按顺序入队,循环至队列为空即可
复杂层序遍历(需要分层):
难点是如何表示每一层的层级呢?我们可以用一种巧妙的方法修改广度优先搜索:

首先根结点root入队,当队列不为空的时候求当前队列的长度L
依次出队L个元素并将这些元素的子结点全部按顺序入队
返回步骤2,进入下一次迭代
优化后的算法和普通广度优先搜索的区别在于,普通广度优先搜索每次只出队一个元素并拓展,而这里每次同时出队L个元素,而这L个元素就是当前层的所有结点。因此我们的优化就是每次循环结束时队列中仅存在同一层的结点,并且这种同时打包出队本层结点同时打包入队下一层结点并不影响其原本的相对顺序(层序遍历的特性)。
C++

class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> q;
vector<vector<int>> ans;
if(!root)
return ans;
// 根结点入队
q.push(root);
while(!q.empty())
{
int length = q.size();
// curr存储出队的当前层结点
vector<int> curr;
for(int n = 0; n < length; n++)
{
// 对每个出队结点扩展子结点
auto node = q.front();
if(node->left != NULL)
q.push(node->left);
if(node->right != NULL)
q.push(node->right);
curr.push_back(node->val);
q.pop();
}
// 直接将这一层结点的结果放入二维数组中
ans.push_back(curr);
}
return ans;
}
};
沉下心来看到这里,吃透了所有知识点的你,一定会感谢当初一步一个脚印付出的自己。
当然如果可以我也建议你自己单独写一个题解,能提升记忆,加深理解,冲冲冲!

作者:bei-zhi-hu
链接:https://leetcode-cn.com/problems/binary-tree-preorder-traversal/solution/cer-cha-shu-san-chong-bian-li-qian-zhong-erk2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

posted @ 2021-07-31 21:02  linqiaozhou  阅读(69)  评论(0编辑  收藏  举报