数据结构----二叉树的遍历(非递归)

3 阅读5分钟

如果不使用递归对二叉树进行深度遍历遍历,那么需要一个数据结构来记录我们之前走过的节点,通过对遍历过程的预演可以得到我们需要用栈来记录,因为走过的节点是先进先出.

所以我们需要构建一个链式栈和一棵二叉树.本文重点在于遍历的过程,栈和树的构建可以参考以前的文章.

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

//树的节点结构
typedef struct BTNode {
	char data;
	struct BTNode* l;
	struct BTNode* r;
}BTNode,*BTree;

//链式栈
//栈的节点结构
typedef struct StackNode {
    BTNode* data;
	struct StackNode* next;
}sstack;

//初始化链式栈
sstack* InitStack() {
	sstack* s = (sstack*)malloc(sizeof(sstack));
	if (s == NULL) {
		printf("内存申请失败\n");
		return s;
	}
	s->next = NULL;
	return s;
}

//入栈
void Push(sstack* s, BTNode* k) {
	sstack* p = (sstack*)malloc(sizeof(sstack));
	if (p == NULL) {
		printf("内存申请失败\n");
		return ;
	}
	p->data = k;
	p->next = s->next;
	s->next = p;
	return ;
}

//判空
int Empty(sstack* s) {
	if (s->next == NULL) {
		return 1;
	}
	return 0;
}

//出栈
BTNode* Pop(sstack* s) {
	if (Empty(s)) {
		printf("栈空无法出栈\n");
		return NULL;
	}
	sstack* p = s->next;
	BTNode* k = p->data;
	s->next = p->next;
	free(p);
	p = NULL;
	return k;
}

//得到栈顶元素但不出栈
BTNode* Get(sstack* s) {
	if (Empty(s)) {
		printf("栈空无法获取\n");
		return NULL;
	}
	BTNode* k = s->next->data;
	return k;
}

//建树
BTree InitTree(char root) {
	BTNode* rt = (BTNode*)malloc(sizeof(BTNode));
	if (rt == NULL) {
		printf("内存申请失败\n");
		return rt;
	}
	rt->data = root;
	rt->l = NULL;
	rt->r = NULL;
	return rt;
}

//查找函数
BTNode* Find(BTree root,char x) {
	if (root->data == x) {
		return root;
	}
	if (root->l != NULL) {
		BTNode* ans = Find(root->l, x);
		if (ans != NULL && ans->data == x) {
			return ans;
		}
	}
	if (root->r != NULL) {
		BTNode* ans = Find(root->r, x);
		if (ans != NULL && ans->data == x) {
			return ans;
		}
	}
	return NULL;
}

BTree Insert(BTNode* root, char x, char fx, int flag) {
	BTNode* f = Find(root, fx);
	BTNode* s = (BTNode*)malloc(sizeof(BTNode));
	if (s == NULL) {
		printf("内存申请失败\n");
		return root;
	}
	s->data = x;
	if (flag) {
		f->r = s;
	}
	else {
		f->l = s;
	}
	s->l = NULL;
	s->r = NULL;
	return root;
}

//遍历
//----------------------------------------------------
void Visit(BTNode* k) {
	printf("%c ",k->data);
}

//先序遍历
/*  引入一个指针p和一个链式栈 p始终指向根节点
    p非空, 说明当前要遍历以p为根的子树,先访问p,p入栈,p指向其左孩子p=p->l,继续循环
    p为NULL,说明栈顶结点的左子树访问完了,栈顶出栈 p指向栈顶的右孩子,继续循环*/
void PreOrder(BTree root) {
	if (root == NULL) {
		printf("空树\n");
		return;
	}
	BTNode* p = root;
	//初始化一个链式栈
	sstack* top = InitStack();
	BTNode* f = NULL;
	while (p != NULL || !Empty(top)) {
		if (p != NULL) {
			Visit(p);
			Push(top, p);
			p = p->l;
		}
		else {
			f = Pop(top);
			p = f->r;
		}
	}
	
}

//中序遍历
/*  引入一个指针p和一个链式栈 p始终指向根节点
    p非空, 说明当前要遍历以p为根的子树,先访问p的左子树,p入栈,p指向其左孩子p=p->l,继续循环
    p为NULL,说明栈顶结点的左子树访问完了,栈顶出栈  访问该结点, p指向栈顶的右孩子,继续循环*/
