二叉树的遍历问题是二叉树的基本问题,也是在数据结构中比较常见的问题,二叉树的遍历分为前序遍历、中序遍历和后序遍历,其中这些某序指的是每棵子树根节点的位置,对于左右分支来说,顺序永远都是先左后右。我相信学过数据结构的姑娘和小伙子一定对这个都有所了解,并且也都能写出对已知二叉树进行遍历的代码。
所以提到二叉树前中后序遍历最先想到的算法是什么呢? 我猜绝大多数人首先想到的都是递归的方法,我也是哦,一说到前中后序就递归,这多简单啊,直接将返回值按照所需的顺序组合就行了。可是呢算法就是个千变万化的东西,如果就是不许你用递归那该怎么办呢,所以学会用非递归的方法来前中后序遍历二叉树也是非常重要滴,至少我失败的面试经验是酱告诉我的。。。
废话不多说,我们来看思路和代码(代码是C++的哦)
一、非递归前序遍历
前序遍历顾名思义就是根节点在前,也就是遍历的结果是 根节点 + 左子树 + 右子树。如果几个例子试一下我们会发现有这样一个规律,就是如果一个节点它有左子节点,那它的下一个前序遍历结果一定是它的左子树,那如果没有左子树呢? 那就往上一步一步的“回退”,找到第一个含有右子节点的节点,把它的右子节点设为当前节点,为什么要这么做呢,那是因为左边遍历完了就轮到右边了吧,这就是前序遍历的定义。然后我们不断的重复以上步骤,就完成了遍历。
那这个过程怎么才能不递归的实现呢,关键在于那个“回退”的过程,这时我们就想到在数据结构中有一个先入后出的神奇结构,没错那就是栈,我们只要在遍历左子树的时候压栈,而需要去找右子树的时候弹栈,这样就完成了之前的“回退”过程。
原理分析完了就放代码吧:
vector<int> PreOrder(TreeNode* root)
{
if(root == NULL) return vector<int>();
stack<TreeNode*> s;
vector<int> res;
s.push(root);
res.push_back(root->val);
TreeNode* cur = root->left;
while(!s.empty() || cur != NULL)
{
while(cur != NULL)//遍历左子节点直到叶子节点
{
s.push(cur);
res.push_back(cur->val);
cur = cur->left;
}
cur = s.top()->right;//将当前节点设为最近右子节点
s.pop();
}
return res;
}
值得注意的是,在将当前节点设为右子节点的时候,需要将当前子树的根节点从栈中弹出,因为它已经完成使命了,留着会造成混乱和死循环。外括号的判断条件也不用漏掉cur != NULL,因为当根节点被弹出,右子树为当前节点时,栈是空的,不加这个判断条件会是根节点的右子树无法被遍历哦。
二、非递归中序遍历
非递归中序遍历和前序遍历其实很像的,中序遍历是 左子树 + 根节点 + 右子树,我们在之前进行前序遍历的时候,是在根节点入栈的时候,且在访问左子树之前,访问根节点将内容取出,好啦那现在我们的顺序变了,中序遍历的时候根节点要在左子树的后面了这该怎么办呢?这时候我们发现之前的前序遍历有一个根节点弹栈的过程,这个弹栈的步骤正好是在访问完左子树接下来马上就要访问右子树的时候,而这个地方恰好就是中序遍历中访问根节点的时机,哇真是好巧啊,那干脆入栈的时候不要访问,弹栈的时候再访问根节点就好了,只要做小小的改动,就可以完成中序遍历了,那我们来看一下代码吧:
vector<int> InOrder(TreeNode* root)
{
if(root == NULL) return vector<int>();
stack<TreeNode*> s;
vector<int> res;
s.push(root);
TreeNode* cur = root->left;
while(!s.empty() || cur != NULL)
{
while(cur != NULL)
{
s.push(cur);
cur = cur->left;
}
cur = s.top()->right;
res.push_back(s.top()->val);//在弹栈时访问根节点
s.pop();
}
return res;
}
通过对比我们发现,中序遍历和前序遍历唯一的区别就是根节点访问的时机,果然通过修改访问时机就能完成前中序遍历的自如转换呢~
三、非递归后序遍历
之前我们一起看了非递归前序遍历和中序遍历的思路和写法,我们发现他们之间有着很大的联系,连思路基本上都差不多,那这样就简单了,后序遍历也就改改就可以了对吧
但是等一下,是这样吗?后序遍历是 左子树 + 右子树 + 根节点 这样的访问顺序,在我们之前的思路中在访问右子树之前就已经把根节点弹栈了,也就是说访问完右子树后是无法找到其根节点的。那我们就不弹栈那不就行了,但是这样的话下一次访问的还是栈顶元素,我们不知道这个栈顶元素的右子树是不是已经被遍历过了,那说到这的话你可能就会想了,那我加一个标记不就行了,如果访问过该节点的右子树,就给他做个标记,下次访问到他的时候只是把值输出然后不再访问右子树不就行了,嗯,看来我们已经找到后序遍历的思路了,那就开始写吧,这个标记我在这里用了一根bool类型的栈来存储
vector<int> PostOrder(TreeNode* root)
{
if(root == NULL) return vector<int>();
stack<TreeNode*> s;
stack<bool> isFirst;//存储是否是第一次被访问
vector<int> res;
s.push(root);
isFirst.push(true);
TreeNode* cur = root->left;
while(!s.empty() || cur != NULL)
{
while(cur != NULL)
{
s.push(cur);
isFirst.push(true);
cur = cur->left;
}
if(isFirst.top())//如果第一次被访问更新标记,更新当前节点为右子树
{
isFirst.pop();
isFirst.push(false);
cur = s.top()->right;
}
else//如果已经被访问过一次,则返回值且弹出
{
res.push_back(s.top()->val);
isFirst.pop();
s.pop();
}
}
return res;
}
上面的代码中如果已经被访问过一次,也就是else的部分为什么不更新当前节点的位置呢,那是因为我们不知道之前的节点中是不是也有被访问过的,贸然更新节点位置可能会出错,所以不更新的话下次循环还是会直接判断当前栈顶节点是否被访问过,直到遇到第一个未被访问过的节点时,才将当前节点更新为右子树。
上面的代码是用另一个栈实现的,当然实现方式有很多,可以通过改变树节点的结构,增加一个是否访问过的属性,但是这种做法在已给出树的结构的情况下是不允许的。还可以用一个pair的栈来直接将节点和是否访问的信息存下来,可以缩减几行代码哈哈,还有其他的方式就不一一列举了。
以上举出的三个非递归遍历的例子只是最普通的求解思路,如有错误请各位留言指正,蟹蟹:)