200203题(二叉树的Morris遍历算法)
作者:互联网
法1:中序遍历
//如果对没有错误的二叉树进行中序遍历,应该是按升序排列的
//那如果对两个结点交换了顺序,那一定有两个地方不满足“前一个元素 < 当前元素 < 后一个元素”
class Solution {
private:
TreeNode* first = NULL;
TreeNode* second = NULL;
TreeNode* pre = new TreeNode(INT_MIN);//初始化
void DFS(TreeNode* root) {
if (root == NULL) {
return;
}
//中序遍历依次找出first和second
DFS(root->left);
if (first == NULL&&pre->val > root->val) {
first = pre;
}
if (first != NULL&&pre->val > root->val) {
second = root;
}
pre = root;//更新pre
DFS(root->right);
}
public:
void recoverTree(TreeNode* root) {
DFS(root);
swap(first->val, second->val);
}
};
上面的实现中,有函数的递归调用,递归的深度等于二叉树的高度,也就是说递归导致的调用堆栈的高度等于二叉树的高度h,这样的话,程序虽然没有显式地通过new 来分配内存,但实际上消耗的内存大小也是 O(h). 如果二叉树的高度很大,那么按照传统的中序遍历,需要消耗大量的内存。
接下来引入的Morris遍历法,能以O(1)的空间复杂度实现二叉树的中序遍历。例如给定下面二叉树:
显然采用中序遍历的话,结果如下:
1,2,3,4,5,6,7,8,9,10
给定某个结点,在中序遍历中,直接排在它前面的结点,我们称之为该节点的前序结点,例如结点5的前序结点就是4,同理,结点10的前序结点就是9.
在二叉树中如何查找一个结点的前序结点呢?
如果该结点有左孩子,那么从左孩子开始,沿着右孩子指针一直向右走到底,得到的结点就是它的前序结点,例如结点6的左孩子是4,沿着结点4的右指针走到底,那就是结点5,结点9的左孩子是7,沿着它的右指针走到底对应的结点就是8.如果左孩子的右结点指针是空,那么左孩子就是当前结点的前序结点。
如果当前结点没有左孩子,并且它是其父结点的右孩子,那么它的前序结点就是它的父结点,例如8的前序结点是7,10的前序结点是9.
如果当前结点没有左孩子,并且它是父结点的左孩子,那么它没有前序结点,并且它自己就是首结点,例如结点1.
Morris遍历算法的步骤如下:
1, 根据当前结点,找到其前序结点,如果前序结点的右孩子是空,那么把前序结点的右孩子指向当前结点,然后进入当前结点的左孩子。
2, 如果当前结点的左孩子为空,打印当前结点,然后进入右孩子。
3,如果当前结点的前序结点其右孩子指向了它本身,那么把前序结点的右孩子设置为空,打印当前结点,然后进入右孩子。
我们以上面的例子走一遍。首先访问的是根结点6,得到它的前序结点是5,此时结点5的右孩子是空,所以把结点5的右指针指向结点6:
进入左孩子,也就到了结点4,此时结点3的前序结点3,右孩子指针是空,于是结点3的右孩子指针指向结点4,然后进入左孩子,也就是结点2;
此时结点2的左孩子1没有右孩子,因此1就是2的前序结点,并且结点1的右孩子指针为空,于是把1的右孩子指针指向结点2,然后从结点2进入结点1:
此时结点1没有左孩子,因此打印它自己的值,然后进入右孩子,于是回到结点2.根据算法步骤,结点2再次找到它的前序结点1,发现前序结点1的右指针已经指向它自己了,所以打印它自己的值,同时把前序结点的右孩子指针设置为空,同时进入右孩子,也就是结点3.于是图形变为:
此时结点3没有左孩子,因此打印它自己的值,然后进入它的右孩子,也就是结点4. 到了结点4后,根据算法步骤,结点4先获得它的前序结点,也就是结点3,发现结点3的右孩子结点已经指向自己了,所以打印它自己的值,也就是4,然后把前序结点的右指针设置为空,于是图形变成:
接着从结点4进入右孩子,也就是结点5,此时结点5没有左孩子,所以直接打印它本身的值,然后进入右孩子,也就是结点6,根据算法步骤,结点6获得它的前序结点5,发现前序结点的右指针已经指向了自己,于是就打印自己的值,把前序结点的右指针设置为空,然后进入右孩子。
接下来的流程跟上面一样,就不再重复了。
MorrisTraval函数做的就是前面描述的算法步骤,在while循环中,进入一个结点时,先判断结点是否有左孩子,没有的话就把结点值打印出来,有的话,先获得前序结点,然后判断前序结点的右孩子指针是否指向自己,是的话把自己的值打印出来,进入右孩子,前序孩子的右孩子指针是空的话,就把右孩子指针指向自己,然后进入左孩子。
Morris遍历,由于要把前序结点的右指针指向自己,所以暂时会改变二叉树的结构,但在从前序结点返回到自身时,算法会把前序结点的右指针重新设置为空,所以二叉树在结构改变后,又会更改回来。
在遍历过程中,每个结点最多会被访问两次,一次是从父结点到当前结点,第二次是从前序结点的右孩子指针返回当前结点,所以Morris遍历算法的复杂度是O(n)。在遍历过程中,没有申请新内存,因此算法的空间复杂度是O(1).
自己写的完整代码如下:
#include<iostream>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
//如果对没有错误的二叉树进行中序遍历,应该是按升序排列的
//那如果对两个结点交换了顺序,那一定有两个地方不满足“前一个元素 < 当前元素 < 后一个元素”
class Solution {
private:
TreeNode* pre_node = new TreeNode(INT_MIN);
TreeNode* first = NULL;
TreeNode* second = NULL;
public:
TreeNode* getPre(TreeNode* root) {//如果该结点有左孩子,那么从左孩子开始,沿着右孩子指针一直向右走到底,得到的结点就是它的前序结点
TreeNode*pre = root;
if (root->left != NULL)
{
pre = pre->left;
while (pre->right != NULL&&pre->right != root)//注意这里要加上pre->right != root
{
pre = pre->right;
}
}
return pre;
}
void MorrisTraval(TreeNode* root) {
TreeNode* cur = root;
while (cur != NULL) {
if (cur->left == NULL) {
// cout << cur->val << endl;
if (first == NULL&&pre_node->val > cur->val) {
first = pre_node;
}
if (first != NULL&&pre_node->val > cur->val) {
second = cur;
}
pre_node = cur;
//
cur = cur->right;
}
else //注意,只在当前结点有左孩子的情况下才找前序结点
{
TreeNode* pre = getPre(cur);
if (pre->right == NULL)
{
pre->right = cur;
cur = cur->left;
}
else if (pre->right == cur) {
pre->right = NULL;
// cout << cur->val << endl;
if (first == NULL&&pre_node->val > cur->val) {
first = pre_node;
}
if (first != NULL&&pre_node->val > cur->val) {
second = cur;
}
pre_node = cur;
//
cur = cur->right;
}
}
}
}
void recoverTree(TreeNode* root) {
if (root == NULL)return;
MorrisTraval(root);
swap(first->val, second->val);
}
};
参考文献:https://www.jianshu.com/p/484f587c967c
ShenHang_ 发布了160 篇原创文章 · 获赞 3 · 访问量 5669 私信 关注标签:pre,结点,TreeNode,cur,孩子,前序,200203,Morris,二叉树 来源: https://blog.csdn.net/ShenHang_/article/details/104131317