对于二叉树遍历,现有的很多算法大多基于栈,递归或者迭代。下面是中序遍历的迭代版本:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
stack<TreeNode*> sta;
vector<int> ans;
TreeNode* cur = root;
while(1){
while(cur != NULL)
sta.push(cur), cur = cur->left;
if(sta.empty())
break;
cur = sta.top();
sta.pop();
ans.push_back(cur->val);
cur = cur->right;
}
return ans;
}
};
其实,栈的作用就是保存父节点,否则,回溯到父节点并继续扩展其右子树就无法进行。
Morris traversal是一种不使用栈的二叉树遍历算法,主要思想是基于线索二叉树(右子树为空的节点指向中序遍历中该节点的直接后继),并且在过程中对其进行restore,使得树的结构不变。
1968年,Knuth提出这样一个问题:设计一个不使用stack和tag的二叉树中序遍历算法,并在遍历结束后保持二叉树的结构不变。1979年,Morris在其论文“Traversing binary trees simply and cheaply”中提出此算法,对这个问题给出了一个优雅的解法。Morris算法没有使用栈和tag,算法中使用的树其实是一棵线索二叉树,使节点左子树中的最右节点指向该节点(中序序列中,左子树最右节点为左子树最大值,是该节点的直接前驱),则处理完左子树最右节点后,可以回到该节点。
算法伪代码如下:
1. Initialize current as root
2. While current is not NULL
If current does not have left child
a) Print current’s data
b) Go to the right, i.e., current = current->right
Else
a) Make current as right child of the rightmost node in current's left subtree
b) Go to this left child, i.e., current = current->left
其中current node表示当前待扩展的节点,中序遍历要保持左儿子—父节点—右儿子的顺序,所以当且仅当左子树为空时,才能打印该节点,继续扩展其右子树。否则,由于要先扩展左子树,所以需要保存父节点current的信息。保存方法上面已经提到过:找到current的左子树中最右节点(中序序列中current的直接前驱),将这个rightmost node的右儿子指向current。父节点的信息保存完毕,可以拓展其左儿子了,current = current->left。
以上将树修改成了线索树,何时恢复?左子树何时遍历完毕呢?由于将rightmost node指向了其后继节点current,所以对企图对rightmost node的右儿子进行扩展时,由于current有左子树(否则不会存在rightmost node),所以算法会重复寻找current的直接前驱(rightmost node),碰巧这个直接前驱又已经指向了它自己,说明current的左子树扩展完毕,可以打印current,并继续扩展其右子树。
加上restore的伪代码如下:
1. Initialize current as root
2. while current is not NULL
If current does not have left child
a) Print current’s data
b) Go to the right, i.e., current = current->right
Else
Go to the rightmost node in current's left subtree, as pre(pre does not have right child or pre's right child is current)
If pre does not have right child
(a) Make current as right child of pre, i.e, pre->right = current
(b) Go to the left, i.e,current = current->left
Else
(a) restore pre's right child as NULL
(b) Print current's data
(c) Go to the right, i.e, current = current->right
完整代码如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
TreeNode* cur = root;
while(cur != NULL){
if(cur->left == NULL)
ans.push_back(cur->val), cur = cur->right;
else{
TreeNode* pre = cur->left;
while(pre->right != NULL && pre->right != cur)
pre = pre->right;
if(pre->right == NULL)
pre->right = cur, cur = cur->left;
else{
pre->right = NULL;
ans.push_back(cur->val);
cur = cur->right;
}
}
}
return ans;
}
};
对于先序遍历,因为先序遍历是先打印父节点,左子树为空时先序和中序是一样的,直接打印父节点,所以只需增加一个打印语句即可——Morris算法是保存父节点指针后,再开始扩展左子树。所以在找到rightmost node之后,增加一个打印父节点的动作即可。代码如下:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> ans;
TreeNode* cur = root;
while(cur != NULL){
if(cur->left == NULL)
ans.push_back(cur->val), cur = cur->right;
else{
TreeNode* pre = cur->left;
while(pre->right != NULL && pre->right != cur)
pre = pre->right;
if(pre->right == NULL)
ans.push_back(cur->val), pre->right = cur, cur = cur->left;
else{
pre->right = NULL;
cur = cur->right;
}
}
}
return ans;
}
};