简介
本文介绍关于二叉树头文件的基本信息,主要包括二叉树的构建和二叉树的遍历等。这里采用的二叉树存储方式是链式存储结构,其他存储方法还有顺序存储方式等。
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代表空结点,在输入时按照完全二叉树输入即可,叶子节点非空时需要输入左右子节点为空( )。即:
2. 二叉树的遍历
本部分介绍二叉树的递归遍历、非递归遍历、层次遍历。
(1)递归
void preVisitBiTree(TreeNode* &T) {
if (T) {
Visit(T);
preVisitBiTree(T->left);
preVisitBiTree(T->right);
}
}
这里是先序遍历二叉树的形式,中序遍历方法和后序遍历方法同先序遍历类似(参考各种构建二叉树的方法)。
针对上述图1中的二叉树,先序遍历结果是: ;中序遍历结果是: ;后序遍历结果是: 。
(2)非递归
递归遍历二叉树的方法是采用系统栈的方法,这里采用非递归的方法也是采用栈这种数据结构。栈的特点是后进先出。
(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-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--;
}
}
打印路径的思路是:由于二叉树的遍历总是由上而下、由下而上循环访问完成。则可以利用这一特点打印路径,在由上而下时将节点输入一个数组中,在由下而上时输出数组中的元素即可完成路径的打印。首先定义一个存放路径的数组 用于存放需要打印的节点, 为数组最后一个元素的索引 。
4. 总结
二叉树的大多数算法都是以二叉树的遍历为基础的。由于二叉树的不同遍历方式具有不同的特点,往往可以应用到不同场景。二叉树的先序、中序、后序遍历的顺序是从上到下,再从下到上反复此过程直至结点访问完成,采用非递归实现是需要借助栈。主要应用场景为求二叉树的深度、打印路径等。二叉树的层次遍历以二叉树的每层为单位进行遍历,实现需要借助队列。主要应用场景为求二叉树每层的结点数、二叉树的宽度等。