看了一些关于二叉树遍历算法的文章,例如:
二叉树三种遍历方式的递归和循环实现
二叉树的递归与非递归遍历(前序、中序、后序)
二叉树遍历之morris traversal
回忆了一下二叉树的遍历思路,然后用递归的方式来写三种遍历算法,都非常简单,但是使用非递归方式来写三种遍历算法就有点绕了。所以总结了一下比较简单并且逻辑清晰的遍历算法。
二叉树的基本结构
//头文件
#include <stdio.h>
#include <iostream>
#include <stack> //非递归方式用到了栈
//二叉树基本结构
struct BTree{
std::string value;
BTree* leftChild;
BTree* rightChild;
BTree(const std::string &val, BTree* left = NULL, BTree* right = NULL)
{
value = val;
leftChild = left;
rightChild = right;
}
};
先序(先根)遍历算法
遍历顺序:根节点 - 左子节点 - 右子节点
这里也可以写作:根节点 - 右子节点 - 左子节点。
因为对于二叉树(非排序二叉树)来说,先遍历左节点还是右节点实际上是一样的,在算法上也只是交换一下所指元素的区别。
先序、中序、后序遍历算法中的顺序,实际上是指的访问根节点的顺序。
递归实现思路:先处理当前节点的值,然后将左子节点递归处理,最后将右子节点递归处理。
//先序遍历(递归)
void pre_visit_recursive(BTree* root)
{
if(root)
{
std::cout << root->value.c_str() << "\t"; //处理当前节点值
pre_visit_recursive(root->leftChild); //递归处理左子节点
pre_visit_recursive(root->rightChild); //递归处理右子节点
}
}
非递归实现思路:使用栈来保存需要返回后处理的节点。
- 如果当前节点存在,则处理当前节点的value(先处理根节点的值),然后将当前节点入栈,当前节点指向leftChild,并对leftChild(此时的当前节点)进行相同处理。重复1
- (当前节点不存在)当前节点指向栈顶元素,栈顶元素出栈,当前节点指向rightChild,并对rightChild(此时的当前节点)进行相同处理。重复1
//先序遍历(非递归)
void pre_visit(BTree* root)
{
std::stack<BTree*> stack_tree; //使用栈来保存需要返回再处理的元素
BTree* cur_node = root; //定义一个指针用来指向当前节点
while(cur_node != NULL || !stack_tree.empty())
{
if(cur_node != NULL)
{
std::cout << cur_node->value.c_str() << "\t"; //处理当前节点的值
stack_tree.push(cur_node); //当前节点入栈
cur_node = cur_node->leftChild; //指向左子节点,进行相同处理
}
else
{
cur_node = stack_tree.top(); //指向栈顶元素,这里不会将栈顶元素出出栈
stack_tree.pop(); //栈顶元素出栈
cur_node = cur_node->rightChild; //指向右子节点,进行相同处理
}
}
}
中序遍历
遍历顺序:左子节点 - 根节点 - 右子节点
递归实现思路:先将左子节点进行递归处理,然后处理当前节点的值,最后将右子节点进行递归处理。
//中序遍历(递归)
void mid_visit_recursive(BTree* root)
{
if(root)
{
mid_visit_recursive(root->leftChild); //左子节点递归处理
std::cout << root->value.c_str() << "\t"; //处理当前节点的值
mid_visit_recursive(root->rightChild); //右子节点递归处理
}
}
非递归实现思路:只用栈来保存需要返回处理的节点。与先序遍历类似。
- 如果当前节点存在,则当前节点入栈,指向leftChild,并leftChild(此时的当前节点)进行相同处理。重复1
- (当前节点不存在)当前指向栈顶元素,栈顶元素出栈,处理当前节点值(因为左子节点不存在或者已经处理完了),指向rightChild,并对rightChild(此时的当前节点)进行相同处理。重复1
//中序遍历(非递归)
void mid_visit(BTree* root)
{
std::stack<BTree*> stack_tree;
BTree* cur_node = root;
while(cur_node != NULL || !stack_tree.empty())
{
if(cur_node != NULL)
{
stack_tree.push(cur_node); //当前节点入栈
cur_node = cur_node->leftChild; //指向左子节点,进行相同处理
}
else
{
cur_node = stack_tree.top(); //指向栈顶节点
stack_tree.pop(); //栈顶节点出栈
std::cout << cur_node->value.c_str() << "\t"; //处理当前节点的值
cur_node = cur_node->rightChild; //指向右子节点,进行相同处理
}
}
}
后序遍历
遍历顺序:左子节点 - 右子节点 - 根节点
递归实现思路:先将左子节点进行递归处理,再将右子节点进行递归处理,最后处理当前节点的值。
//后序遍历(递归)
void post_visit_recursive(BTree* root)
{
if(root)
{
post_visit_recursive(root->leftChild); //左子节点递归处理
post_visit_recursive(root->rightChild); //右子节点递归处理
std::cout << root->value.c_str() << "\t"; //处理根节点的值
}
}
非递归实现思路:使用栈来保存需要返回处理的节点。这里用到了两个栈,一个用于存放二叉树节点,一个用于存放标志位,0表示处理了左子节点,1表示处理了右子节点。
后序遍历与前两者不同,前两者在代码逻辑上区分处理了左、右子节点(即压栈时,就已经处理过了左子节点,出栈后直接指向右子节点即可),但是后续遍历存在着需要区分是否处理过左子节点的问题(压栈时左右子节点都没有先处理过,需要等待左右子节点均处理完后才能处理根节点的值),所以需要添加标识来判断当前是否已经处理过左右子节点。
- 如果当前节点存在,则当前节点入栈,指向leftChild,标识0入栈,并对leftChild(此时的当前节点)进行相同处理。重复1
- (当前节点不存在)指向栈顶元素,栈顶元素出栈,标志位出栈。如果标志位为0,则当前节点入栈,指向rightChild,标识1入栈,并对rightChild(此时的当前节点)进行步骤1的处理,重复1;如果标志位为1,则说明处理过了左右子节点,此时处理当前节点的value,继续对栈顶元素进行相同处理(当前节点置空,重复步骤2,重复2)。
//后序遍历(非递归)
void post_visit(BTree* root)
{
std::stack<BTree*> stack_tree; //保存需要返回处理的节点
std::stack<int> stack_flag; //保存返回的路径标识
BTree* cur_node = root;
while(cur_node != NULL || !stack_tree.empty())
{
if(cur_node != NULL)
{
stack_tree.push(cur_node); //当前节点入栈
stack_flag.push(0); //下一步处理leftChild,返回时从leftChild返回,保存标识为0
cur_node = cur_node->leftChild; //指向leftChild,进行步骤1处理
}
else
{
cur_node = stack_tree.top(); //指向栈顶元素
stack_tree.pop(); //节点出栈
int flag = stack_flag.top(); //获取返回路径
stack_flag.pop(); //标识出栈
if(flag == 0)
{
//从leftChild返回,此时应该处理rightChild
stack_tree.push(cur_node); //当前节点入栈
stack_flag.push(1); //下一步处理rightChild,保存标识1
cur_node = cur_node->rightChild; //指向rightChild,进行步骤1处理
}
else
{
//从rightChild返回,此时应该处理根节点的值
std::cout << cur_node->value.c_str() << "\t"; //处理根节点的值
cur_node = NULL; //为了进行步骤2,根据循环逻辑,这里要将cur_node置空
}
}
}
}
最后测试用的代码如下:
int main()
{
BTree* A = new BTree("A");
BTree* B = new BTree("B");
BTree* C = new BTree("C");
BTree* D = new BTree("D");
BTree* E = new BTree("E");
BTree* F = new BTree("F");
A->leftChild = B;
A->rightChild = C;
B->leftChild = D;
B->rightChild = E;
C->leftChild = F;
//测试代码
std::cout << "先序遍历-递归" << std::endl;
pre_visit_recursive(A);
std::cout << std::endl;
std::cout << "先序遍历-非递归" << std::endl;
pre_visit(A);
std::cout << std::endl;
std::cout << "中序遍历-递归" << std::endl;
mid_visit_recursive(A);
std::cout << std::endl;
std::cout << "中序遍历-非递归" << std::endl;
mid_visit(A);
std::cout << std::endl;
std::cout << "后序遍历-递归" << std::endl;
post_visit_recursive(A);
std::cout << std::endl;
std::cout << "后序遍历-非递归" << std::endl;
post_visit(A);
std::cout << std::endl;
getchar();
return 0;
}
结果:
先序遍历-递归
A B D E C F
先序遍历-非递归
A B D E C F
中序遍历-递归
D B E A F C
中序遍历-非递归
D B E A F C
后序遍历-递归
D E B F C A
后序遍历-非递归
D E B F C A
总结
上述算法只是相对便于理解的写法,对于后续遍历的非递归算法肯定还有更高效的解。
除了了解二叉树的遍历,还应该了解二叉树的应用场景,以及优势与劣势。