目录
一.简单二叉树的构建
由于现在对二叉树结构掌握还不够深入,为了降低学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。
先按照如下图构建一颗二叉树:
#include <stdio.h>
#include <stdlib.h>
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BinaryTreeNode* left;
BinaryTreeNode* right;
BTDataType data;
}BTNode;
//获取结点
BTNode* BuyBTNode(BTDataType x)
{
BTNode* tmp = (BTNode*)malloc(sizeof(BTNode));//开的是结点的大小,而不是数据的大小
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
BTNode* newnode = tmp;
newnode->data = x;
newnode->left = newnode->right = NULL;
return newnode;
}
//获取一颗二叉树
BTNode* CreateBinaryTree()
{
BTNode* node1 = BuyBTNode(1);
BTNode* node2 = BuyBTNode(2);
BTNode* node3 = BuyBTNode(3);
BTNode* node4 = BuyBTNode(4);
BTNode* node5 = BuyBTNode(5);
BTNode* node6 = BuyBTNode(6);
node1->left = node2;
node2->left = node3;
node1->right = node4;
node4->left = node5;
node4->right = node6;
return node1;
}
int main()
{
BTNode* root = CreateBinaryTree();
return 0;
}
注意:上述代码并不是创建二叉树的方式。
再看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:
- 空树
- 非空:根节点,根节点的左子树、根节点的右子树组成的。
从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。
二. 二叉树的遍历
1. 前序、中序、后序遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉 树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
- 前序遍历(Preorder Traversal 亦称先序遍历,也叫作先根遍历))——访问根结点的操作发生在遍历其左右子树之前,访问顺序——根,左子树,右子树。
- 中序遍历(Inorder Traversal,中根遍历)——访问根结点的操作发生在遍历其左右子树之中(间),访问顺序——左子树,根节点,右子树。
- 后序遍历(Postorder Traversal,后根遍历)——访问根结点的操作发生在遍历其左右子树之后,访问顺序——左子树,右子树,根节点。
由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
// 二叉树前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
// 二叉树中序遍历
void InOrder(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;
}
InOrder(root->left);
InOrder(root->right);
printf("%d ", root->data);
}
下面为分析前序,中序与后序递归遍历的示例图:
以前序遍历为例:
先从根结点开始,打印根节点的值1,然后走到左子树(即值为2),当前的左子树是下面结点的根节点,所以打印当前的值2,然后走到左子树(即值为3),当前的左子树是下面结点的根节点,打印当前的值3,然后走到左子树(即为NULL),当前的左子树是NULL,即返回到上一个根节点(即值为3),然后再走到右节点(即值为NULL),右节点是NULL,回到上一个根节点(即值为3),走完当前的栈帧,回到上一个结点(即值为2的),然后走到它的右子树(即值为NULL),为NULL然后回到上一个根节点(即值为2),当前栈帧走完,回到最初的根节点1,然后走到右子树(即值为4),依旧如此下去。同理,中序遍历和后序遍历也是如此。
2. 层序遍历
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
// 二叉树层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
//创建队列并进行初始化
Queue q;
QueueInit(&q);
//将根节点push
if (root)
QueuePush(&q, root);
//拿出一个根节点,插入两个子节点
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%d ", front->data);
if (front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
printf("\n");
}
需要使用到队列,这里可以使用之前已经写好的(c不像c++有stl库),需要注意的是:我们要在加进来的队列两个文件(.c和.h)中的.h的文件中做一点点修改:
修改typedef中的类型和在其上面增加二叉树结点的声明,修改typedef中的类型是因为我们传给队列的是BTNode*的类型,而增加声明是因为编译器只会向上查找在代码中使用到的类型和函数。
这里使用队列的原因:我们从根节点开始将结点存入队列中,然后取出队头元素,紧接着把根节点的左右子节点依次从队尾存入队列,依次往下,从队头取出根节点再紧接着从队尾插入左右子结点,这样就完成了层序遍历
三. 节点个数以及高度等
接下来的接口需要使用分治的思想。
注:这里其实是分治的思想,分治——把复杂的问题,分成更小规模的子问题,子问题再分成更小的子问题......直到子问题不可再分割,直接能出结果。
1.节点个数:
int BTreeSize(BTNode* root)
{
return root == NULL ? 0 : BTreeSize(root->left) + BTreeSize(root->right) + 1;
}
思路:子问题
1. 空树,最小规模子问题,结点返回0
2. 非空,左子树节点个数+右子树个数+1(自己)
2.二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root->left == NULL && root->right == NULL)
return 1;
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
思路:子问题
1.空树,最小规模子问题,节点返回0
2.非空,左子树+右子树的叶子节点,叶子节点即是当左右子树的子节点都为空时。
3.二叉树的深度
int BinaryTreedepth(BTNode* root)
{
if (root == NULL)
return 0;
int leftdepth = BinaryTreedepth(root->left);
int rightdepth = BinaryTreedepth(root->right);
return leftdepth > rightdepth ? leftdepth + 1 : rightdepth + 1;
}
思路:子问题
1.空树,最小规模子问题,节点返回0
2.非空,比较左子树和右子树的高度,大的+1即可
4.二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
assert(k >= 1);
if (root == NULL)
return 0;
if (k == 1)
return 1;
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
思路:子问题
1.空树,最小规模子问题,节点返回0
2.非空,k==1,返回1
3 .非空,k>1,转换成左子树k-1层节点个数+右子树k-1层节点个数
即我们要找第k层的节点个数,可以从左子树的k-1层开始找,直到k==1时,当前的情况即是第k层,这里的左子树是指当前根节点的左子树,也就是说,从根节点走到左子树上,也就少了一层,第k层就会变成第k-1层,同理继续找左子树的k-1层,直至到k==1时,才是我们要找的第k层,同理右子树的情况也是这样。
5. 二叉树查找值为x的节点
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BTNode* leftres = BinaryTreeFind(root->left, x);
if (leftres != NULL)
return leftres;
BTNode* rightres = BinaryTreeFind(root->right, x);
if (rightres != NULL)
return rightres;
return NULL;
}
思路:子问题
1. 空树,最小规模子问题,找不到,返回NULL
2. 非空,使用前序遍历,从当前的根节点开始找,找不到去左子树找,找到了则直接返回节点,不再需要去右子树找,找不到去右子树找,右子树都找不到则返回NULL。
6. 判断二叉树是否是完全二叉树
//判断是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)
break;
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front != NULL)
return false;
}
return true;
}
这里需要使用到队列,因为判断的本质是层序遍历,只是在层序遍历进行了修改,这里存储进队列有NULL,左右子节点是NULL也要存入进队列,当我们从队头取元素时,如果取到了NULL就跳出循环,判断队列中当前NULL的后面是否还有非NULL元素,如果有的话说明不是完全二叉树,否则是完全二叉树。
如下图:
我们将1存入队列,取出1,再将2和4存入队列,取出2,将3和NULL存入4的后面,取出4,将5和6存入3和NULL的后面,取出3,将NULL和NULL存入5和6的后面,这时就会取到NULL,即跳出循环,进到下面的循环进行判断,由于上面拿到NULL后就会删除队头元素,下面的循环一进来就是5,说明NULL的后面有非空元素,所以不是完全二叉树,如图所示,2的右子节点因为为空,所以这颗二叉树不是完全二叉树。
四.二叉树的创建和销毁
1. 二叉树的销毁
//二叉树的销毁
void BinaryTreeDestroy(BTNode* root)
{
//后续遍历销毁
if (root == NULL)
return;
BinaryTreeDestroy(root->left);
BinaryTreeDestroy(root->right);
free(root);
}
二叉树的销毁我们采用的是后续遍历方式,原因:我们销毁结点应该是从右节点->左节点->根节点的顺序销毁,否则无论是先销毁左节点还是根节点都会有内存泄漏,因为一旦根节点被销毁了,就找不到左右子树了,所以根节点只能最后销毁,而最后遍历根节点的方式就是后序遍历,需要注意的是我们接受的是一级指针,所以调用方需要将传过来的形参置NULL
2. 二叉树的创建
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
//二叉树结点的定义
typedef struct BinaryTreeNode {
char data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
} BTNode;
//创建二叉树
BTNode* CreateBTree(char str[], int* pi)
{
//#代表空
if(str[*pi] == '#')
{
(*pi)++;//'#'是空,即结点为NULL,pi往后走一步
return NULL;
}
//创建结点并把str中的值存入二叉树结点中
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
root->data = str[(*pi)++];
//递归创建左节点和右节点,直到为空时返回
root->left = CreateBTree(str, pi);
root->right = CreateBTree(str, pi);
//将结点返回
return root;
}
通过遍历数组来创建二叉树,采用的是方式是前序遍历的方式来创建二叉树,当为#时说明是空结点,返回NULL,否则就创建根节点,然后递归创建左子树和右子树,最后再将结点返回