栈遍历二叉树及c++实现

分析

如果是使用递归来实现二叉树的先,中,后序遍历只需要更改三行代码的位置,但若是使用栈来写那便会有趣得多

无论是根节点还是左右子树,在栈中的表示方式都是指针,所以我们可以将左右子树各当作是一个整体,这样问题就简化为具有3个结点的完全二叉树的输出了

左右子树有三种状态,空,遍历过,未遍历过

通用逻辑

  1. 栈中初始化只有一个指向根结点的指针
    • 遍历往往以根节点作为基础参数
  2. 栈顶指针指向的结点总是当做根结点处理
    • 模拟递归
  3. 只有栈顶指针指向的结点内容才会被输出
    • 栈顶易于操作
  4. 结点内容输出后指向该结点的指针会立即出栈
    • 避免结点重复输出
  5. 当一个指针出栈时,如果该指针指向的结点的左右子树有未被使用过(不为空并且未遍历过),那么指向其左右子树的指针将随后入栈,且右子树总是优先于左子树入栈(左子树总是优先于右子树输出)
    • 入栈是避免信息的丢失,顺序是栈的特性(先进后出)
  6. 当栈为空时遍历结束
    • 即遍历完所有结点(可以由2,3推导出来)

栈,逻辑,输出顺序分析

输出顺序
遍历类型输出顺序
先序遍历根结点->左子树->右子树
中序遍历左子树->根结点->右子树
后序遍历左子树->右子树->根结点
简单空栈实现操作(栈先进后出)
遍历类型实现操作
先序遍历右子树入栈->左子树入栈->根结点入栈->输出->输出->输出
中序遍历右子树入栈->根结点入栈->左子树入栈->输出->输出->输出
后序遍历根结点入栈->右子树入栈->左子树入栈->输出->输出->输出
更进一步(括号视作默认操作)

实际情况是栈总是当做根结点处理,这意味着栈中始终存在根结点,相当于根结点入栈是默认操作,在此前提下完成的操作如下

遍历类型实现操作
先序遍历(根结点入栈)->输出根结点->右子树入栈->左子树入栈->输出左子树->输出右子树
中序遍历(根结点入栈)->左子树入栈->输出左子树->输出根结点->右子树入栈->输出右子树
后序遍历(根结点入栈)->左子树入栈->输出左子树->右子树入栈->输出右子树->输出根结点
还有一点

只有栈顶的结点才能被输出,又因为栈顶总是被视作根结点,那么只有根结点可以输出

遍历类型实现操作
先序遍历(根结点入栈)->输出根结点->右子树入栈->左子树入栈
中序遍历(根结点入栈)->左子树入栈->输出根结点->右子树入栈
后序遍历(根结点入栈)->左子树入栈->右子树入栈->输出根结点

通用逻辑推导栈顶操作流程

大部分操作都在栈顶完成,所以只要分析栈顶变化就可以解决问题

栈顶只有两个选择,要么输出,要么选择相应子树入栈

​ 输出后要备份,出栈,再考虑有没有丢失信息然后回到栈顶状态

graph TD 1(栈顶)==>2(可输出???) 2==>|否|33(栈顶相应子树入栈) 33==>1 2==>|是|3(输出栈顶) 3==>4(备份栈顶) 4==>5(栈顶出栈) 5==>12(备份右子树状态???) 12==>|未遍历|13(备份右子树入栈) 12==>|遍历过或为空|6 13==>6(备份左子树状态???) 6==>|未遍历|7(备份左子树入栈) 6==>|遍历过或为空|1 7==>1

算法

先序遍历

分析
  • 先序遍历的实际操作:(根结点入栈)->输出根结点->右子树入栈->左子树入栈
  • 栈顶指向的结点总是可以输出的
  • 由于先序遍历的特性,根结点输出早于左右子树所以不存在左右子树遍历过的情况(所以左右子树只有空和非空两种状态)
结合分析修改栈顶操作流程图
graph TD 1(栈顶)==>3(输出栈顶) 3==>4(备份栈顶) 4==>5(栈顶出栈) 5==>12(备份右子树状态???) 12==>|不为空|13(备份右子树入栈) 12==>|为空|6 13==>6(备份左子树状态???) 6==>|不为空|7(备份左子树入栈) 6==>|为空|1 7==>1
流程
  1. 先输出栈顶指针指向的结点
  2. 栈顶指针出栈(栈顶指针备份,否则左右子树信息丢失)
  3. 备份指针指向结点的右子树入栈(如果右子树指针不为空)
  4. 备份指针指向结点的左子树入栈(如果左子树指针不为空)
  5. 回到步骤1
C++代码实现
	void preorder_travel() {
		if (root == NULL)
			return;
		std::stack<Node*> s;
		s.push(root);//根指针入栈
		while (!s.empty())
		{
			std::cout << s.top()->data << " ";//输出栈顶指针指向的结点
			Node* temp = s.top();//备份栈顶指针
			s.pop();//栈顶指针出栈
			if (temp->R != NULL) s.push(temp->R);//备份指向结点右子树不为空则将其压入栈
			if (temp->L != NULL) s.push(temp->L);//备份指向结点左子树不为空则将其压入栈
		}
	}

