二叉树:先序遍历,中序遍历,后序遍历,层序/层次遍历

目录

二叉树

二叉树的递归遍历

先序递归遍历

中序递归遍历

后序递归遍历

二叉树的非递归遍历

先序遍历使用栈结构

中序遍历使用栈结构

后序遍历使用栈结构

中序遍历:(morris遍历)空间复杂度O(1),非递归,不用栈

先序遍历:空间复杂度O(1),非递归,不用栈

后序遍历:空间复杂度O(1),非递归,不用栈

层序遍历

层序遍历:使用队列辅助


二叉树

相信各位小伙伴能进来,就已经知道二叉树是什么,这里就不一一描述了.

下面给出二叉树结点:

class Node {
public:
	int data;
	Node*left;
	Node*right;
};

二叉树的递归遍历

在递归遍历一颗二叉树的任意非空节点时,我们会先后调用函数进入左子树和右子树,在这个过程中,我们有三次处理数据的机会,即进入左子树之前一次,出左子树后/进右子树之前一次,出右子树之后一次,根据处理时机的不同,有:先序遍历(先处理根结点数据->根左右),中序遍历(在左右子树中间处理根结点->左根右),后序遍历(最后处理根结点->左右根);

 

 如图先序遍历:124536

        中序遍历:425136

        后序遍历:452631

有了以上解释,下面三种简单的递归遍历就不需要多说了

先序递归遍历

void PreOrderTreeWalk(Node* root) {
	if (root == nullptr)return;
	cout << root->data << " ";
	PreOrderTreeWalk(root->left);
	PreOrderTreeWalk(root->right);
}

中序递归遍历

void InOrderTreeWalk(Node* root) {
	if (root == nullptr)return;
	InOrderTreeWalk(root->left);
	cout << root->data << " ";
	InOrderTreeWalk(root->right);
}

后序递归遍历

void PostOrderTreeWalk(Node* root) {
	if (root == nullptr)return;
	PostOrderTreeWalk(root->left);
	cout << root->data << " ";
	PostOrderTreeWalk(root->right);
}

二叉树的非递归遍历

先序遍历使用栈结构(不知道栈的评论区留言,下次可以出栈)

由于栈是先进后出结构,先序遍历是根左右形式,我们可以考虑先处理根节点,再将右树根压入栈中待处理(下面代码push的根节点,但pop后会让它指向右子树,可以避免push空节点,也可以在push前判断右子树是否为空),去处理左树根,以根节点root为例:先push右根,处理左根,左根有右树时会继续push,当处理到无左节点时开始pop,作为新根开始处理;上面过程模拟了递归调用,我个人通俗的理解是,处理根,存右树,走左树

void PreOrderTreeWalkWithStack(Node* root) {
	//建栈
	stack<Node*>st;
	Node*p = root;
	
	while (p != nullptr || !st.empty()) {
		//当p空时栈不空,栈空时p不空
		if (p != nullptr) {
			cout << p->data << " ";
			st.push(p);
			p = p->left;
		}
		else {
			p = st.top();
			st.pop();
			p = p->right;
		}
	}
}

中序遍历使用栈结构

这个...中序遍历和先序遍历一样,我就改了两行代码,因为中序遍历是左根右,所以直接压根结点,处理左子树,处理完后pop,接着处理根结点,然后根结点右指,处理右子树;

void InOrderTreeWalkWithStack(Node* root) {
	//建栈
	stack<Node*>st;
	Node*p = root;

	while (p != nullptr || !st.empty()) {
		//当p空时栈不空,栈空时p不空
		if (p != nullptr) {
			st.push(p);
			p = p->left;
		}
		else {
			p = st.top();
			st.pop();
			cout << p->data << " ";
			p = p->right;
		}
	}
}

后序遍历使用栈结构

后序遍历非递归栈有一点难,我们找一种简单的方法吧,前面我们提到过,一个节点可以有三次处理的机会,后序遍历就是在第三次时处理的,我们只需要找一种简单的方法判断是第几次遇见该结点,就可以实现后序遍历

