在之前学习的二叉树遍历(前文传送门)当中,其时间复杂度均为 O ( 1 ) \mathcal{O}(1) O(1),而空间复杂度为 O ( n ) \mathcal{O}(n) O(n),其或是利用递归调用栈或是数据结构中的栈结构来完成对树中节点的多次访问,也就是利用了辅助空间来实现二叉树的遍历。
1. 实现思路
- 将当前所遍历到的节点记为 c u r cur cur;
- (1) 如果 c u r cur cur无左孩子,则 c u r cur cur迭代为其右子节点,即
cur = cur->right;
- (2) 如果 c u r cur cur有左孩子,则 c u r cur cur的左子树的最右叶子节点记为 m o s t R i g h t mostRight mostRight:
① 如果 m o s t R i g h t mostRight mostRight的右孩子为空,则将右孩子指针指向当前所遍历的节点(mostRight->right = cur;
),同时将当前所遍历的节点 c u r cur cur遍历为其左孩子(cur = cur->left;
);
② 如果 m o s t R i g h t mostRight mostRight的右孩子指向了当前所遍历的节点 c u r cur cur(mostRight->right == cur;
),将其右孩子指向空指针(mostRight->right = nullptr;
),同时将当前所遍历节点 c u r cur cur迭代为其右孩子节点(cur = cur->right;
)。
2. 代码实现
void MorrisTraversal(TreeNode* root) {
if(root == nullptr) return;
TreeNode *cur = root, *mostRight = nullptr;
while(cur != nullptr) {
mostRight = cur->left; //获取cur的左子树
//(1)若cur的左子树不为空
if(mostRight != nullptr) {
//寻找cur左子树的最右叶子节点
while(mostRight->right != nullptr && mostRight->right != cur)
mostRight = mostRight->right;
//①若是cur左子树的最右叶子节点为空,则将其指向cur,并将cur迭代至其左孩子节点
if(mostRight->right == nullptr) {
mostRight->right = cur;
cur = cur->left;
continue;
//②若是左子树的最右叶子节点不为空,则将叶子节点的右孩子指向空,cur迭代至其右孩子节点
} else {
mostRight->right = nullptr;
}
} //(2)若cur的左子树为空;或是cur的左子树不为空,但是左子树已经遍历完毕
cur = cur->right;
}
}
3. 分析推导
3.1 Morris遍历的规律和与递归栈方式的对比
对下图的二叉树进行分析,可以看出如下规律:
- (1) 左子树非空的节点会遍历两次:
① 左子树的最右叶子节点为空时,该节点第一次遍历;
② 左子树的最右叶子节点非空时,该节点第二次遍历; - (2) 左子树为空的节点只遍历一次。
结合递归方法的二叉树遍历方式(如下代码),进行分析:
void recursiveTraversal(TreeNode* root) {
if(root == nullptr) return;
//递归栈中的第一次遍历,此时进行输出打印则为前序遍历
//cout << root->val << ' ';
recursiveTraversal(root->left);
//递归栈中的第二次遍历,此时进行输出打印则为中序遍历
//cout << root->val << ' ';
recursiveTraversal(root->right);
//递归栈中的第三次遍历,此时进行输出打印则为后序遍历
//cout << root->val << ' ';
}
可以看到结合节点被遍历的次数,选择合适的输出打印时机,即可得到相应的遍历序列。
那么借鉴选择输出打印时机的这一思路,我们就可以较为便捷的实现 M o r r i s Morris Morris前序遍历和中序遍历了。
3.2 Morris前序遍历
在递归栈方式进行前序遍历的过程中,在首次遍历到节点的时候输出该节点的值,即可得到二叉树的先序遍历。
那么我们要寻找到何时是二叉树中节点第一次遍历,经过 3.1 3.1 3.1节的分析,我们发现:
(1) 对于左子树不为空的节点,第一次遍历时机是其左子树的最右叶子节点为空的时候;
(2) 对于左子树为空的节点,第一次遍历时机是 c u r cur cur在当前节点,且当前节点要迭代为 c u r cur cur的右孩子节点 的时候;
有了如上的分析,则可以实现 M o r r i s Morris Morris前序遍历,代码如下:
void MorrisPreTraversal(TreeNode* root) {
if(root == nullptr) return;
TreeNode *cur = root, *mostRight = nullptr;
while(cur != nullptr) {
mostRight = cur->left; //获取cur的左子树
//(1)若cur的左子树不为空
if(mostRight != nullptr) {
//寻找cur左子树的最右叶子节点
while(mostRight->right != nullptr && mostRight->right != cur)
mostRight = mostRight->right;
//①若是cur左子树的最右叶子节点为空,则将其指向cur,并将cur迭代至其左孩子节点
if(mostRight->right == nullptr) {
//★★★时机(1):左子树不为空的节点第一次遍历
cout << cur->val << ' ';
mostRight->right = cur;
cur = cur->left;
continue;
//②若是左子树的最右叶子节点不为空,则将叶子节点的右孩子指向空,cur迭代至其右孩子节点
} else {
mostRight->right = nullptr;
}
} else {
//★★★时机(2):左子树为空的节点第一次遍历
cout << cur->val << ' ';
}
//(2)若cur的左子树为空;或是cur的左子树不为空,但是左子树已经遍历完毕
cur = cur->right;
}
cout << endl;
}
3.3 Morris中序遍历
中序遍历和前序遍历的分析,其实就是个套娃过程,对于中序遍历,就是在第二次遍历节点的时机进行打印输出。而 3.1 3.1 3.1节的分析中说了,对于左子树为空的节点只有一次遍历,思考一下,中序遍历的顺序是左中右,对于没有左子树的节点而言,其中序遍历和中右别无二样。所以其第一次遍历即可认为是第二次遍历。
那么可以得到第二次遍历的时机如下:
(1) 对于左子树不为空的节点,第二次遍历时机是其左子树的最右叶子节点不为空的时候;
(2) 对于左子树为空的节点,第二次遍历时机是 c u r cur cur在当前节点,且当前节点要迭代为 c u r cur cur的右孩子节点 的时候;
代码如下:
void MorrisInTraversal(TreeNode* root) {
if(root == nullptr) return;
TreeNode *cur = root, *mostRight = nullptr;
while(cur != nullptr) {
mostRight = cur->left; //获取cur的左子树
//(1)若cur的左子树不为空
if(mostRight != nullptr) {
//寻找cur左子树的最右叶子节点
while(mostRight->right != nullptr && mostRight->right != cur)
mostRight = mostRight->right;
//①若是cur左子树的最右叶子节点为空,则将其指向cur,并将cur迭代至其左孩子节点
if(mostRight->right == nullptr) {
mostRight->right = cur;
cur = cur->left;
continue;
//②若是左子树的最右叶子节点不为空,则将叶子节点的右孩子指向空,cur迭代至其右孩子节点
} else {
mostRight->right = nullptr;
}
} else {
//★★★时机(1)&(2):左子树的最右叶子节点非空 或 左子树为空的节点第二(一)次遍历
cout << cur->val << ' ';
}
//(2)若cur的左子树为空;或是cur的左子树不为空,但是左子树已经遍历完毕
cur = cur->right;
}
cout << endl;
}
3.4 Morris后序遍历
对于后序遍历,就无法使用套娃了,因为递归栈方法中的第三次遍历根本就没有啊!!!思路是,使用左子树非空的节点的第二次遍历这个时间节点,①此时逆序输出其左子树的所有右边界边节点,②最后输出整棵二叉树的右边界边界点,至于为啥是可以这么来搞,确实得感叹青出于蓝而胜于蓝(据说Morris只在论文中给出了前序和中序的遍历方法,后序遍历是后来人搞出来的,只能大喊:真牛批!!!)
画图分析一下,会发现确实是这么回事:
就是这么神奇,同时注意到 M o r r i s Morris Morris算法实现是空间复杂度为 O ( 1 ) \mathcal{O}(1) O(1)的,所以如何实现不使用辅助空间的限制下逆序打印左子树右边界这一要求呢,哦对了,就是使用链表反转的方法,这里的实现没有进行树的还原,其实,在使用中再调用一次该函数即可实现二叉树的还原。代码如下:
TreeNode* reverseRightBdry(TreeNode* from) {
if(from == nullptr || from->right == nullptr) return from;
TreeNode *pre = nullptr, *next = nullptr;
while(from != nullptr) {
next = from->right; //next节点指向当前节点的右孩子节点
from->right = pre; //from的右孩子指针指向其父节点
//节点前移
pre = from;
from = next;
}
return pre;
}
那么有了逆序打印的功能实现,就可以完成 M o r r i s Morris Morris后序遍历的过程了,其中链表的反转过程只增加了时间复杂度n的常数系数,所以时间复杂度仍为 O ( n ) \mathcal{O}(n) O(n),代码如下:
void MorrisPosTraversal(TreeNode* root) {
if(root == nullptr) return;
TreeNode *cur = root, *mostRight = nullptr;
while(cur != nullptr) {
mostRight = cur->left; //获取cur的左子树
//(1)若cur的左子树不为空
if(mostRight != nullptr) {
//寻找cur左子树的最右叶子节点
while(mostRight->right != nullptr && mostRight->right != cur)
mostRight = mostRight->right;
//①若是cur左子树的最右叶子节点为空,则将其指向cur,并将cur迭代至其左孩子节点
if(mostRight->right == nullptr) {
mostRight->right = cur;
cur = cur->left;
continue;
//②若是左子树的最右叶子节点不为空,则将叶子节点的右孩子指向空,cur迭代至其右孩子节点
} else {
//★★★时机:左子树非空的节点的第二次遍历的时间节点
printRightBdry(cur->left);
mostRight->right = nullptr;
}
}
//(2)若cur的左子树为空;或是cur的左子树不为空,但是左子树已经遍历完毕
cur = cur->right;
}
printRightBdry(root);//★★★时机:逆序输出整棵二叉树的右边界节点
cout << endl;
}
void printRightBdry(TreeNode* head) {
TreeNode* tail = reverseRightBdry(head); //对二叉树左子树右边界进行反转
TreeNode* cur = tail;
while(cur != nullptr) {
cout << cur->val << ' ';
cur = cur->next;
}
reverseRightBdry(tail); //对二叉树进行还原
}