前言
我们知道二叉树有两种存储表示方式:1.数组结构;2. 链式结构。 数组表示法用于完全二叉树的存储非常有效,但表示一般二叉树,特别是形态剧烈变化的,存储空间的利用不是很理想。使用链式结构表示,可以克服这些缺点。 这一节介绍二叉树链式结构的实现以及二叉树常见的一些基础问题。
前置声明:
这篇博客将二叉树的创建和销毁放在了二叉树的遍历后面,因为二叉树的创建需要使用到一些关于二叉树遍历的思想。 所以我们需要先学习二叉树的遍历,这里我们先手动创建一个简单的二叉树(该方法不是真正创建二叉树的方法,只是为了方便学习二叉树的遍历而快速简单地创建的一颗二叉树)
#include<stdio.h>
#include<stdlib.h>
typedef int BTDataType;
typedef struct BTNode {
struct BTNode* _left;
struct BTNode* _right;
BTDataType _data;
}BTNode;
BTNode* BuyBTNode(BTDataType data)
{
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
if (newnode == NULL)
{
printf("fail : malloc");
exit(-1);
}
newnode->_left = NULL;
newnode->_right = NULL;
newnode->_data = data;
return newnode;
}
BTNode* CreateBinTree()
{
BTNode* node1 = BuyBTNode(1);
BTNode* node2 = BuyBTNode(2);
BTNode* node3 = BuyBTNode(3);
BTNode* node4 = BuyBTNode(4);
BTNode* node5 = BuyBTNode(5);
BTNode* node6 = BuyBTNode(6);
BTNode* node7 = BuyBTNode(7);
node1->_left = node2;
node1->_right = node3;
node2->_left = node4;
node2->_right = node5;
node3->_left = node6;
node3->_right = node7;
return node1;
}
int main()
{
BTNode * tree = CreateBinTree();
return 0;
}
创建出来的结构是这样的:
二叉树的遍历
二叉树是最基本的树形结构,也是我们重点研究的对象,在二叉树中所有可用的操作中,遍历是最常用的操作,所谓二叉树的遍历,就是遵从某种次序,访问二叉树中的所有节点,使得每一个节点被访问一次,而且只访问一次。
学习遍历二叉树之前,我们需要知道树是一种递归结构,每一颗树都是由三部分组成:根、左子树、右子树。 左子树也是一棵树,同样也被分为了三部分:根、左子树、右子树…
遍历二叉树时中重要的就是递归思想:递归思想是将一个问题一直分解为子问题,直到分解为不可分割的子问题 ,就结束了。
二叉树有四种遍历结构:前序遍历、中序遍历、后序遍历、层序遍历。 前面三种遍历也被称为深度优先遍历,层序遍历也被称为广度优先遍历。
前序遍历
也叫做先根遍历。即一棵树的访问顺序是:根节点、左子树、右子树。
遍历规则是:先访问一棵树的根节点,然后访问到这棵树的左子树、如果该树的左子树已经遍历完全,那么接下来就遍历右子树。访问到空节点或者访问完右子树后才能返回。
我们利用前面实现好的这棵树来作为例子:
这是一颗完全二叉树,每一颗节点中存储的数据都是一个整形数据。
第一步,我们需要访问这棵树的根节点,即节点1 (在演示过程的时候根节点都使用淡蓝色来注明)。
第二步:访问这棵树的左子树:在访问左子树的时候,我们需要先将整个左子树看作一颗完整的树,然后遍历完这棵树中所有数据。
同样,需要先访问这棵树的根节点:节点2 ,然后我们还需要去访问节点2 的左子树:以节点4 为根节点的树。
和每一个节点一样,节点4也有两个指针用来指向它的左右子树,但是因为这个节点是叶子节点,所以这两个指针都是空指针。
思考:我们知道节点4 和节点5 都是叶子节点,在先序遍历节点2为根节点的树的过程中:遍历的顺序是 2、4、5。但是程序是怎样判断出2的左子树已经遍历完全了呢?(换一种问法:遍历函数在什么情况下会返回呢?) 其实,遍历函数在两种情况下会返回:
- 遍历的时候遇到了空指针。例如,当我们遍历节点4的左子树的时候,发现指向做指数的指针是空指针,表示该节点没有左子树,此时就需要返回。
- 遍历完全的时候。当程序遍历完一棵树的左右子树的时候就需要返回。例如,程序遍历以节点2 为根节点的树的时候,如果左子树4 与右子树5 都已经遍历完全的时候,就需要返回上一级遍历。
如图,节点4 的左右指针为空指针,在遍历这一颗树的时候,首先访问了根节点:节点4 ,然后访问左子树:空指针。当出现空指针的时候,就说明节点4 已经没有左子树需要遍历。我们接下来需要遍历节点4 的右子树,指向右子树的指针也是空指针,所以节点4 也没有右子树需要遍历。这时,我们就可以返回上一层遍历了。
我们利用函数栈帧的知识来解释(每一次函数调用都会创建一个函数栈帧):
这时就代表一节点4 为根节点的树已经遍历完全了。同时也代表了以节点2 为根节点的树的左子树遍历完全,接下来可以遍历该树的右子树:以节点5为根节点的树。
下面是前序遍历的代码实现:(为了可以更明确的看出整个过程,这段代码将遍历到的空指针也打印出来了) 这里我们采用打印的方法来遍历整棵树:
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->_data);
PrevOrder(root->_left);
PrevOrder(root->_right);
}
代码的逻辑:
dayin 打印(遍历)的结果:
可以利用这张图片去看到完整的遍历过程:(n表示空指针)
中序遍历
中序遍历和先序遍历的唯一区别就是访问顺序。中序遍历的访问顺序是:左子树、根、右子树。
访问每一棵树的时候都需要先遍历该树的左子树。左子树遍历完成后访问根节点,然后遍历这棵树的右子树。
void InvOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->_left);
printf("%d ", root->_data);
InOrder(root->_right);
}
这里同样利用这样一棵树来解释遍历的过程:
完整的遍历过程:
后续遍历
后序遍历和前面两种方式类似,区别就是:根节点的访问是在遍历完左右子树后。
所以后续遍历的顺序是:左子树、右子树、根。
void PostOrder(BTNOde* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->_left);
PostOrder(root->_right);
printf("%d ", root->_data);
}
层序遍历
二叉树的层序遍历就是从搜哦在的根节点出发,首先访问第一层的 节点,然后从左向右访问第二层的节点接着是第三层的节点,以此类推。
要实现层序遍历,我们需要利用队列的思想:从第一个节点开始,进入队列,然后再弹出队列头的时候,将左子树和右子树也放入队列。
代码的实现:
void levelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
QueuePush(&q, root);
while ( !QueueEmpty(&q))
{
BTNode* top = QueueFront(&q);
if (top)
{
printf("%c ", top->data);
QueuePush(&q, top->left);
QueuePush(&q, top->right);
}
QueuePop(&q);
}
}
二叉树的创建和销毁
二叉树的创建
在已经了解过二叉树的几种遍历思想后,我们可以来看看怎样创建出一个二叉树:
在二叉树的创建过程中也是采用了递归的思路:确定二叉树的根节点前需要先确定好二叉树的左右子树。
这里是通过一个字符串来确定二叉树的结构的(字符串是先序遍历的结果) 比如这样:
利用递归的思想:我们需要返回一个根节点,根节点的左子树指针需要得到左子树的地址,右子树指针需要得到右子树的地址。
BTNode* CreateBinTree(char* arr, int* pi)
{
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
root->data = arr[(*pi)++];
if (root->data == '#')
return NULL;
root->left = CreateBinTree(arr, pi);
root->right = CreateBinTree(arr, pi);
return root;
}
这段代码的逻辑:
二叉树的销毁
二叉树的销毁也是利用了递归的思想:和后序遍历很类似:我们需要先确定两个子树的空间已经被释放了,然后再去释放掉自身的空间:
void BTreeDestory(BTNode* root)
{
if (root == NULL)
return;
BTreeDestory(root->left);
BTreeDestory(root->right);
free(root);
}
二叉树中的常见问题
计数问题
计算树中节点的个数
对于二叉树,计算树中节点的总数有两种方法:
-
利用一个全局变量,在遍历的时候计数 void BTreeSize(BTNode* root, int* pCount)
{
if (root == NULL)
return;
++(*pCount);
BTreeSize(root->left, pCount);
BTreeSize(root->right, pCount);
}
但是这种方法在计算的时候需要一个整形数据的地址作为参数,否则就不能有效地保存计算后的结果。 调用方法: int main()
{
int count = 0;
BTreeSize(tree, &count);
printf("这棵树中有 %d 个节点",count);
return 0;
}
-
利用分支的思想,树种节点的总数等于左子树中节点数量加右子树节点数量+1
s
u
m
总
=
n
u
m
左
+
n
u
m
右
+
1
(
自
身
sum_总 = num_左 + num_右 + 1(自身
sum总?=num左?+num右?+1(自身 int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 :
BinaryTreeSize(root->_left)
+ BinaryTreeSize(root->_right)
+ 1;
}
计算树中叶子节点的个数
计算树中叶子节点的方法和计算所有节是一样的。有两种方法(主要是介绍第二种方法):
-
遍历整个树,找到满足条件的节点,然后利用全局变量去存储起来。 -
利用分治的思想:一棵树中的所有叶子结点的数量等于这棵树中左子树中的所有叶子节点的数量加上右子树中所有叶子节点的数量。
s
u
m
总
=
n
u
m
左
+
n
u
m
右
sum_总 = num_左 + num_右
sum总?=num左?+num右? int LeafBTNodeSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->_left == NULL && root->_right == NULL)
return 1;
return LeafBTNodeSize(root->_left) + LeafBTNodeSize(root->_right);
}
计算二叉树的高度(深度)
前提:以根节点为第一层。
计算二叉树的高度的时候,我们可以使用深度优先遍历+加上分治的思想:整棵树的高度等于高度最高的子树+1;
int BTreeDeepth(BTNode* root)
{
if (root == NULL)
return 0;
int leftdeepth = BTreeDeepth(root->_left);
int rightdeepth = BTreeDeepth(root->_right);
return leftdeepth > rightdeepth ? leftdeepth + 1 : rightdeepth + 1;
}
计算第K层有多少节点
同样,分治的思想可以计算出二叉树中第K层的节点数量:
第K层的节点数量等于:左子树中第K-1 层的节点数量 + 右子树中第K-1 层的节点数量。
int BTreeKLevelSize(BTNode* root,int k )
{
assert(k >= 1);
if (root == NULL)
return 0;
if (k == 1)
return 1;
return BTreeKLevelSize(root->_left, k - 1) + BTreeKLevelSize(root->_right, k - 1);
}
判断问题
判断完全二叉树
判断完全二叉树时需要使用到层序遍历的思想: 首先需要将根节点存储在队列中,然后开始循环,循环的内部是:弹出队列头的节点,并且将这个节点中用来指向左右子树的两个指针存储在队列中。如果某一次弹出的队列头是一个空节点,那么我们就可以通过检查队列中是否还有非空的指针。
如果我们判断一棵树是不是完全二叉树,只需要判断在第一次出现看那个指针后,队列中是否全是空指针。如果是,那么这棵树就是完全二叉树,反之,这棵树就不是完全二叉树。
代码实现:
bool isCompBinTree(BTNode* root)
{
Queue q;
QueueInit(&q);
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* top = QueueFront(&q);
if (top)
{
QueuePush(&q, top->left);
QueuePush(&q, top->right);
}
else
break;
QueuePop(&q);
}
while (!QueueEmpty(&q))
{
BTNode* top = QueueFront(&q);
if (top != NULL)
return false;
QueuePop(&q);
}
return true;
}
判断单值二叉树
二叉树中的每一个节点存储的数据都相同,这样的树就是单值二叉树。
我们怎样判断出一棵树是不是单值二叉树呢?
利用分治的思想:我们可以对整棵树进行一次深度优先搜索,如果是一颗棵单值二叉树,那么这棵树的任何一颗子树都是单值二叉树。
利用树的特点:每一次都去比较根节点和左右子树的值,如果值相同,就继续向下搜索,如果出现了两个不相等的值,那么就可以不再遍历下去,直接返回false
bool isUnivalTree(struct TreeNode* root){
if (root == NULL)
return true;
if ((root->left != NULL && root->val != root->left->val)
|| (root->right != NULL && root->val != root->right->val))
return false;
return isUnivalTree(root->left)
&& isUnivalTree(root->right);
}
代码段中的|| 与&& :
if ((root->left != NULL && root->val != root->left->val)
|| (root->right != NULL && root->val != root->right->val))
return false;
这里使用 或|| ,是因为在判断根节点是否和左子树值相等的时候,必须满足同时相等才能返回真。
return isUnivalTree(root->left)
这里是用与&& ,是因为只有满足根节点的左右子树都是单值二叉树的时候才能返回真。
判断相同二叉树
两个二叉树相同,当且仅当两个二叉树的结构完全相同,且所有对应节点的值相同。判断两个数是否完全相同,首先需要判断两棵树的根节点是否相同,然后再判断两棵树的左右子树是否相等。我们可以使用深度优先遍历去比较两棵树中的每一颗子树是否相等。
判断根节点是否相同时,我们只需要找出不相同的情况,返回false,剩下的情况都返回true就行了。
- 返回false:
- 两个根节点中有一个是空指针
- 两个根节点的值不同
- 返回true:
- 当两个根节点都是空指针的时候就返回真
bool isSameTree(struct TreeNode* p, struct TreeNode* q){
if(p == NULL && q ==NULL)
return true;
if(p == NULL ||q == NULL)
return false;
if(p->val != q->val)
return false;
return isSameTree(p->left,q->left)
&& isSameTree(p->right,q->right);
判断对称二叉树
判断一棵树是否是对称二叉树,首先需要确定这棵树不是空树。然后去比较这棵树的左子树和右子树
两棵树镜像的特点:
- 这两棵树的根节点值相同
- 这两棵树的所有子树相互对称(A树的左子树根节点的值等于B树右子树根节点的值、A树的右子树根节点的值等于B树左子树根节点的值)。
我们可以这样实现:
bool _isSymmetricTree(BTNode* p, BTNode* q)
{
if (p == NULL && q == NULL)
return true;
if (p == NULL || q == NULL)
return false;
return p->data == q->data
&& _isSymmetricTree(p->left, q->right)
&& _isSymmetricTree(p->right, q->left);
}
bool isSymmetricTree(BTNode* root)
{
if (root == NULL)
return true;
return _isSymmetricTree(root->left, root->right);
}
另一棵树的子树
这个问题里面我们会得到两棵树:root和subroot.我们需要判断subroot树是不是root树的子树。
其实这个问题和判断两棵树相等比较类似,差别在于: 如果我们判断两棵树完全相同,我们只需要利用深度优先遍历去比较两棵树中的每一颗子树。 如果我们需要判断一棵树是不是另一个树的子树,那么我们就需要利用root树中的每一个子树去和subroot做比较,如果存在root树种的一棵子树和subroot完全相同,那么就可以判断出subroot是root树的子树。
这个问题的关键是找到root树中的每一颗子树,也就是需要遍历这一颗树,我们可以采用深度优先遍历的方法: 首先判断root和subroot是否相同,然后判断root的左右子树和subroot是否完全相同,这样递归下去,如果有一次存在完全相同的情况,那么就可以返回true,并且终止递归;其他情况都返回false,直到遍历结束。
bool issametree(struct TreeNode* p,struct TreeNode *q)
{
if(p == NULL && q == NULL)
return true;
if(p == NULL || q == NULL)
return false;
return p->val == q->val
&& issametree(p->left,q->left)
&& issametree(p->right,q->right);
}
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){
if(root == NULL)
return false;
return issametree(root,subRoot)
|| isSubtree(root->left,subRoot)
|| isSubtree(root->right,subRoot);
}
|