常用的二叉树遍历主要分为深度优先遍历(dfs)和广度优先遍历(bfs),其中dfs又有前序、中序、后序遍历之分。然而不管你用迭代还是递归的方法实现,它们的空间复杂度都为O(N)。而本文介绍的Morris算法,只需O(1)的空间复杂度,本质上是使用时间换取空间的一种方法。
假设我们要遍历如下二叉树:
如果采用中序遍历,那么遍历顺序应该为1 2 3 4 5 6 7 8 9 10,我们定义一个cur表示当前节点,首先我们需要最遍历的是节点1,但是当前cur在根节点6,所以需要向左子树遍历。但是这会产生一个问题,就是如果我们不想使用太多的空间,而是直接使用cur=cur->left记录当前节点,那么在我们向左子树遍历的时候,原来的节点6就会丢失,在遍历完左子树之后将无法返回节点6。这里的解决方法就是,找到6的前驱节点,这里为5,将5的右节点指向6,这样在遍历6节点的左子树时,6节点也将会被记录,最后通过节点5重新遍历到节点6。
同样的,在我们遍历4 2节点的左子树时,我们也将其前驱节点3 1的右子树指向它们,这样在遍历完它们的左子树之后就能回到当前节点。如下图:
回到当前节点之后只需将其前驱节点右子树记录的当前节点重新置为NULL即可。
Morris遍历算法的步骤如下:
1, 根据当前节点,找到其前驱节点,如果前序节点的右孩子是空,那么把前序节点的右孩子指向当前节点,然后进入当前节点的左孩子。
2, 如果当前节点的左孩子为空,打印当前节点,然后进入右孩子。
3,如果当前节点的前序节点其右孩子指向了它本身,那么把前序节点的右孩子设置为空,打印当前节点,然后进入右孩子。
c++代码如下(中序遍历):
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
vector<int> inorderTraversal(TreeNode* root) {
vector<int>v;
TreeNode* cur = root;
while (cur) {
if (cur->left == NULL) {// 左子树为空时,直接比较,然后进入右子树
v.push_back(cur->val);
cur = cur->right;
}
else {// 进入左子树
/* 找cur的前驱结点,找到后分两种情况
/* 1、cur的左子结点没有右子结点,那cur的左子结点就是前驱
/* 2、cur的左子结点有右子结点,就一路向右下,走到底就是cur的前驱*/
TreeNode* preceesor = cur->left;
while (preceesor->right && preceesor->right != cur) {
preceesor = preceesor->right;
}
// 前驱已经指向自己了,说明以及遍历过左子树了,进入右子树
if (preceesor->right == cur) {
v.push_back(cur->val);
preceesor->right = NULL; // 断开连接,恢复原树
cur = cur->right;
}
else { // 前驱还没有指向自己,说明左边还没有遍历,将前驱的右指针指向自己,后进入左子树
preceesor->right = cur;
cur = cur->left;
}
}
}
return v;
}
知道了Morris的中序遍历之后,它的前序后续版本也就很简单了。
如果是前序遍历,还是先找当前节点的前驱节点,然后再将前驱节点的右子树指向当前节点的后继节点即可。
如果是后续遍历,我们还是可以按照前序遍历的方法。我们知道,后序遍历顺序为左右中,前序遍历顺序为中左右,我们可以将前序遍历的左右交换一下,按照中右左的顺序对树进行遍历,之后再进行反转操作,即可得到左右中的后序遍历顺序。我们只是增加了一个反转操作,时间复杂度仍然为O(N)。