中序遍历

分析
  • 中序遍历的实际操作:(根结点入栈)->左子树入栈->输出根结点->右子树入栈
  • 左子树入栈在输出根结点之前,这意味着必须一直向左搜索没有左子树(左子树为空)的结点,然后输出
    • 向左搜索时一直添加栈顶左子树,这样回退的时候就不用考虑左子树(必定遍历过或者为空),即当栈顶可输出时,左子树必定遍历过或为空
      • 但是一旦添加了右子树就会打破规则(左子树必定遍历过或者为空),因为新栈顶没有向左搜索,需要重新执行向左搜索
结合分析修改栈顶操作流程图
graph TD 1(栈顶)==>2(可输出???) 2==>|否|33(栈顶左子树入栈) 33==>1 2==>|是|3(输出栈顶) 3==>4(备份栈顶) 4==>5(栈顶出栈) 5==>12(备份右子树状态???) 12==>|未遍历|13(备份右子树入栈) 12==>|遍历过或为空|3 13==>1
流程
  1. 重复压入栈顶结点左子树指针,直到栈顶结点左子树指针为空
  2. 重复输出栈顶指针指向的结点(输出之前备份,输出之后出栈),然后检查
    • 如果备份结点右子树未遍历过,然后将其右子树入栈,回到1
C++代码实现
void inorder_travel() {
		if (root == NULL)
			return;
		std::stack<Node*> s;
		s.push(root);//根指针入栈
		while (!s.empty())
		{
			while (s.top()->L != NULL)//重复将栈顶指针指向结点的左子树压栈,直到栈顶指针指向结点的左子树为空
			{
				s.push(s.top()->L);
			}
			while (!s.empty())//重复输出栈顶指针指向的结点
			{
				std::cout << s.top->data << " ";//输出栈顶指针指向的结点
				Node* temp = s.top();//备份栈顶指针
				s.pop();//栈顶指针出栈
				if (temp->R != NULL) {//检查,如果栈顶指针指向的结点右子树不为空,则将其右子树入栈,退出检查
					s.push(temp->R);
					break;//回到1
				}
			}
		}
}

后序遍历

分析
  • 中序遍历的实际操作:(根结点入栈)->左子树入栈->右子树入栈->输出根结点
  • 左子树入栈在输出根结点之前,这意味着必须一直向左搜索没有左子树(左子树为空)的结点
    • 向左搜索时一直添加栈顶左子树,这样回退的时候就不用考虑左子树(必定遍历过或者为空),即当栈顶可输出时,左子树必定遍历过或为空
      • 但是一旦添加了右子树就会打破规则(左子树必定遍历过或者为空),因为新栈顶没有向左搜索
  • 右子树入栈在左子树入栈之后,这意味这找到左子树为空的结点,然后检查其右子树
    • 当根结点右子树为空或者遍历过就输出根结点
    • 当根结点右子树未遍历过就将其入栈打破规则需要重新向左搜索
结合分析修改栈顶操作流程图
graph TD 1(栈顶)==>2(可输出???) 2==>|否|33(栈顶左子树入栈) 33==>1 2==>|是|4(备份栈顶) 3(输出备份指向结点) 4==>5(栈顶出栈) 5==>12(备份右子树状态???) 12==>|未遍历|13(备份右子树入栈) 12==>|遍历过或为空|3 3==>2 13==>1
流程
  1. 重复压入栈顶结点左子树指针,直到栈顶结点左子树指针为空
  2. 重复将栈顶出栈(出栈之前备份,输出之后出栈)然后检查
    • 如果备份指向结点右子树未遍历过,然后将其右子树入栈,回到1
    • 如果备份指向结点右子树遍历过或为空,输出备份指向结点
C++代码实现
void postorder_travel() {
	if (root == NULL)
		return;
	std::stack<Node*> s;
	s.push(root);//根指针入栈
	while (!s.empty())
	{
		while (s.top()->L != NULL)//重复将栈顶指针指向结点的左子树压栈,直到栈顶指针指向结点的左子树为空
		{
			s.push(s.top()->L);
		}
		Node* last = NULL;//上一次遍历过的指针
		while (!s.empty())//重复检查
		{
			if (s.top()->R==NULL||last==s.top()->R) {//如果栈顶指针指向结点的右子树为空或者遍历过
				std::cout << s.top()->data << " ";//输出栈顶指向的结点
				last = s.top();//更新指针last
				s.pop();//栈顶指针出栈
			}
			else if(s.top()->R!=NULL)//如果栈顶指针指向的结点的右子树未遍历过
			{
				s.push(s.top()->R);//将右子树入栈
				break;//退出检查
			}
		}
	}
}

整体代码