/*
改变结点结构,这个有点难
每次visited++,判断visited%3+1
class Node {
public:
	int data;
	Node*left;
	Node*right;
	int visited = 0;
};
*/
/*
哈希表这么好用,不会有人不会吧
map<Node*, int>m;
*/

//建新结点,复制原树
class pNode {
public:
	int data;
	pNode*left;
	pNode*right;
	int visited = 0;
};
//利用了递归先序遍历
pNode* CopyTree(Node* root) {
	if (root == nullptr)return nullptr;
	pNode*p = new pNode;
	p->data = root->data;
	p->left = CopyTree(root->left);
	p->right = CopyTree(root->right);
	return p;
}

void PostOrderTreeWalkWithStack(pNode*root) {
	stack<pNode*>st;
	pNode*p = root;

	while (p != nullptr || !st.empty()) {
		//当p空时栈不空,栈空时p不空
		if (p != nullptr) {
			//第一次
			p->visited++;
			st.push(p);
			p = p->left;
		}
		else {
			p = st.top();
			st.pop();
			//这里是第二次
			if (p->visited % 3 + 1 == 2) {
				p->visited++;
				st.push(p);
				p = p->right;
			}
			//第三次
			else {
				//visited++三次,不妨碍下次使用
				p->visited++;
				cout << p->data << " ";
				//该结点处理时子树已经处理完了,需要置空
				p = nullptr;
			}
		}
	}
}

上述代码时间复杂度O(n),空间复杂度看起来也不低

下面开始介绍不用栈结构和递归实现的二叉树遍历

中序遍历:(morris遍历)空间复杂度O(1),非递归,不用栈

空间复杂度O(1)难点在于,遍历子结点时如何返回父节点,由于二叉树有许多叶结点,他们的左右指针都是空,我们可以利用这些指针返回父节点;

步骤如下:

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

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

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

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

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

 

void InOrderTreeWalkWithoutStack(Node* root) {
	Node*cur = root;
	Node*pre = nullptr;

	while (cur!=nullptr) {
		if (cur->left == nullptr) {
			//cur没有左子树,只会停留一次
			cout << cur->data << " ";
			cur = cur->right;
		}
		else {
			pre = cur->left;
			//找到cur左树最右节点
			while (pre->right != nullptr&&pre->right != cur) {
				pre = pre->right;
			}
			//第一次停留,储存父结点,cur处理左边
			if (pre->right == nullptr) {
				pre->right = cur;
				cur = cur->left;
			}
			//第二次停留,恢复叶结点,左边处理完成,cur处理右边
			else {
				pre->right = nullptr;
				cout << cur->data << " ";
				cur = cur->right;
			}
		}
	}
}

先序遍历:空间复杂度O(1),非递归,不用栈

先序和中序只有一行不同,先序是第一次到达时处理,中序是第二次

void PreOrderTreeWalkWithoutStack(Node* root) {
	Node*cur = root;
	Node*pre = nullptr;

	while (cur != nullptr) {
		if (cur->left == nullptr) {
			//cur没有左子树,只会停留一次
			cout << cur->data << " ";
			cur = cur->right;
		}
		else {
			pre = cur->left;
			//找到左树最右节点
			while (pre->right != nullptr&&pre->right != cur) {
				pre = pre->right;
			}
			//第一次停留,储存父结点,处理左边
			if (pre->right == nullptr) {
				pre->right = cur;
				cout << cur->data << " ";
				cur = cur->left;
			}
			//第二次停留,恢复叶结点,左边处理完成,处理右边
			else {
				pre->right = nullptr;
				cur = cur->right;
			}
		}
	}
}

后序遍历:空间复杂度O(1),非递归,不用栈

 如图,对于后序遍历,可以看成--逆序打印每个结点左树右边界,最后逆序打印大树右边界

如果用递归

