Morris方法遍历二叉树

本文介绍了一种称为Morris遍历的算法,该算法可在O(1)空间复杂度下实现二叉树的前序、中序及后序遍历。与传统递归或栈迭代方法相比,Morris遍历不依赖额外空间,通过巧妙利用二叉树的空闲指针实现了遍历过程。文章详细解释了遍历的步骤,并提供了具体的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Morris 方法遍历二叉树(非递归,不用栈,O(1)空间)

本文主要解决一个问题,如何实现二叉树的前中后序遍历,有两个要求:

1. O(1)空间复杂度,即只能使用常数空间;

2. 二叉树的形状不能被破坏(中间过程允许改变其形状)。

通常,实现二叉树的前序(preorder)、中序(inorder)、后序(postorder)遍历有两个常用的方法:一是递归(recursive),二是使用栈实现的迭代版本(stack+iterative)。这两种方法都是O(n)的空间复杂度(递归本身占用stack空间或者用户自定义的stack),所以不满足要求。(用这两种方法实现的中序遍历实现可以参考这里。)

Morris Traversal方法可以做到这两点,与前两种方法的不同在于该方法只需要O(1)空间,而且同样可以在O(n)时间内完成。

要使用O(1)空间进行遍历,最大的难点在于,遍历到子节点的时候怎样重新返回到父节点(假设节点中没有指向父节点的p指针),由于不能用栈作为辅助空间。为了解决这个问题,Morris方法用到了线索二叉树(threaded binary tree)的概念。在Morris方法中不需要为每个节点额外分配指针指向其前驱(predecessor)和后继节点(successor),只需要利用叶子节点中的左右空指针指向某种顺序遍历下的前驱节点或后继节点就可以了。

Morris只提供了中序遍历的方法,在中序遍历的基础上稍加修改可以实现前序,而后续就要再费点心思了。所以先从中序开始介绍。

首先定义在这篇文章中使用的二叉树节点结构,即由data,lchild和rchild组成:

template<class T>
class TNode
{
public:
	TNode() { lchild = rchild = parent = nullptr; };
	T data;
	TNode<T>* lchild, *rchild, *parent;
};
一、中序遍历

步骤:

1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。

2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。

   a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。

   b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子。

3. 重复以上1、2直到当前节点为空。

图示:

下图为每一步迭代的结果(从左至右,从上到下),cur代表当前节点,深色节点表示该节点已输出。

(以下图为本人转载)

代码:

template<class T>
void TriTree<T>::MorrisInOrder(TNode<T>* bt)
{
printf("Morris 中序遍历:");
if (nullptr == bt)
{
return;
}
TNode<T>* pCur = bt;
TNode<T>* pPre = nullptr;
while (nullptr != pCur)
{
if (nullptr == pCur->lchild)
{
Visited(pCur);
pCur = pCur->rchild;
}
else
{
pPre = pCur->lchild;
while (nullptr != pPre->rchild&&pCur!=pPre->rchild)
//pCur!=pPre->rchild很容易写掉 会出现 死循环
{
pPre = pPre->rchild;
}
if (nullptr == pPre->rchild)
{
pPre->rchild = pCur;//1
pCur = pCur->lchild;
}
else if(pCur==pPre->rchild)
{
pPre->rchild = nullptr;
Visited(pCur);
pCur = pCur->rchild;
}
}
}
puts("");
}

复杂度分析:

空间复杂度:O(1),因为只用了两个辅助指针。

时间复杂度:O(n)。证明时间复杂度为O(n),二叉树中每个节点最多经历一次步骤二中的a和步骤二中的b过程,二叉树节点数为n,则时间复杂度为O(n);

二、前序遍历

前序遍历与中序遍历相似,代码上只有一行不同,不同就在于输出的顺序。

步骤:

1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。

2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。

   a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子。

   b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子。

3. 重复以上1、2直到当前节点为空。

图示:

代码:

