二叉树遍历之三(Moriis traversal)

 二叉树的Morris traversal是个很值得学习的算法,也是此系列重点想要记叙的一个算法。Morris traversal的一个亮点在于它是O(1)空间复杂度的。前面的递归和迭代都是需要O(n)空间复杂度的。那么这个O(1)空间复杂度怎样做到?这个借鉴了些线索二叉树的相关原理,Morris traversal通过在遍历时修改叶子节点的右孩子指针达到了回退到根节点的目的。Morris其实在原始论文中只给出了中序遍历的相关代码,但是依据这个思路,通过修改相关步骤,便可得到前序和后序的Morris traversal实现。以下将以前序,中序,后序的Morris traversal步骤来分别细述Morris traversal的具体实现。

中序遍历

 在中序遍历中,对于一个节点,我们都是处理它的左子树,再处理它,再处理它的右子树。一个节点的前驱节点就是它的左子树的最右节点(这里先假定所有节点都是有左孩子的,下段同)。

对于一个节点np,定义它的前驱节点为pre。显然,在遍历完np的左子树后还需要回到np。并且,依据中序遍历的定义,任意节点的前驱节点的右孩子必然是为空的。那么,这个前驱节点的右孩子指针便可以为之所用。在此可以让前驱节点的右孩子指针指向当前节点。这样,在遍历完左子树后便可以据这个指针再次返回到节点np.

那么当通过右孩子指针进入一个节点时,怎么区分它究竟是通过自己的父节点访问到的,还是通过自己的左子树最右节点(即前驱节点)访问到的呢?这个就很简单了,直接找到它的左子树最右节点,若通过这样的查找,再次访问到了该节点。不言而喻,该节点是通过自己的左子树最右节点访问到的。那么,就只需要输出它,再依据同样的步骤处理它的右指针即可。

前面假定了所有节点都是有左孩子的。那么对于没有左孩子的节点呢?这个更简单了!那它一定是被第一次访问到,这时只需要输出它并且,依据前面步骤处理它的右孩子节点即可。

这样,依据以上分析,很容易就可以写出Morris traversal中序遍历的具体步骤(下以伪码表示):

```
s1.if(!node) return;
s2.if(!node->left)
{put(node),node=node->right;goto s1;}
s3. findlrightmost(node->left)
s4. if(rightmost‘s right child==node)
{put(node),node=node->right;goto s1}
s5. {rightmost's right child=node,node=node->left,goto s1;}
```

依据以上分析,可以很轻松将如上伪码转换为c++代码如下:

vector<int> inorderTraversal(TreeNode* root) {
TreeNode *cur=root;
vector<int> res;
while(cur) {
if(!cur->left) { //must first meet this node
res.push_back(cur->val);
cur=cur->right;
}
else {
TreeNode *pre=cur->left;
//find right most
while(pre->right && pre->right!=cur)
pre=pre->right;
if(pre->right) {
pre->right=NULL;
res.push_back(cur->val);
cur=cur->right;
}
else {
pre->right=cur;
cur=cur->left;
}
}
}
return res;
}

前序遍历

前序遍历和中序遍历是很相似的。前序遍历和中序遍历的Morris traversal唯一不同在于,前序在遇到一个节点时,若它是第一次遇到,则输出,否则则不输出。容易看出,改一下输出语句的位置即可将其变成前序遍历。

