1. 简介
二叉树的遍历方式有多种,最常见的四种为前序遍历(深度优先遍历),中序遍历,后序遍历,以及宽度优先遍历。其中前三种遍历方式都是递归定义的,它们三种遍历对空树都不做任何事情。
前序遍历要求先访问根结点,然后前序遍历左子树,然后再前序遍历右子树,简称”根左右”。
中序遍历要求先中序遍历左子树,然后访问根结点,然后中序遍历右子树,简称”左根右”。
后序遍历则要求先后序遍历左子树,再后序遍历右子树, 最后访问根结点,简称”左右根”。
2. 非递归实现
由于前序遍历,中序遍历,以及后序遍历的定义就是递归定义的,因此使用递归来实现非常的方便,我们主要关注这三种遍历的非递归实现。
2.1 前序遍历
前序遍历,为了模拟前序遍历,我们用一个栈存储待访问且待展开左右子树的结点。因此,我们从根节点开始,访问,然后将其右结点和左结点放到栈中。接下来弹出栈顶,访问该结点,不断重复该过程,直到栈为空。
也可以用栈存储待展开右子树的结点,在展开左子树的时候就进行访问,这种实现方式和中序遍历相似。
2.2 中序遍历
中序遍历,为了模拟中序遍历,我们用一个栈存储待访问且待展开右子树的结点。因此,我们从根结点开始,不断访问其左子树,并把其中非空的结点(包括根节点)存到栈中,直到树为空为止,此时我们弹出栈顶,访问该结点,然后从其右结点当作根节点,重复上述过程直到栈为空。
2.3 后序遍历
后序遍历,为了模拟后序遍历,我们用一个栈存储待访问或待展开右子树的结点,该访问或者该展开右子树取决于我们在弹出栈的元素时第几次碰到该结点,如果是第一次,那么不弹出也不访问他,而是展开其右子树,否则,访问该结点并弹出。。因此,我们从根结点开始,不断访问其左子树,并把其中非空的结点(包括根节点)存到栈中,直到树为空为止。此时我们查看栈顶元素,如果该结点是第一次访问,把其右结点当作根结点,重复上述过程;如果是第二次,那么访问该结点并且弹出,继续查看栈顶元素。
之所以要第二次出现在栈顶才弹出,是为了确保其右子树已经访问完毕,如果不想保存每个结点在栈顶出现的次数,可以更简单的在每次弹出元素时记录该结点,在查看下一个结点的时候只要判断它的右结点是否为之前记录的结点(两者均为null也是正确的,如果左结点是null,那么右结点也是null,该结点照样该弹出,不必再访问其右结点)。
更简单的方法,注意到后序遍历是”根右左”的反序,我们可以用两个栈,一个实现”根右左”的前序遍历,但不访问,而是放到另一个栈中,最后再将该栈不断弹出访问即可。
3. LeetCode OJ
leetcode上提供了三道相关的题目,分别是94,144和145,分别对应中序,前序和后序。通过这三道题的非递归代码和上面分析的一样,前序最简单,中序其次,后序最难。
//Leetcode上二叉树用的数据结构
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
//前序遍历
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode *> mystack;
mystack.push(root);
TreeNode *current;
while(!mystack.empty()) { //栈正确的顺序存储了每个待访问且待展开左右子树的结点
current = mystack.top();
mystack.pop();
if(current == NULL)
continue;
result.push_back(current->val);
mystack.push(current->right); //先放右子树,因为存储结构是栈
mystack.push(current->left); //后放左子树
}
return result;
}
};
//另一种前序遍历实现方式,类似前序遍历
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode *> mystack;
TreeNode *current = root;
while(current != NULL || !mystack.empty()) {
while(current != NULL) {
result.push_back(current->val); //在展开左子树的过程就访问
mystack.push(current); //栈中存放的是待展开右子树的结点
current = current->left;
}
current = mystack.top(); //当前左子树访问完毕的元素会出现在栈顶
mystack.pop();
current = current->right; //展开其右子树
}
return result;
}
};
//中序遍历
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode *> mystack;
TreeNode *current = root;
while(current != NULL || !mystack.empty()) {
while(current != NULL) { //不断寻找其左子树,直到为空
mystack.push(current); //栈正确的顺序存储了每个待访问且待展开右子树的结点
current = current->left;
}
current = mystack.top(); //当前左子树访问完毕的结点会出现在栈顶
mystack.pop();
result.push_back(current->val);
current = current->right; //展开其右子树
}
return result;
}
};
//后序遍历
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode *> mystack;
TreeNode *current = root;
TreeNode *pre = NULL;
while(true) {
while(current != NULL) { //不断寻找左子树,直到为空
mystack.push(current);
current = current->left;
}
pre = NULL;
while(!mystack.empty()) {
current = mystack.top(); //当前左子树访问完毕的结点会出现在栈顶
if(current->right == pre) { //如果右子树也访问完毕,访问并弹出
pre = current;
mystack.pop();
result.push_back(current->val);
} else { //否则展开其右子树
current = current->right;
break;
}
}
if(mystack.empty()) {
break;
}
}
return result;
}
};
//第二种解法,借助前序遍历,使用两个栈
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode *> mystack;
stack<TreeNode *> mystack2;
TreeNode *current = root;
mystack.push(root);
while(!mystack.empty()) {
current = mystack.top();
mystack.pop();
if(current == NULL)
continue;
mystack2.push(current); //不访问,而是存入第二个栈中
mystack.push(current->left); //为了先访问右子树,得先放左结点
mystack.push(current->right); //为了先访问右子树,得后放右结点
}
while(!mystack2.empty()) {
current = mystack2.top();
mystack2.pop();
result.push_back(current->val);
}
return result;
}
};