void InOrder(BTree root) {
	if (root == NULL) {
		printf("树空\n");
		return;
	}
	BTNode* p = root;
	sstack* top = InitStack();
	BTNode* f = NULL;
	while (p != NULL || !Empty(top)) {
		if (p != NULL) {
			Push(top,p);
			p = p->l;
		}
		else {
			f = Pop(top);
			Visit(f);
			p = f->r;
		}
	}
}

//后序遍历
/*  引入两个指针p和pre和一个链式栈 p始终指向根节点
	p非空, 说明当前要遍历以p为根的子树,先访问p的左子树,p入栈,p指向其左孩子p=p->l,继续循环
	p为NULL,说明栈顶结点的某棵子树访问完了,判断栈顶有没有右子树 若有再判断刚刚访问的是不是右子树*/
void PostOrder(BTree root) {
	if (root == NULL) {
		printf("树空\n");
		return;
	}
	BTNode* p = root;
	sstack* top = InitStack();
	BTNode* pre = NULL;
	BTNode* f = NULL;
	while (p != NULL || !Empty(top)) {
		if (p != NULL) {
			Push(top, p);
			p = p->l;
		}
		else {
			f = Get(top);
			if (f->r != NULL && pre != f->r) {
				p = f->r;
			}
			else {
				f = Pop(top);
				Visit(f);
				pre = f;
			}
		}
	}
}
int main() {
	int n;
	int flag;//flag == 0左孩子 == 1右孩子
	scanf("%d", &n);
	getchar();
	char x, fx;
	scanf("%c", &x);
	BTree root = InitTree(x);
	for (int i = 2; i <= n; i++) {
		getchar();
		scanf("%c %c %d", &x, &fx, &flag);
		Insert(root, x, fx, flag);
	}

	//先序遍历
	PreOrder(root);

	printf("\n");
	//中序遍历
	InOrder(root);

	printf("\n");
	//后序遍历
	PostOrder(root);

	printf("\n");
	return 0;
}
/*
9
A
B A 0
E A 1
C B 1
D C 0
F E 1
G F 0
H G 0
K G 1

*/

二叉树的非递归遍历:核心要点总结

一、为什么需要非递归遍历

  • 效率问题:递归调用有函数开销,频繁压栈出栈影响性能
  • 栈溢出风险:树深度过大时,系统栈空间可能不足
  • 解决方案:手动维护栈来模拟递归过程,完全控制压栈和出栈时机

二、统一框架思路

三种遍历都遵循同一个循环框架:

  • 当前节点非空时:根据遍历规则决定是否访问,然后压栈,走向左孩子
  • 当前节点为空时:从栈中弹出节点,根据规则决定是否访问,然后走向右孩子

三种遍历的本质区别仅在于:访问节点的时机不同

三、三种遍历的核心区别

遍历方式访问时机难度核心要点
先序遍历入栈前⭐ 简单根 → 左 → 右
中序遍历出栈时⭐ 简单左 → 根 → 右
后序遍历出栈时(有条件)⭐⭐⭐ 较难左 → 右 → 根,需记录上次访问节点

四、各遍历的关键点

先序遍历

  • 当前节点非空时立即访问
  • 访问完再入栈,然后向左走
  • 逻辑最直观,最容易理解

中序遍历

  • 入栈时不访问,继续向左走
  • 出栈时才访问节点
  • 与先序遍历仅差一行代码的位置

后序遍历

  • 需要额外指针记录上一次访问的节点
  • 出栈前必须判断:右子树是否已经被访问过
  • 判断依据:上一次访问的节点是否为当前节点的右孩子
  • 只有右子树为空或已访问,才能访问当前节点

五、为什么后序遍历最难

因为处理完左子树后,不能立即处理当前节点,必须先去处理右子树。从右子树返回时,需要知道右子树已经处理完毕。上一次访问指针就是用来解决这个问题的——如果上次访问的是当前节点的右孩子,说明右子树已经处理完了。

六、一句话总结

先序入栈前访问,中序出栈时访问,后序出栈前先问右子树。