二叉树遍历的非递归实现
基础知识
二叉树的遍历分为深度优先遍历(DFS)和广度优先遍历(BFS)。
深度优先遍历:尽可能地向左(或右)进行,在遇到第一个转折点,向左(或右)一步,然后再尽可能地向左(或右)发展。这一过程重复直到访问了所以节点。树的深度优先遍历一般有三种:
VLR——前序遍历(根左右)
LVR——中序遍历(左根右)
LRV——后序遍历(左右根)
核心思路
遍历时一定要搞清楚一点就是,输出都是根节点(核心思想),因此不同的遍历方式仅仅是根节点的输出时机不同。
中序遍历:访问顺序为左根右。遇到节点不能直接输出(需要确定该点为根节点),向左发展,直到左节点为空(遇到转折点),向右一步,重复上述过程。在遇到转折点时即可确定该节点为根节点,此时可以输出。
后序遍历也是同样的思想:左右子树都已经遍历才可以输出。因此需要设置标志位,某一个点需要在压入时已经访问了左子树,当访问右子树时我们将标志设为true,下一次访问到即可输出。
二叉树存放节点的结构体
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、先序遍历
分析:
先序遍历逻辑:根据深度优先的定义,一直往左发展,遇到转折点向右一步,重复上述过程直到访问完成。
非递归实现逻辑:遇到结点就输出并且压入左结点,直到(while)访问到最左端,从栈中取出栈顶元素往右一步,重复上述过程。
代码实现:
class solutions {
private:
vector<int> res; //存放结果
public:
//先序遍历递归和非递归版本
void PreOrderTree(TreeNode* root) {
if (root != nullptr) {
res.push_back(root->val);
PreOrderTree(root->left);
PreOrderTree(root->right);
}
}
void PreOrderTree2(TreeNode* root) {
stack<TreeNode*> stk;//使用栈模拟递归的调用
auto p = root;//记录头结点
while (p != nullptr || !stk.empty()) {
//遍历直到叶子结点
while (p != nullptr) {
res.push_back(p->val);
stk.push(p);
p = p->left;
}
//栈不为空,弹出元素,检测右子树
if (!stk.empty()) {
p = stk.top();
stk.pop();
p = p->right;
}
}
}
}
2、中序遍历
分析:
一直往左发展,遇到转折点(根据左根右的特点,此时可以输出)向右一步,重复上述过程直到访问完成。
非递归实现逻辑:压入左结点,直到(while)访问到最左端,从栈中取出栈顶元素并输出,然后往右一步,重复上述过程。
代码实现:
class solutions {
private:
vector<int> res; //存放结果
public:
//中序遍历(左根右)递归和非递归实现
void InOrderTree(TreeNode* root) {
if (root != nullptr) {
InOrderTree(root->left);//到最左边才能输出
res.push_back(root->val);
InOrderTree(root->right);//存在右子树,重复上述过程
}
}
void InOrderTree2(TreeNode* root) {
stack<TreeNode*> stk; //栈模拟递归流程
auto p = root;
//中序遍历逻辑:先遍历到最左,弹出过程进行输出,然后转到右子树
while (p != nullptr || !stk.empty()) {
while (p != nullptr) {
stk.push(p);
p = p->left;
}
if (!stk.empty()) {
p = stk.top();
stk.pop();
res.push_back(p->val);
p = p->right;
}
}
}
}
2、后序遍历
分析:
一直往左发展,遇到转折点向右一步(根据左右根的特点,该点左右已经访问完成,此时可以输出),重复上述过程直到访问完成。
非递归实现逻辑:压入左结点,直到(while)访问到最左端,从栈中取出栈顶元素判断是否输出(当该元素只访问了左子树就不能输出,如何解决?设置标志位,当该节点进行了右子树访问,表示可以输出),然后往右一步(设置标志位),重复上述过程。
代码实现:
class solutions {
private:
vector<int> res; //存放结果
public:
//后序(左右根)遍历递归和非递归实现
void PostOrderTree(TreeNode* root) {
if (root != nullptr) {
PostOrderTree(root->left);//遍历到最左边
PostOrderTree(root->right);
//如果存在右边,该左就变成了根,转向右子树进行遍历,直到该点左右都为空,才能输出
//而且在每一个 root->left 的return过程中都会继续向下执行检测 root->right
res.push_back(root->val);
}
}
void PostOrderTree2(TreeNode* root) {
stack<TreeNode*> stk;
//设置标志位和节点同时入栈,因为后序遍历为左右跟,每个输出实际都是根节点
//只有满足左右子树都遍历过才可以输出。节点第一次入栈时置为false,当该节点往右遍历时置为true,下次即可输出。
stack<bool> flag;
auto p = root;
while (p != nullptr || !stk.empty()) {
//遍历直到最左端
while (p != nullptr) {
stk.push(p);
flag.push(false);
p = p->left;
}
//栈不为空,转向,进行判断
if (!stk.empty()) {
p = stk.top();
//当标志位为1时打印
if (flag.top()) {
res.push_back(p->val);
stk.pop();
flag.pop();
p = nullptr;//flag=1,说明右子树已经遍历,不用执行while
}
else {
flag.top() = true;
p = p->right;
}
}
}
}
};
总结
后序遍历遍历思路:左子树右子树根节点。当某一个节点需要输出,其实该节点是根节点。当明白这一点之后遍历的逻辑就很好理解。 既然某一节点(根)需要输出,那么左右子树都已经遍历完成。因此设置标志位在压入左节点时一同压入标志(0),在转向遍历右子树时(此时该节点左右子树都遍历完成—是不是应该将标志位设为1)。之后在进行弹出操作时候判断flag是否满足输出条件即可。
按照输出都为根节点,以及先序遍历(根左右),中序遍历(左根右)的特点,同样可以写出对于的非递归版本。
先序非递归:在往左遍历的时候即可输出;
中序遍历:在左子树遍历完成后,该点(根节点)需要往右子树遍历,根据中序的特点,在转往右子树之前即可输出。
非递归的实现需要先理解递归的实现,迭代只是显示地使用了栈操作。
使用以下二叉树结果进行结果验证:
先序遍历结果:1,2,3,4,5,6,7,8;
中序遍历结果:4,3,5,2,1,7,6,8;
后序遍历结果:4,5,3,2,7,8,6,1;