二叉树的前序,中序,后序遍历有递归和非递归两种,其中递归的没什么好说的。难点就在于非递归如何处理。
困惑
网上有很多讲述非递归的文章,说实话,记不住,太复杂了。前序,中序,后序非递归是三种处理思路,总是看了就忘。感觉前序,中序,后序的非递归遍历在某种程度上应该是相似的,应该是一个函数就搞定了,不至于需要三个函数。
非递归遍历难在哪里
不管是前序,后序,还是中序,都与深度遍历非常像,这里面的不同就是深度遍历是先访问当前节点,后访问子节点。因此前序遍历与深度遍历是吻合的,知道深度遍历咋回事就知道前序遍历怎么写了。为什么后序遍历和中序遍历不好写,因为这两种遍历都是先访问当前节点的子节点,然后再访问当前节点。因此需要将当前节点先存下来,然后等后面合适时机的时候再访问它并将其打印,当前节点保存在哪里,后面什么时候该访问这个节点,这个就是难点所在了。因此网上一些非递归中序和前序遍历的实现基本就抛弃了深度遍历的遍历主框架了,然后考虑一些取巧的办法了。但是这种办法终究没有一般性,没法进行一般化。譬如下面的四叉树的这种遍历要用非递归实现该如何实现呢。
struct TreeNode
{
int m_iValue;
TreeNode* child1;
TreeNode* child2;
TreeNode* child3;
TreeNode* child4;
};
void Trav(TreeNode* pNode)
{
if (pNode == nullptr)
return;
Trav(pNode->child1);
Trav(pNode->child2);
Trav(pNode->child3);
std::cout << pNode->m_iValue << std::endl;
Trav(pNode->child4);
}
破局
这篇文章就是提供一种通用的思路来处理,可以推广到N叉树的任意序遍历。
首先,遍历的流程就是深度遍历流程,这个是遍历的主框架,不能抛弃。深度遍历是采用栈这个数据结构,然后将当前节点入栈,然后弹出,并将所有子节点入栈,然后再弹出,如此循环。然后难点在于除了前序遍历之后,其它遍历都是需要先访问子节点,再回过头来访问当前节点。因此我们可以将当前节点作为当前节点的一个子节点。譬如上面这个里面,当前节点只有四个子节点,那我可以把当前节点作为自己的一个子节点(我们姑且称为特殊子节点吧),这样子,从栈中弹出当前节点,并压入五个子节点(其中有个节点是特殊子节点)。通过调节特殊子节点在这五个子节点中的入栈顺序就可以模拟各种序的遍历方式了。
新的问题
这里面有一个问题,如果自己是自己的子节点,那后面不是陷入死循环了吗。因此我们需要对这个节点进行标记,当后面访问到这个节点的时候只需要进行打印就好了,不要再获取其子节点了。为了添加这个标记,有两种办法:
(1)一个是在TreeNode结构体中加入一个flag,这样子的坏处就是这个方案是侵入性的,与TreeNode耦合较大,不太好。
(2)还有一各办法就是声明一个TreeNodeWrap结构体,对TreeNode*进行包装,并在TreeNodeWrap加入一个flag表示是否为特殊的子节点即可。为了降低这个TreeNodeWrap的影响范围可以在函数内部定义这个结构体。此处我们采用该方案。
代码实现:
#include <iostream>
#include <stack>
//遍历模式
enum class TreeNodeTravMode
{
Pre, //前序遍历
Mid, //中序遍历
Post //后续遍历
};
struct TreeNode
{
TreeNode(int iValue, TreeNode* pLeftNode = nullptr, TreeNode* pRightNode = nullptr)
{
m_iValue = iValue;
m_pLeftNode = pLeftNode;
m_pRightNode = pRightNode;
}
int m_iValue;
TreeNode* m_pLeftNode;
TreeNode* m_pRightNode;
};
int CreateTree(TreeNode** ppRootNode);
template<class Fun>
void TravTree(TreeNode* pRootNode, TreeNodeTravMode travMode, Fun fun);
int main()
{
TreeNode* pRootNode = nullptr;
CreateTree(&pRootNode);
TravTree(pRootNode, TreeNodeTravMode::Pre,
[](const TreeNode* p)
{
std::cout << p->m_iValue << std::endl;
});
std::cout << "Hello World!\n";
getchar();
}
template<class Fun>
void TravTree
(
TreeNode* pRootNode,
TreeNodeTravMode travMode,
Fun fun
)
{
struct TreeNodeWrap
{
TreeNodeWrap(TreeNode* pNode, int iFlag)
{
m_pNode = pNode;
m_iFlag = iFlag;
}
TreeNode* m_pNode = nullptr;
int m_iFlag = 0;
};
std::stack<TreeNodeWrap> stackNodeWrap;
stackNodeWrap.push({pRootNode, 0});
while (!stackNodeWrap.empty())
{
TreeNodeWrap nodeWrap = stackNodeWrap.top();
stackNodeWrap.pop();
if (nodeWrap.m_iFlag == 1)
{
fun(nodeWrap.m_pNode);
continue;
}
nodeWrap.m_iFlag = 1;
if (travMode == TreeNodeTravMode::Post)
{ stackNodeWrap.push(nodeWrap); }
if (nodeWrap.m_pNode->m_pRightNode)
{ stackNodeWrap.push({ nodeWrap.m_pNode->m_pRightNode, 0 }); }
if (travMode == TreeNodeTravMode::Mid)
{ stackNodeWrap.push(nodeWrap); }
if (nodeWrap.m_pNode->m_pLeftNode)
{ stackNodeWrap.push({ nodeWrap.m_pNode->m_pLeftNode, 0 }); }
if (travMode == TreeNodeTravMode::Pre)
{ stackNodeWrap.push(nodeWrap); }
}
}
int CreateTree(TreeNode** ppRootNode)
{
if (!ppRootNode)
return 1;
TreeNode* p1 = new TreeNode(1);
TreeNode* p3 = new TreeNode(3);
TreeNode* p2 = new TreeNode(2, p1, p3);
TreeNode* p5 = new TreeNode(5);
TreeNode* p7 = new TreeNode(7);
TreeNode* p6 = new TreeNode(6, p5, p7);
TreeNode* p4 = new TreeNode(4, p2, p6);
*ppRootNode = p4;
return 0;
}
看了这个例子,你应该知道如何处理二叉树的非递归前序,中序和后续遍历了,想怎么遍历就怎么遍历了,N叉树也不在话下。而且整个思维是统一的,不用对各个不同的遍历方式进行不同的处理了。