一个函数搞定二叉树非递归前中后序遍历

1 篇文章 0 订阅
这篇博客探讨了二叉树和N叉树的非递归遍历方法,尤其是中序和后序遍历的难点。通过创建一个 TreeNodeWrap 结构体来存储节点和标志,并使用栈进行深度遍历,实现了前序、中序和后序遍历的一般化处理。这种方法可以应用于任意序的N叉树遍历,避免了对不同遍历方式的特殊处理。
摘要由CSDN通过智能技术生成

二叉树的前序,中序,后序遍历有递归和非递归两种,其中递归的没什么好说的。难点就在于非递归如何处理。

困惑

网上有很多讲述非递归的文章,说实话,记不住,太复杂了。前序,中序,后序非递归是三种处理思路,总是看了就忘。感觉前序,中序,后序的非递归遍历在某种程度上应该是相似的,应该是一个函数就搞定了,不至于需要三个函数。

非递归遍历难在哪里

不管是前序,后序,还是中序,都与深度遍历非常像,这里面的不同就是深度遍历是先访问当前节点,后访问子节点。因此前序遍历与深度遍历是吻合的,知道深度遍历咋回事就知道前序遍历怎么写了。为什么后序遍历和中序遍历不好写,因为这两种遍历都是先访问当前节点的子节点,然后再访问当前节点。因此需要将当前节点先存下来,然后等后面合适时机的时候再访问它并将其打印,当前节点保存在哪里,后面什么时候该访问这个节点,这个就是难点所在了。因此网上一些非递归中序和前序遍历的实现基本就抛弃了深度遍历的遍历主框架了,然后考虑一些取巧的办法了。但是这种办法终究没有一般性,没法进行一般化。譬如下面的四叉树的这种遍历要用非递归实现该如何实现呢。

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叉树也不在话下。而且整个思维是统一的,不用对各个不同的遍历方式进行不同的处理了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值