二叉树的前中后序遍历(非递归)的多种方式

前序迭代遍历

前序遍历是先根后左再右,第一次接触到某个节点的时候就立即访问。

刚开始的时候我们能接触到的只有根节点,然后我们将其值输出,然后访问它的左子女,因为我们对于一个节点只访问一次,所以我们访问完根节点之后就把根节点从栈中删除了,此时如果我们之前没有保存根节点的右子女,那么根节点右子女的信息变消失了,无法去访问,因为我们使用的是栈,后存入的先弹出,如果我们先存入左子女后存入右子女,会造成我们先访问的右子女的孩子,这就导致顺序发生了错误,所以我们应该先存入右子女,后存入左子女,下面是相应的代码。

void preorder(TreeNode* root)
{
	if(root == NULL)return;
	stack<TreeNode*> stk;
	stk.push(root);
	while(!stk.empty())
	{
		TreeNode * top = stk.top();
		stk.pop();
		cout<<top->val<<endl;
		if(top->right)
			stk.push(top->right);
		if(top->left)
			stk.push(top->left);
	}
}

Morris Traversal(线索二叉树的前序遍历方式)

线索二叉树和一般的二叉树的不同点在于,普通二叉树的叶节点的左右子女都为null,但是线索二叉树的叶节点的左右子女分别指向了前(中)(后)序的前驱和后序节点,或者只存储了后续结点的指针。一般来说,我们得到的二叉树都是普通二叉树,所以我们想要使用线索二叉树就需要自己来构造。
使用线索二叉树的前序遍历方式的具体算法是
1、当前节点的左子女为空,那么输出当前结点,当前节点改为指向其右子女。
2、当前节点的左子女不为空,那么我们就往左子女的方向前进,首先构建线索二叉树,我们找到当前节点的左子女的最右子女。这个节点是当前节点的前驱节点。如果这个节点的右孩子为空,说明我们还没有将此节点的右节点连到它的后继节点上,这也说明了我们现在是第一次遇到当前节点,于是输出当前节点,并且将此节点的右子女指向当前节点,当前节点改为指向其左子女;如果当前节点的左子女的最右子女的右子女是当前节点,说明我们不是第一次碰到当前节点,则说明当前节点的左子树上的所有节点全部走完,那么直接将当前节点指向其右子女。
事实上,前序中序后序的遍历树的方式可以是一样的,只是输出的顺序需要做一些调整,观察Morris Traversal的前中后序的代码可以发现此结论。

void preorderMorrisTraversal(TreeNode *root)
{
	TreeNode *cur = root, *prev = NULL;
	while (cur != NULL)
	{
		if (cur->left == NULL)
        {
            printf("%d ", cur->val);
            cur = cur->right;
        }
        else
        {
            prev = cur->left;
            while (prev->right != NULL && prev->right != cur)
                prev = prev->right;

            if (prev->right == NULL)
            {
                printf("%d ", cur->val);  // the only difference with inorder-traversal
                prev->right = cur;
                cur = cur->left;
            }
            else
            {
                prev->right = NULL;
                cur = cur->right;
            }
        }
    }
}

层次序遍历

一层一层的从左往右(以下两种方法相同)

void levelorder(TreeNode* root)
{
	if(root == NULL)return;
	queue<TreeNode*> que;
	TreeNode* cur = root;
	que.push(cur);
	while(!que.empty())
	{
		cur = que.front();
		que.pop();
		if(cur->left)que.push(cur->left);
		if(cur->right)que.push(cur->right);
	}
}

这种方法一次性遍历一层

void levelorder(TreeNode* root)
{
	if(root == NULL)return;
	queue<TreeNode*> que;
	TreeNode* cur = root;
	que.push(cur);
	int count = 1;
	while(!que.empty())
	{
		for(int i = 0; i < count; i++)
		{
			cur = que.front();
			que.pop();
			cout<<cur->val<<endl;
			if(cur->left)que.push(cur->left);
			if(cur->right)que.push(cur->right);
		}
		count = que.size();
	}
}

层次序遍历的递归解法,但是只适用于一下子将所有结果输出的题目,不适合按顺序输出

vector<vector<int>> levelOrder(TreeNode *root)
{
	vector<vector<int>> res;
	levelorder(root, 0, res);
	return res;
}
void levelorder(TreeNode *root, int level, vector<vector<int>> &res)
{
	if (!root) return;
	if (res.size() == level) res.push_back({});
	res[level].push_back(root->val);
	if (root->left) levelorder(root->left, level + 1, res);
	if (root->right) levelorder(root->right, level + 1, res);
}

