二叉树的基本遍历方法是lintcode题目中的常用思想,有的题目还会要求写非递归的代码。现以递归的先序遍历为例子,分析非递归的写法。
如果用递归的写法,很容易:
void preOrder(TreeNode *T,vector<int>& result){
if(T){
result.push_back(T->val);
preOrder(T->left,result);
preOrder(T->right,result);
}
}
举例来说清楚递归的过程:
我们知道函数调用就要入栈,返回就要出栈,用栈的思路重新思考这一过程:
由于先访问左子树,会一直向左递归,故要先找到最左侧的节点,在本例中b的左子树为null;向左侧走的同时,访问节点,并用栈记录路径,代码块如下:
while(p!=null){
访问p
p入栈
p向左侧走
}
栈中有a,b,向左走到b的左子树发现为null,返回,一返回便出栈,使得p指向b,再让p向右侧走,访问右子树,便实现了根左右的顺序
当b为根的子树访问完了,出栈,p便指向了a,再向右侧走.....同理便可实现整个过程。代码块如下:
if(栈非空){
p=栈顶元素
出栈
p向右侧走
}
最后,我们再来分析一下整个函数终止的条件:
首先是栈只要不空则要执行,因为栈里有元素表示还有子树没处理
其次p不空则要执行,本例中从b返回后,a出栈,p指向a,此时栈空了,p不为空,要往右侧访问右子树
结合递归的过程来看,最后是指向到c的子树,为空,返回,结束
非递归与递归是一致的,p指向c,访问c,向左侧走,p为null;此时栈为空,p也为null,终止了
结合分析,写出代码:
void preOrder(BiTree * root){
stack<Bitree *> s;
Bitree *p=root;
while(p!=NULL&&!s.empty()){
while(p!=NULL){
cout<<p->data<<endl;
s.push(p);
p=p->left;
}
if(!s.empty){
p=s.top();
s.pop();
p=p->right;
}
}
}
最后总结一下非递归先序遍历的思路:指针从根节点不断向左侧移动,移动的同时访问节点并将节点入栈;指针不断向左移动,处理完最左侧节点后,再向左必为null(因为已经最左了,再向左不可能还有节点了),此时栈顶是最左侧子树的根节点,故出栈,p指向它,p再向右侧走(因为在入栈时这个节点已经访问过了,不需要再访问)....以此类推
这样便实现了根左右的访问顺序
思考中序遍历,如何实现左根右的顺序?结合上面的思路,很容易发现,只要改动一处:指针向左移动时不访问,只入栈,访问完了左侧后后,栈顶是对应的根,然后出栈,此时再访问,然后想右侧走,便实现了中序遍历。对应代码:将cout语句放在p=s.top()后即可
接着思考一下后序遍历,如何实现左右根的顺序?借助前面的思路,先访问左子树容易实现,紧接着要访问右子树,最后再访问根。出栈,向右侧搜索,但一旦出栈向右,我们保存在栈中的根节点的信息就丢失了。问题在于:如何从左子树跳到右子树,而且跳到右子树后栈中仍然保存着根的信息?我们想到可以将每个节点入栈2次,节点的第一次出栈不访问,直接转向右子树(因为从左子树到右子树必须要用到根的信息,第一次的信息只是为了找到右子树),第二次出栈时,左右子树已经访问了,故再访问根。如何判断是第一次还是第二次?具体的代码我会在下一篇文章中贴出来。