复制代码
 1 void preorderMorrisTraversal(TreeNode *root) {
 2     TreeNode *cur = root, *prev = NULL;
 3     while (cur != NULL)
 4     {
 5         if (cur->left == NULL)
 6         {
 7             printf("%d ", cur->val);
 8             cur = cur->right;
 9         }
10         else
11         {
12             prev = cur->left;
13             while (prev->right != NULL && prev->right != cur)
14                 prev = prev->right;
15 
16             if (prev->right == NULL)
17             {
18                 printf("%d ", cur->val);  // the only difference with inorder-traversal
19                 prev->right = cur;
20                 cur = cur->left;
21             }
22             else
23             {
24                 prev->right = NULL;
25                 cur = cur->right;
26             }
27         }
28     }
29 }
复制代码

复杂度分析:

时间复杂度与空间复杂度都与中序遍历时的情况相同。

三、后序遍历

后续遍历稍显复杂,需要建立一个临时节点dump,令其左孩子是root。并且还需要一个子过程,就是倒序输出某两个节点之间路径上的各个节点。

步骤:

当前节点设置为临时节点dump。

1. 如果当前节点的左孩子为空,则将其右孩子作为当前节点。

2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。

   a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。

   b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。倒序输出从当前节点的左孩子到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右孩子。

3. 重复以上1、2直到当前节点为空。

图示:

代码:

复制代码
template<class T>
void TriTree<T>::Reserve(TNode<T>* Begin, TNode<T>* End)
{
	//在后序遍历中 逆序相当于环形链表 (注意当有指向父亲节点的指针时 可能相当于环形双链表)
	//是循环链表 还是 非循环链表 关键是输出是放在断开之前 还是之后
	//但是在输出后会再次逆序 所以不用理会parent指针的指向
	if (nullptr == Begin || nullptr == End||nullptr==Begin->rchild)
	{
		return;
	}
	TNode<T>* p1 = Begin;
	TNode<T>* p2 = Begin->rchild;
	TNode<T>* p3 = p2->rchild;
	while (true)
	{
		p2->rchild = p1;
		if (nullptr == p3 || Begin == p3)
		{
			break;
		}
		p1 = p2;
		p2 = p3;
		p3 = p3->rchild;
	}
}


template<class T>
void TriTree<T>::PrintReserve(TNode<T>* Begin, TNode<T>* End)
{
	if (nullptr == Begin || nullptr == End)
	{
		return;
	}
	Reserve(Begin,End);
	TNode<T>* pTemp = End;
	while (nullptr != pTemp)
	{
		Visited(pTemp);
		//system("pause");
		pTemp = pTemp->rchild;
		if (nullptr == pTemp)
		{
			break;
		}
		//这个条件也是很重要 调用者 可能断开前驱结点右孩子 指针 也可能未断开
		if (Begin == pTemp)
		{
			Visited(pTemp);
			break;
		}
	}
	Reserve(End, Begin);
}
 
   
template<class T>
void TriTree<T>::MorrisPostOrder(TNode<T>* bt)
{
	printf("Morris 后序遍历:");
	if (nullptr == bt)
	{
		return;
	}
	TNode<T> dump;
	dump.lchild = bt;
	TNode<T>* pCur = &dump;
	TNode<T>* pPre = nullptr;
	while (nullptr != pCur)
	{
		if (nullptr == pCur->lchild)
		{
			pCur = pCur->rchild;
		}
		else
		{
			pPre = pCur->lchild;
			while (nullptr != pPre->rchild&&pPre->rchild!=pCur)
			{
				pPre = pPre->rchild;
			}
			if (nullptr == pPre->rchild)
			{
				pPre->rchild = pCur;
				pCur = pCur->lchild;
			}
			else if (pCur == pPre->rchild)
			{
				pPre->rchild = nullptr;
				PrintReserve(pCur->lchild,pPre);
				pCur = pCur->rchild;
			}
		}
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值