中序迭代遍历

中序遍历的方式是先左后根最后右节点

先输出的是根节点的最左节点,但是我们想要到达那个节点,必须通过根节点到最左节点的路径上的所有节点,我们需要存储这些节点,同时,访问完最左节点之后,访问的就是最左节点的父节点,然后就是最左节点的父节点的右节点。
再然后就是将父节点的右节点作为根节点继续重复以上过程。
在上面的过程中必然会出现左子树的所有节点全都出栈的状况,这样子栈为空,但是根节点的右子树还没有访问,所以while循环的判断条件不能只是一个!stk.empty(),还应该加上当前结点是否为空的条件。代码如下:

void inorder(TreeNode* root)
{
	if(root == NULL)return;
	stack<TreeNode*> stk;
	stk.push(root);
	TreeNode* cur = root;//当前处理的结点
	while(!cur || !stk.empty())//只有两个都为空时才代表当前已经将整棵树处理完
	{
		//先一次性地将该节点的所有左孩子,左孙子等等全都放入栈内
		while(!cur)
		{
			stk.push(cur);
			cur = cur->left;
		}
		//从栈中取出一个节点,并访问此节点,然后将此节点作为一个根节点对其子树进行如上的操作
		if(!stk.empty())
		{
			cur = stk.top();
			stk.pop();
			cout<<cur->val<<endl;
			cur = cur->right;
		}
	}
}

Morris Traversal(中序遍历)

算法如下:
将根节点作为当前节点开始遍历
1、当前节点的左子女为空,那么输出当前节点,并将此节点的右子女做为当前节点。
2、当前节点的左子女不为空,那么便找到当前节点的左子女的最右子女,如果最右子女的右孩子为空的话,说明当前我们是第一次碰到当前节点,因为是中序遍历,所以暂时不应该输出当前节点,然后将最右子女的右子女指向当前节点。随后当前节点指向其左子节点(因为需要先过左子树);如果当前节点的左子女的最右节点的右孩子是当前节点,说明我们这是第二次碰到当前节点并且当前节点的左子树已经访问完毕,那么输出当前节点,并将当前节点移向其右子女。

void Morris_Traversal(TreeNode* root)
{
	if(root == NULL)return;
	TreeNode* cur = root, *pre;
	while(cur)
	{
		if(!cur->left)
		{
			cout<<cur->val<<endl;
			cur = cur->right;
		}
		else
		{
			pre = cur->left;
			while(pre->right && pre->right != cur)pre = pre->right;
			if(!pre->right)
			{
				pre->right = cur;
				cur = cur->left;
			}
			else
			{
				pre->right == NULL;
				cout<<cur->val<<endl;
				cur = cur->right;
			}
		}
	}
}

后序迭代遍历

后序遍历的过程是先左子女,右子女,最后根节点

一个栈的方式一

我们处理一个非根节点时,必然是通过它的父母节点才访问到它,但是后序遍历中输出过右子女后才会输出根节点,也就是说我们再经过根节点的时候要将根节点存储起来,然后处理其右子女,等右子女及右孙子等等处理完之后才将根节点拿出处理,但是我们怎么判断当前节点的右子树是否已经处理完了呢,如果已经处理完了,那么接下来处理的就是根节点,否则就应该处理当前节点的右子树,所以加上一个last指向上一个处理的结点会好很多。
具体算法如下:
1、如果当前节点是叶节点,那么之后不会再遇到它,此时也就是最后一次遇见它,也就是将其输出,并不再存储此节点,并且使last指向此节点。
2、如果当前节点的右子女为空,并且左子女等于last,说明当前节点的左子树已经完全处理过了,那么直接输出当前节点,并且不再需要保存当前节点,使last指向此节点。
3、如果当前节点的右子女不为空,并且右子女等于last,说明当前节点的左子树和右子树全部已经处理完毕,那么输出当前节点,并且不再需要保存当前节点,使last指向此节点。
4、除却以上的状况就是左子数和右子树全都没有处理过。这种情况下,由于后序遍历的顺序是先左后又再根,并且我们使用的是栈,所以将当前节点的右子女先入栈,然后左子女入栈。