void PrintReverse(Node*root) {
	if (root == nullptr) return;
	PrintReverse(root->right);
	cout << root->data << " ";
}

但我们说了不用递归,栈.所以我们将它看成链表,先反转,然后处理,处理后再反转


void Reverse(Node* from, Node* to) {
	if (from == to)return;
	Node*pre = from->right;
	Node*cur = from;
	Node*ptr = nullptr;
	while (cur != to) {
		ptr = pre->right;
		pre->right = cur;
		cur = pre;
		pre = ptr;
	}
}
void PrintReverse1(Node* from, Node* to) {
	Reverse(from, to);
	Node*p = to;
	while (p != from) {
		cout << p->data << " ";
		p = p->right;
	}		
	cout << p->data << " ";
	Reverse(to, from);
}
//使用了dump,这样就不用单独打印大树右边界了
void PostOrderTreeWalkWithoutStack(Node* root) {
	Node dump;
	dump.right = nullptr;
	dump.data = 0;
	dump.left = root;
	Node*cur = &dump;
	Node*pre = nullptr;

	while (cur != nullptr) {
		if (cur->left == nullptr) {
			cur = cur->right;
		}
		else {
			pre = cur->left;
			//找到左树最右节点
			while (pre->right != nullptr&&pre->right != cur) {
				pre = pre->right;
			}
			//第一次停留,储存父结点,处理左边
			if (pre->right == nullptr) {
				pre->right = cur;
				cur = cur->left;
			}
			//第二次停留,恢复叶结点,左边处理完成,处理右边
			else {
				PrintReverse1(cur->left, pre);
				pre->right = nullptr;
				cur = cur->right;
			}
		}
	}
}

层序遍历

上图层序遍历结果为:1 2 3 4 5 6

我们可以知道这是从上到下从左到右输出的

有一种比较好懂的层序遍历,先用深度搜索找最大深度d,然后循环1-d,递归先序遍历记录当前层,相等就输出

void PreOrderWalk(Node*root, int i, int j) {
	if (root == nullptr)return;
	if (i == j) {
		cout << root->data << " ";
		return;
	}
	PreOrderWalk(root->left, i, j + 1);
	PreOrderWalk(root->right, i, j + 1);
}
int DeepSearch(Node*root, int i) {
	if(root==nullptr)return i;
	return max(DeepSearch(root->left, i + 1), DeepSearch(root->right, i + 1));
}
void LevelOrderTreeWalk(Node*root) {
	int d = DeepSearch(root, 0);
	for (int i = 0; i < d; i++) {
		PreOrderWalk(root, i, 0);
	}
}

使用先序遍历保证了每层之间元素的相对次序,这种方法时间复杂度较高

层序遍历:使用队列辅助(不知道队列的也可以留言哈)

队列是先入先出的结构,这个是真简单,执行以下步骤即可

首先,根结点 1 入队;
根结点 1 出队,出队的同时,将左孩子 2 和右孩子 3 分别入队;
队头结点 2 出队,出队的同时,将结点 2 的左孩子 4 和右孩子 5 依次入队;
队头结点 3 出队,出队的同时,将结点 3 的左孩子 6 和右孩子 7 依次入队;
不断地循环,直至队列内为空。

从左到右使子结点入队,可以保证下一层的相对次序,可以看成从第一层开始相对次序就是正确的

void LevelOrderTreeWalkWithQueue(Node*root) {
	queue<Node*>qu;
	qu.push(root);
	Node*p = nullptr;
	while (!qu.empty()) {
		p = qu.front();
		qu.pop();
		cout << p->data << " ";
		if (p->left != nullptr) {
			qu.push(p->left);
		}
		if (p->right != nullptr) {
			qu.push(p->right);
		}
	}
}

如果你想知道哪些数据是哪一层的,可以从第一层开始,记录当前队尾元素,每次处理到该元素时,说明到达该层结尾了,处理结束后刷新记录的队尾,重复上述操作即可.

如果有问题可以评论区,大家一起讨论

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值