二叉树遍历有四种形式:先序遍历、中序遍历、后序遍历和层序遍历,其中先序、中序、后序遍历均有递归遍历和非递归遍历两种方式。本文将介绍这四种遍历的原理与C++实现。不熟悉C++的小伙伴儿可以不关注程序具体实现,只查阅其实现思路即可。
首先,我们先定义下树节点结构,并假设节点中存储的是int类型的数据,实现如下:
class TreeNode
{
public:
TreeNode(int data):m_data(data), m_leftChild(NULL), m_rightChild(NULL) {}
~TreeNode() {
delete m_leftChild;
delete m_rightChild;
m_leftChild = NULL;
m_rightChild = NULL;}
public:
int m_data;
TreeNode* m_leftChild;
TreeNode* m_rightChild;
};
注:TreeNode其实可以定义成模板类,存储的数据类型可以在使用时再指定,本文为了方便描述,暂时用了简单数据类型int,小伙伴们后续可自行修改为模板类实现。不熟悉模板类的小伙伴儿可以参看我之前的博客C++ 模板。
接下来,我们定义下二叉树结构,如下:
class BinaryTree
{
public:
BinaryTree():m_root(NULL){}
~BinaryTree(){ delete m_root; m_root = NULL; }
void PreOrderTraversal_Recursive(TreeNode* tree);//先序遍历递归实现
void PreOrderTraversal_NotRecursive(TreeNode* tree);//先序遍历非递归实现
void InOrderTraversal_Recursive(TreeNode* tree);//中序遍历递归实现
void InOrderTraversal_NotRecursive(TreeNode* tree);//中序遍历非递归实现
void PostOrderTraversal_Recursive(TreeNode* tree);//后序遍历递归实现
void PostOrderTraversal_NotRecursive(TreeNode* tree);//后序遍历非递归实现
void LevelOrderTranversal(TreeNode* tree);//层序遍历
protected:
TreeNode* m_root;
};
m_root定义为protected,方便后续特殊树(二叉搜索树等)继承该类时能访问到该成员。准备工作准备完毕,接下来将对各类遍历进行介绍并实现BinaryTree中的遍历函数。
遍历过程为:
该递归遍历的思路和实现按照上述描述的遍历过程都比较好理解,也就是对每一个树节点,先访问自己,再访问其左儿子,最后访问其右儿子。其实现如下:
void BinaryTree::PreOrderTraversal_Recursive(TreeNode* tree)
{
if (!tree)
return;
std::cout << tree->m_data << " ";
PreOrderTraversal_Recursive(tree->m_leftChild);
PreOrderTraversal_Recursive(tree->m_rightChild);
}
递归实现根本实现方法其实是堆栈,非递归遍历实现的基本思路就是直接使用堆栈。
实现思路:
void BinaryTree::PreOrderTraversal_NotRecursive(TreeNode* tree)
{
if (!tree)
return;
std::stack<TreeNode*> nodeStack;
TreeNode* curNode = tree;
while (curNode || !nodeStack.empty())
{
while (curNode)
{
std::cout << curNode->m_data << " ";
nodeStack.push(curNode);
curNode = curNode->m_leftNode;
}
if (!nodeStack.empty())
{
curNode = nodeStack.top();
nodeStack.pop();
curNode = curNode->m_rightNode;
}
}
}
遍历过程为:
对每一个树节点,先访问其左儿子,再访问自己,最后访问其右儿子。其实现如下:
void BinaryTree::InOrderTraversal_Recursive(TreeNode* tree)
{
if (!tree)
return;
InOrderTraversal_Recursive(tree->m_leftChild);
std::cout << tree->m_data << " ";
InOrderTraversal_Recursive(tree->m_rightChild);
}
中序遍历的非递归实现思路与先序遍历类似,只有一点不同:
因此,其实现如下:
void BinaryTree::InOrderTraversal_NotRecursive(TreeNode* tree)
{
if (!tree)
return;
std::stack<TreeNode*> nodeStack;
TreeNode* curNode = tree;
while (curNode || !nodeStack.empty())
{
while (curNode)
{
nodeStack.push(curNode);
curNode = curNode->m_leftNode;
}
if (!nodeStack.empty())
{
curNode = nodeStack.top();
std::cout << curNode->m_data << " ";
nodeStack.pop();
curNode = curNode->m_rightNode;
}
}
}
可看出其与先序遍历唯一的区别是打印节点数据的位置不同。
遍历过程为:
对每一个树节点,先访问其左儿子,再访问其右儿子,最后访问自己。其实现如下:
void BinaryTree::PostOrderTraversal_Recursive(TreeNode* tree)
{
if (!tree)
return;
PostOrderTraversal_Recursive(tree->m_leftChild);
PostOrderTraversal_Recursive(tree->m_rightChild);
std::cout << tree->m_data << " ";
}
后序遍历较前两种遍历来说要复杂些,对于前两种遍历,在访问一个节点的右子树之前,该节点是从堆栈中弹出了的,后续只需处理其右子树;而后序遍历时,从堆栈中获取该节点后,需要先处理其右子树,不能直接弹出该节点,也就是说该节点需要等左右子树都处理完毕后才能最终从栈里弹出后进行访问。
现在我们再分析一个问题:一个节点从栈里弹出的时机。该时机应该有三种情况:
我们再继续分析一个问题:一个节点本身、其左儿子和其右儿子在堆栈中的位置关系。需要先访问左儿子,再访问右儿子,最后访问自己,按照该访问过程,其在堆栈中应为:
结合上一个问题,我们可以看到,只需要记录上一个从栈里弹出的节点,比较其与现在处理节点的关系,即可知道该节点是否可以弹出。
基于以上分析,其实现如下:
void BinaryTree::PostOrderTraserval_NotRecursive(TreeNode* tree)
{
if (!tree)
return;
std::stack<TreeNode*> nodeStack;
TreeNode* curNode = tree;
TreeNode* preNode = NULL;
nodeStack.push(curNode);
while (!nodeStack.empty())
{
curNode = nodeStack.top();
if ((NULL == curNode->m_leftNode && NULL == curNode->m_rightNode) ||
(NULL != preNode && preNode == curNode->m_leftNode && NULL == curNode->m_rightNode) ||
(NULL != preNode && preNode == curNode->m_rightNode))
{
std::cout << curNode->m_data << " ";
nodeStack.pop();
preNode = curNode;
}
if (curNode->m_rightNode)
nodeStack.push(curNode->m_rightNode);
if (curNode->m_leftNode)
nodeStack.push(curNode->m_leftNode);
}
}
遍历过程为:
对于一个节点来说,层序遍历过程即为:自己 -> 左儿子 -> 右儿子;
如果该节点存在孙子节点,遍历过程为:自己 -> 左儿子 -> 右儿子 -> 孙子。
而孙子节点只能通过自己的儿子节点才能访问到,也就是自己、儿子、孙子节点获取的过程为:自己 -> 左儿子( 获取左儿子的儿子节点) -> 右儿子(获取右儿子的儿子节点), 怎样才能保证在获取到左儿子的儿子节点时,右儿子排在左儿子的儿子节点前面被访问到?答案是借助于队列。即将左儿子、右儿子依次放入队列,从队列中弹出左儿子,将左儿子的儿子节点依次放入队列,此时便能保证孙子节点永远在儿子节点的后面。
基于以上分析,层序遍历实现如下:
void BinaryTree::LevelOrderTraserval(TreeNode* tree)
{
if (!tree)
return;
std::queue<TreeNode*> nodeQueue;
TreeNode* curNode = tree;
nodeQueue.push(curNode);
while (!nodeQueue.empty())
{
curNode = nodeQueue.front();
nodeQueue.pop();
std::cout << curNode->m_data << " ";
if (curNode->m_leftNode)
nodeQueue.push(curNode->m_leftNode);
if (curNode->m_rightNode)
nodeQueue.push(curNode->m_rightNode);
}
}
至此,二叉树的四种遍历方式的各种实现便介绍完了,小伙伴们可以自行构造数据对上述7种方法进行验证,欢迎一起讨论学习哦。