#include <iostream>
#include <stack>
template <typename T>
class BST {
public:
	BST() :root(NULL) {};
	~BST() {};
	void insert(T data) {
		Node* temp = new Node();
		temp->L = NULL;
		temp->R = NULL;
		temp->data = data;
		if (root == NULL) {
			root = temp;
		}
		else {
			Node* tracer = root;
			while (true)
			{
				if (tracer->data >= data)
					if (tracer->L == NULL) {
						tracer->L = temp;
						break;
					}
					else
						tracer = tracer->L;
				else
					if (tracer->R == NULL) {
						tracer->R = temp;
						break;
					}
					else
						tracer = tracer->R;
			}
		}
	}
	void preorder_travel() {
		if (root == NULL)
			return;
		std::stack<Node*> s;
		s.push(root);//根指针入栈
		while (!s.empty())
		{
			std::cout << s.top()->data << " ";//输出栈顶指针指向的结点
			Node* temp = s.top();//备份栈顶指针
			s.pop();//栈顶指针出栈
			if (temp->R != NULL) s.push(temp->R);//备份指向结点右子树不为空则将其压入栈
			if (temp->L != NULL) s.push(temp->L);//备份指向结点左子树不为空则将其压入栈
		}
	}
	void inorder_travel() {
		if (root == NULL)
			return;
		std::stack<Node*> s;
		s.push(root);//根指针入栈
		while (!s.empty())
		{
			while (s.top()->L != NULL)//重复将栈顶指针指向结点的左子树压栈,直到栈顶指针指向结点的左子树为空
			{
				s.push(s.top()->L);
			}
			while (!s.empty())//重复输出栈顶指针指向的结点
			{
				std::cout << s.top->data << " ";//输出栈顶指针指向的结点
				Node* temp = s.top();//备份栈顶指针
				s.pop();//栈顶指针出栈
				if (temp->R != NULL) {//检查,如果栈顶指针指向的结点右子树不为空,则将其右子树入栈,退出检查
					s.push(temp->R);
					break;//回到1
				}
			}
		}
	}
	void postorder_travel() {
		if (root == NULL)
			return;
		std::stack<Node*> s;
		s.push(root);//根指针入栈
		while (!s.empty())
		{
			while (s.top()->L != NULL)//重复将栈顶指针指向结点的左子树压栈,直到栈顶指针指向结点的左子树为空
			{
				s.push(s.top()->L);
			}
			Node* last = NULL;//上一次遍历过的指针
			while (!s.empty())//重复检查
			{
				if (s.top()->R==NULL||last==s.top()->R) {//如果栈顶指针指向结点的右子树为空或者遍历过
					std::cout << s.top()->data << " ";//输出栈顶指向的结点
					last = s.top();//更新指针last
					s.pop();//栈顶指针出栈
				}
				else if(s.top()->R!=NULL)//如果栈顶指针指向的结点的右子树未遍历过
				{
					s.push(s.top()->R);//将右子树入栈
					break;//退出检查
				}
			}
		}
	}
private:
	struct Node
	{
		Node* L;
		Node* R;
		T data;
	};
	Node* root;
};
int main(){
	BST<int> b;
	b.insert(2);
	b.insert(4);
	b.insert(1);
	b.insert(5);
	b.insert(3);
	b.insert(0);
	b.preorder_travel();
	std::cout << std::endl;
	b.inorder_travel();
	std::cout << std::endl;
	b.postorder_travel();
}

转载于:https://www.cnblogs.com/redo19990701/p/11307423.html

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
反向遍历二叉树,即从右子节点,根节点,左子节点的顺序进行遍历。 以下是使用C语言编写的反向遍历二叉树的代码: ``` #include <stdio.h> #include <stdlib.h> // 定义二叉树节点结构体 typedef struct Node { int data; struct Node* left; struct Node* right; } Node; // 创建新节点 Node* createNode(int data) { Node* newNode = (Node*)malloc(sizeof(Node)); if(newNode == NULL) { printf("创建节点失败!内存不足。\n"); exit(1); } newNode->data = data; newNode->left = NULL; newNode->right = NULL; return newNode; } // 反向遍历二叉树 void reverseInOrderTraversal(Node* root) { if(root == NULL) { return; } // 将右子节点作为根节点递归调用 reverseInOrderTraversal(root->right); // 输出根节点的数据 printf("%d ", root->data); // 将左子节点作为根节点递归调用 reverseInOrderTraversal(root->left); } int main() { // 创建二叉树 Node* root = createNode(1); root->left = createNode(2); root->right = createNode(3); root->left->left = createNode(4); root->left->right = createNode(5); // 反向遍历二叉树 printf("反向遍历二叉树结果:"); reverseInOrderTraversal(root); return 0; } ``` 这段代码中,我们首先定义了一个二叉树节点结构体,包含了节点的数据以及左右子节点的指针。然后我们创建了一个用于创建新节点的函数 createNode,该函数分配了节点的内存,并对其数据进行赋值。接着我们实现了反向遍历二叉树的函数 reverseInOrderTraversal,该函数使用递归的方式进行遍历,先遍历右子节点,然后输出根节点的数据,最后遍历左子节点。在主函数中,我们创建了一个二叉树并调用了反向遍历函数进行输出。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值