s1.if(!node) return;
s2.if(!node->left)
{put(node),node=node->right;goto s1;}
s3. findlrightmost(node->left)
s4. if(rightmost‘s right child==node)
{node=node->right;goto s1}//删掉了put语句
s5.{put(node),rightmost's right child=node,node=node->left,goto s1;}//添加了put语句

具体代码如下:

vector<int> preorderTraversal(TreeNode* root) {
TreeNode *cur=root;
vector<int> res;
while(cur) {
if(!cur->left) { //must first meet this node
res.push_back(cur->val);
cur=cur->right;
}
else {
TreeNode *pre=cur->left;
while(pre->right && pre->right!=cur)
pre=pre->right;
if(pre->right) {
pre->right=NULL;
cur=cur->right;
}
else {
res.push_back(cur->val);
pre->right=cur;
cur=cur->left;
}
}
}
return res;
}

后序遍历

相比而言,后序遍历的思路就显得要复杂一些,也要多加一些操作。

依据前面的步骤,可以看到,不论是中序遍历还是前序遍历,对于中间节点的操作都是"用完即丟"的。即,当程序遍历完一个节点的左子树后,该节点的地址就会被我们丢弃不管。我们只需专心再处理它的右子树即可。然而,这在后序遍历中很明显是行不通的。后序遍历中,我们遍历完右子树后,才输出该中间节点。这样,怎样从右子树回到中间节点处就成了一个十分棘手的问题。而且,不像中序遍历,在后序遍历中,节点的输出是层层往上跳的。一个节点的前驱节点,为其自己的左孩子节点或右孩子。

这样,用其他地方来保存中间节点地址似乎也都不现实。所以,我们还是考虑继续使用leftchild's rightmost的right child指针来保存中间节点。

仔细看后序遍历的数字输出规律,层层往上跳这个规律似乎对我们很有利。很容易发现,在右孩子输出时,其输出顺序近似一层直线,我们输出该右孩子节点,接着又输出了该右孩子的父节点,再输出其父节点。那么,我们可以试着在碰到对于右孩子节点本身统统先不做输出。对于右孩子节点,我们可以在输出所有左孩子之后,从该子树的根节点开始逆序输出其到右孩子的路径即可。

而由于我们处理完后返回到的是中间节点,那么我们可以看做当返回中间节点后,其左孩子的所有左孩子节点都已处理完毕。接着,我们处理其左孩子节点及左子树的所有右孩子节点即可。如对于以下的树,当返回1时,我们据右孩子指针逆序输出5,2即可。

但这样,又引入了一个新的问题,root并不为任何节点的左孩子节点。解决办法很简单,我们引入一个节点,构造一棵左孩子为root的单边树,然后以该节点为root,从该节点开始处理。

这样我们就可以将所有节点都转换为这种情况,因为每个节点都必为某个节点的左子树的右孩子。这样,我们在每次访问一个节点时,若是第一次访问它,我们就去处理它的左孩子,如果是返回到了它,那么我们就该去处理它的左孩子的所有节点再接着去处理它的右孩子。若左孩子为空,那我们直接去处理它的右孩子即可。

依据以上分析,我们可以写出如下执行步骤(伪码表示)

s0.先构造以root为左孩子的单变树,以该数根为root
s1.if(!node) return;
s2.if(!node->left)
{node=node->right;goto s1}
s3.findrightmost(node->left)
s4. if(rightmost==node)
{reverse_put(node->left,rightmost);node=node->right;goto s1}
s5.{rightmost=node;node=node->left;goto s1}

至于逆序输出,我们可以把根节点到叶子节点的单路径先调转一遍,然后输出。在输出后,再调转回来。

将以上思路写作代码如下:

vector<int> postorderTraversal(TreeNode* root) {
vector<int> res;
TreeNode head(0),*cur=&head;
head.left=root;
while(cur) {
if(cur->left) {
TreeNode *pre=cur->left;
while(pre->right && pre->right!=cur)
pre=pre->right;
if(!pre->right) {
pre->right=cur;
cur=cur->left;
}
else {
reverse_node(cur->left,pre,res);
pre->right=NULL;
cur=cur->right;
}
}
else
cur=cur->right;
}
return res;
} void reverse(TreeNode *cur,TreeNode *pre) {
if(cur==pre)
return;
TreeNode *follow=cur->right,*last=cur;
do {
TreeNode *temp=follow;
follow=follow->right;
temp->right=last;
last=temp;
} while(last!=pre);
}
void reverse_node(TreeNode *cur,TreeNode *pre,vector<int> &res) {
reverse(cur,pre);
TreeNode *p=pre;
while(cur!=p) {
res.push_back(p->val);
p=p->right;
}
res.push_back(p->val);
reverse(pre,cur);
}
上一篇:android 获取适配的bitmap等相关


下一篇:[刷题]算法竞赛入门经典(第2版) 5-7/UVa12100 - Printer Queue