void postorder(TreeNode* root)
{
	if(root == NULL)return;
	stack<TreeNode*> stk;
	TreeNode* pre = NULL;//逻辑上是前一个处理的节点,初始化时是NULL,因为我们现在还没有处理节点
	//首先将root入栈
	stk.push(root);
	while(!stk.empty())
	{
		TreeNode* top = stk.top();
		//此节点是叶节点,可以删
		//此后不会再访问到这一点,那么这是最后一次访问此节点,所以需要将其输出
		if(top->left == NULL && top->right == NULL)
		{
			cout<<top->val<<endl;
			pre = top;//记录这一步处理的结点
			stk.pop();
		}
		//此节点不是叶节点,但是其没有右孩子,只有左孩子,并且左孩子已经处理过了
		else if(top->right == NULL && top->left == last)
		{
			//要么last是个叶节点已经处理过,要么就是last的所有子节点都已经处理完,现在在往上处理
			cout<<top->val<<endl;
			pre = top;
			stk.pop();
		}
		//上一个处理的结点就是右节点,说明当前节点的左右子树全都处理完毕
		else if(top->right == last)
		{
			cout<<top->val<<endl;
			pre = top;
			stk.pop();
		}
		//以上情况之外的便是,该节点的子树还没有处理完
		//由于是先左子女,后右子女,所以左子女后入栈
		else
		{
			if(top->right)stk.push(top->right);
			if(top->left)stk.push(top->left);
		}
	}
}

一个栈的方式二

第一次遇到某个节点时,将这个节点存入栈内两次,这样子,如果当前弹出的节点和栈顶的节点相同的话,说明我们刚刚才把这个节点存进去,那么接下来就应该存储这个节点的右子女和左子女;如果当前弹出的结点和栈顶的节点不同的话,说明当前节点没有子女节点或者其子女节点已经被处理完了,那么就直接将此节点输出。

void postorder(TreeNode* root)
{
	if(root == NULL)return;
	stack<TreeNode*> stk;
	stk.push(root);
	stk.push(root);
	while(!stk.empty())
	{
		TreeNode* top = stk.top();
		stk.pop();
		if(!stk.empty() && top == stk.top())
		{
			if(top->right){stk.push(top->right);stk.push(top->right);}
			if(top->left){stk.push(top->left);stk.push(top->left);}
		}
		else cout<<top->val<<endl;
	}
}

两个栈的方式

前序遍历的镜像的倒转是后序遍历(和前序遍历的栈迭代代码比较一下)

void postorder(TreeNode* root)
{
	if(root == NULL)return;
	stack<TreeNode*> s1,s2;
	s1.push(root);
	while(!s1.empty())
	{
		TreeNode* top = s1.top();
		s1.pop();
		s2.push(top);
		if(top->left)s1.push(top->left);
		if(top->right)s1.push(top->right);
	}
	while(!s2.empty())
	{
		TreeNode* top = s2.top();
		s2.pop();
		cout<<top->val<<endl;
	}
}

Morris Traversal(后序遍历)

建立一个临时节点dump,令其左孩子是root。并且还需要一个子过程,就是倒序输出某两个节点之间路径上的各个节点。
步骤:
当前节点设置为临时节点dump。

  1. 如果当前节点的左孩子为空,则将其右孩子作为当前节点。
  2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
    a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。
    b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。倒序输出从当前节点的左孩子到该前驱节点这条路径上的所有节点。当前节点更新为当前节点的右孩子。
  3. 重复以上1、2直到当前节点为空。
    */
void postorderMorrisTraversal(TreeNode *root)
{
    TreeNode dump(0);
    dump.left = root;
    TreeNode *cur = &dump, *prev = NULL;
    while (cur)
    {
        if (cur->left == NULL)
        {
            cur = cur->right;
        }
        else
        {
            prev = cur->left;
            while (prev->right != NULL && prev->right != cur)
                prev = prev->right;

            if (prev->right == NULL)
            {
                prev->right = cur;
                cur = cur->left;
            }
            else
            {
                vector<int> output; 
                TreeNode *p = cur->left;
                while(p != prev){output.push_back(p->val);p = p->right;}
                for(int i = output.size() - 1; i >= 0; i--)
                    cout<<output[i]<<endl;
                prev->right = NULL;
                cur = cur->right;
            }
        }
    }
}

www.sunshangyu.top
感谢:
彻底理解线索二叉树

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值