(一)算法与数据结构 | 二叉树


简介

本文介绍关于二叉树头文件的基本信息,主要包括二叉树的构建和二叉树的遍历等。这里采用的二叉树存储方式是链式存储结构,其他存储方法还有顺序存储方式等。


0. 结构体定义

struct TreeNode {
	int val;
	TreeNode *left;
	TreeNode *right;
	TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

使用方法: TreeNode* T = new TreeNode(3)语句即可声明一个值(val)为3的结点,
且T->left = NULL,T->right = NULL。


1. 二叉树的构建

这部分介绍三种二叉树的构建方法:先序构建二叉树、中序构建二叉树、后序构建二叉树,均采用递归的方法实现。
(1)先序构建二叉树

void createBiTree(TreeNode* &T) {
	int ch;
	cin >> ch;
	if (ch == -1) {
		T = NULL;
	}
	else
	{
		T = new TreeNode(ch);
		createBiTree(T->left);
		createBiTree(T->right);
	}
}

(2)中序构建二叉树

void createBiTree(TreeNode* &T) {
	int ch;
	cin >> ch;
	if (ch == -1) {
		T = NULL;
	}
	else
	{
		createBiTree(T->left);
		T = new TreeNode(ch);
		createBiTree(T->right);
	}
}

(3)后序构建二叉树

void createBiTree(TreeNode* &T) {
	int ch;
	cin >> ch;
	if (ch == -1) {
		T = NULL;
	}
	else
	{
		createBiTree(T->left);
		createBiTree(T->right);
		T = new TreeNode(ch);
	}
}

使用方法: ( 1 ) ( 3 ) {\rm (1)—(3)} 的使用方法类似,这里以先序构建二叉树为例。假如需要构建的二叉树形状为
在这里插入图片描述
由于在定义时使用-1代表空结点,在输入时按照完全二叉树输入即可,叶子节点非空时需要输入左右子节点为空( 1 {\rm -1} )。即: 3   2   4   1   1   5   1   1   1   1   6   1   1 {\rm 3\ 2\ 4\ -1\ -1\ 5\ -1\ -1\ 1\ 1\ 6\ -1\ -1}


2. 二叉树的遍历

本部分介绍二叉树的递归遍历、非递归遍历、层次遍历。
(1)递归

void preVisitBiTree(TreeNode* &T) {
	if (T) {
		Visit(T);
		preVisitBiTree(T->left);
		preVisitBiTree(T->right);
	}
}

这里是先序遍历二叉树的形式,中序遍历方法和后序遍历方法同先序遍历类似(参考各种构建二叉树的方法)。
针对上述图1中的二叉树,先序遍历结果是: 3   2   4   5   1   6 {\rm 3\ 2\ 4\ 5\ 1\ 6} ;中序遍历结果是: 4   2   5   3   1   6 {\rm 4\ 2\ 5\ 3\ 1\ 6} ;后序遍历结果是: 4   5   2   6   1   3 {\rm 4\ 5\ 2\ 6\ 1\ 3}

(2)非递归

扫描二维码关注公众号,回复: 9900595 查看本文章

递归遍历二叉树的方法是采用系统栈的方法,这里采用非递归的方法也是采用栈这种数据结构。栈的特点是后进先出。

(2-1)先序遍历二叉树

void preVisitBiTree(TreeNode* &T) {
	if (T) {
		stack<TreeNode*> s;
		TreeNode* p;
		s.push(T);
		while (!s.empty())
		{
			p = s.top();
			s.pop();
			cout << p->val << endl;
			if (p->right != NULL) {
				s.push(p->right);
			}
			if (p->left != NULL) {
				s.push(p->left);
			}

		}
	}
}

栈内元素的变化情况:空→3→12(输出3并压入右孩子1和左孩子2)→154(输出2并压入右孩子5和左孩子4)→1(输出4和5)→16(压入右孩子6)→空(输出6和1),得到输出结果3 2 4 5 6 1。使用自定义栈实现二叉树的先序遍历的思路是:压入栈顶元素,当栈不为空时,首先输出栈顶元素并出栈,同时判断该栈顶元素节点有无右孩子和左孩子。如果有则依次压入栈中,直到栈为空则得到最终结果。

(2-2)中序遍历二叉树

void inVisitBiTree(TreeNode* &T) {
	if (T) {
		stack<TreeNode*> s;
		TreeNode* p;
		p = T;
		while (!s.empty() || p)
		{
			while (p)
			{
				s.push(p);
				p = p->left;
			}
			if (!s.empty()) {
				p = s.top();
				s.pop();
				Visit(p);
				p = p -> right;
			}
		}
	}
}

栈内元素的变化情况:空→324→35(输出4和2并压入5)→空(输出5和3)→16→空(输出6和1),得到输出结果4 2 5 3 6 1。使用自定义栈实现二叉树的中序遍历的思路是:当栈不为空或者树不为空时(由于在处理完左子树和根时栈为空而此时右子树还没有处理),将根节点压入栈中,接着不断访问其左孩子并压入栈中直到左孩子为空。然后输出栈顶元素并出栈,同时判断该栈顶元素是否具有右孩子。如果有则将其压入栈中,直到满足最终条件。

(2-3)后序遍历二叉树
先序遍历结果是:3 2 4 5 1 6 , 后序遍历结果是:4 5 2 6 1 3,逆后序遍历结果是:3 1 6 2 5 4。观察先序遍历结果,首先将左子树部分2 4 5和右子树部分1 6交换;并将节点2的左右孩子交换,将节点1的左右孩子交换。得到结果3 1 6 2 5 4,即为逆后序结果。

void postVisitBiTree_(TreeNode* &T) {
	if (T) {
		stack<TreeNode*> s1;
		stack<TreeNode*> s2;
		TreeNode* p;
		s1.push(T);
		while (!s1.empty())
		{
			p = s1.top();
			s1.pop();
			s2.push(p);
			if (p->left != NULL) {
				s1.push(p->left);
			}
			if (p->right != NULL) {
				s1.push(p->right);
			}
		}
		while (!s2.empty())
		{
			p = s2.top();
			s2.pop();
			Visit(p);
		}
	}
}

栈内元素的变化情况:栈1:空→3→21(输出3并压入左孩子2和右孩子1)→26(输出1并压入右孩子6)→45(输出6和2并压入左孩子4和右孩子5)→空(输出5和4),得到输出结果3 1 6 2 5 4;栈2:将先序遍历得到的结果逆序得到4 5 2 6 1 3即得到后序遍历的结果。使用自定义栈实现二叉树的后序遍历的思路是:类似于非递归先序遍历的方法,不同的是在访问栈顶元素时先访问左子树再访问右子树,最后将得到的结果逆序即得到最终结果。

(3)层次遍历二叉树
二叉树的层次遍历为一层一层往下遍历、从左往右遍历二叉树。利用一个队列实现(假设使用的是一个普通队列,即仅限制一端进另一端出且先进先出):将根节点压入队列中,当队列不为空时,元素出队并访问其左孩子和右孩子,如果有则将其依次入队列。重复以上过程直到队列为空。

void levelVisitBiTree(TreeNode* &T) {
	if (T) {
		queue<TreeNode*> q;
		TreeNode* p;
		q.push(T);
		while (!q.empty())
		{
			p = q.front();
			q.pop();
			Visit(p);
			if (p->left != NULL) {
				q.push(p->left);
			}
			if (p->right != NULL) {
				q.push(p->right);
			}
		}
	}
}

队列内元素的变化情况:空→3→21(输出3并压入左子树2和右子树1)→145(输出2并压入2的左孩子4和右孩子5)→456(输出1并压入右孩子6)→空(输出4、5和6),得到输出结果3 2 1 4 5 6。


3. 二叉树的其他操作

二叉树的其他操作大多都是基于二叉树的遍历的,本部分介绍几种二叉树的操作方法。
(1)二叉树的深度
这里给出求二叉树深度的递归方法和非递归方法。
(1-1)递归

int getDepthBiTree(TreeNode* &T) {
	int LD, RD;
	if (T == NULL) {
		return 0;
	}
	else
	{
		LD = getDepthBiTree(T->left);
		RD = getDepthBiTree(T->right);
		return (LD > RD ? LD : RD) + 1;
	}
}

采用递归方法求二叉树的深度时,如果知道左子树的深度和右子树的深度,则二叉树的深度即为左子树深度和右子树深度的最大值加 1 {\rm 1} 。则可以先求出左子树的深度,再求出右子树的深度,然后取二者最大值加 1 {\rm 1} ,这对应于二叉树的后序遍历

(1-2)非递归

int getDepthBiTree_(TreeNode* &T) {
	if (T) {
		queue<TreeNode*> q;
		TreeNode* p, *last;
		int level = 0;
		q.push(T);
		while (!q.empty())
		{
			int nodes = q.size();
			level++;
			while (nodes)
			{
				p = q.front();
				q.pop();
				if (p->left != NULL) {
					q.push(p->left);
				}
				if (p->right != NULL) {
					q.push(p->right);
				}
				nodes--;
			}
		}
		return level;
	}
}

采用非递归方法求二叉树的深度时,采用层次遍历的思想。由于在层次遍历过程中,当前层节点全部出队列后,当前队列里存放的是下一层的全部节点。因此我们可以定义一个变量用于保存二叉树当前层的节点数量,每当当前层二叉树节点遍历完且下一层不为空时将层数加1。
:其他如求二叉树的宽度、判断给定值在二叉树的哪一层等问题都可以用这个思路解决。

(2)打印路径
这里给出打印根节点到所有叶子节点路径的方法,这也是基于二叉树的遍历实现的。

int top = 0;
int path[N];
void getPath(TreeNode* &T) {
	if (T) {
		path[top] = T->val;
		top++;
		if (T->left == NULL && T->right == NULL) {
			for (int i = 0; i < top; i++) {
				cout << path[i];
			}
			cout << endl;
		}
		getPath(T->left);
		getPath(T->right);
		top--;
	}
}

打印路径的思路是:由于二叉树的遍历总是由上而下、由下而上循环访问完成。则可以利用这一特点打印路径,在由上而下时将节点输入一个数组中,在由下而上时输出数组中的元素即可完成路径的打印。首先定义一个存放路径的数组 p a t h {\rm path} 用于存放需要打印的节点, t o p {\rm top} 为数组最后一个元素的索引 1 {\rm -1}


4. 总结

二叉树的大多数算法都是以二叉树的遍历为基础的。由于二叉树的不同遍历方式具有不同的特点,往往可以应用到不同场景。二叉树的先序、中序、后序遍历的顺序是从上到下,再从下到上反复此过程直至结点访问完成,采用非递归实现是需要借助栈。主要应用场景为求二叉树的深度、打印路径等。二叉树的层次遍历以二叉树的每层为单位进行遍历,实现需要借助队列。主要应用场景为求二叉树每层的结点数、二叉树的宽度等。



发布了12 篇原创文章 · 获赞 0 · 访问量 630

猜你喜欢

转载自blog.csdn.net/Skies_/article/details/104321076