一种二叉树非递归遍历的简单写法
二叉树的遍历是数据结构中非常基础的一个知识点,也是面试手撕代码环节的一个常见题目。这个问题的递归写法是比较简单的,但是如果面试官要求使用非递归写法,那么恭喜你同学,你被卷了。
这里提供一种简单的写法,我这里的简单,是指分析方法简单,而不是简洁(简洁的代码要看懂未必简单)。只要把循环中可能出现的各种情况分析清楚,那么写代码时只是把一堆if else
填进去而已。
我们的分析方法的要点是:在每一轮循环中,把握住当前节点的状态,以及其左、右子树是否为空。下面我们将依次对先序、中序和后序遍历进行讨论。
先序遍历
针对当前节点,其左右子树无非下表列的四种情况。当右子树为空时,当前节点无需入栈,因为我们不需要再回头访问它的右子树了。当左子树为空但右子树非空时,当前节点也无需入栈,因为我们下一个就要访问它的右子树了。
同时我们需要一个标志变量,记录下当前的节点是否是从栈里弹出来的,如果是,那么不输出当前节点,并开始访问其右子树。
当前节点不是从栈里弹出来的 | 当前节点是从栈里弹出来的 | |
---|---|---|
左非空,右非空 | 输出当前节点,当前节点入栈,置左子树为当前节点 | 不输出当前节点,当前节点不入栈,置右子树为当前节点 |
左非空,右空 | 输出当前节点,当前节点不入栈,置左子树为当前节点 | 不存在这种情况,因为没有入栈 |
左空,右非空 | 输出当前节点,当前节点不入栈,置右子树为当前节点 | 不存在这种情况,因为没有入栈 |
左空,右空 | 输出当前节点,当前节点不入栈,从栈中弹出节点置为当前节点 | 不存在这种情况,因为没有入栈 |
好了,有了上面这个表,我们只需要把这个逻辑填进循环体就可以了,不过是一堆if else
。
循环终止的条件是什么?栈中保存的是我们需要回头访问其右子树的节点。栈为空,说明整个二叉树中此时已经不存在还需要访问的右子树了,而按照先序遍历的顺序,右子树是最后才访问的,因此整个二叉树中已经不存在需要访问的节点了。所以栈为空时循环结束。
代码如下:
std::vector<int> PreOrderTraversal(TreeNode* root) {
std::vector<int> ans;
if(!root) return ans;
std::stack<TreeNode*> stack_node;
TreeNode* cur_node = root;
bool cur_is_poped = false;
while(true) {
if(!cur_is_poped) {
if(cur_node->left) {
if(cur_node->right) stack_node.push(cur_node);
ans.push_back(cur_node->val);
cur_node = cur_node->left;
} else if(cur_node->right) {
ans.push_back(cur_node->val);
cur_node = cur_node->right;
} else {
ans.push_back(cur_node->val);
if(stack_node.empty()) break;
cur_node = stack_node.top();
stack_node.pop();
cur_is_poped = true;
}
} else {
cur_node = cur_node->right;
cur_is_poped = false;
}
}
return ans;
}
中序遍历
中序遍历的情况相对简单。按照先序遍历的分析方法,我们可以得到类似的表格。
左非空,右非空 | 输出当前节点,置右子树为当前节点,下一轮循环需对右子树进行遍历 |
左非空,右空 | 输出当前节点,从栈中弹出节点置为当前节点 |
左空,右非空 | 输出当前节点,置右子树为当前节点,下一轮循环需对右子树进行遍历 |
左空,右空 | 输出当前节点,从栈中弹出节点置为当前节点 |
代码如下:
std::vector<int> InOrderTraversal(TreeNode* root) {
std::vector<int> ans;
if(!root) return ans;
std::stack<TreeNode*> s;
TreeNode* cur_node = root;
bool cur_is_poped = false;
while(true) {
if(!cur_is_poped) {
while(cur_node) {
s.push(cur_node);
cur_node = cur_node->left;
}
}
if(s.empty()) break;
cur_node = s.top();
s.pop();
ans.push_back(cur_node->val);
cur_is_poped = true;
if(cur_node->right) {
cur_node = cur_node->right;
cur_is_poped = false;
}
}
return ans;
}
可以看出,所有的节点都是先入栈,再出栈并输出的。因此循环中止的条件,仍然是栈为空。
后序遍历
后序遍历比中序遍历复杂的地方在于:中序遍历时,栈顶元素必然被出栈并输出;但是后序遍历需要先访问其右子树,然后再输出该节点。我们需要一个标记变量,标记栈中的元素是否已经在栈顶出现过了,如果没有出现过,且其右子树不为空,那我们需要先访问其右子树。如果出现过,那么直接输出。
当前节点未在栈顶出现过 | 当前节点已在栈顶出现过 | |
---|---|---|
左非空,右非空 | 不输出当前节点,当前节点不出栈,置右子树为当前节点 | 输出当前节点,当前节点出栈,从栈中弹出节点置为当前节点 |
左非空,右空 | 输出当前节点,当前节点出栈,从栈中弹出节点置为当前节点 | 不存在,因为直接出栈了 |
左空,右非空 | 不输出当前节点,当前节点不出栈,置右子树为当前节点 | 输出当前节点,当前节点出栈,从栈中弹出节点置为当前节点 |
左空,右空 | 输出当前节点,当前节点出栈,从栈中弹出节点置为当前节点 | 不存在,因为直接出栈了 |
std::vector<int> PostOrderTraversal(TreeNode* root) {
std::vector<int> ans;
if(!root) return ans;
using NodePair = std::pair<TreeNode*, bool>;
std::stack<NodePair> s;
NodePair cur_node_pair;
TreeNode* cur_node = root;
while(true) {
while(cur_node) {
s.push(std::make_pair(cur_node, false));
cur_node = cur_node->left;
}
if(s.empty()) break;
cur_node_pair = s.top();
if(!cur_node_pair.second) { //当前节点未在栈顶出现过
if(cur_node_pair.first->right) {
cur_node = cur_node_pair.first->right;
cur_node_pair.second = true;
s.top() = cur_node_pair;
} else {
ans.push_back(cur_node_pair.first->val);
s.pop();
}
} else { //当前节点已在栈顶出现过
ans.push_back(cur_node_pair.first->val);
s.pop();
}
}
return ans;
}