目录
前言
我们之前学习了树的基础概念以及二叉堆的实现,那末今天我们来实现二叉树的基本操作。
1.存储结构
顺序结构
二叉树的顺序存储结构就是用一个一维数组来存储二叉树的节点,并且要保持节点之间的位置关系,那也就要求用数组的下标来体现节点之间的逻辑关系,比如:节点之间的父子关系,兄弟关系等。
以一颗完全二叉树为例,它的顺序结构如下图所示:
将这颗二叉树存入数组中,相应的下标对应上面的位置,如下图所示:
当然对于一般的二叉树来说,尽管层序编号不能反映逻辑关系,但是可以将其按完全二叉树编号,只不过,把不存在的节点设置为 “NULL” 而已。如下图,注意虚线结点表示不存在。
考虑一种极端的情况,一棵深度为 k 的右斜树,它只有 k 个结点,却需要分配 2^ k - 1
个存储单元空间,这显然是对存储空间的浪费,例如下图所示。
所以,顺序存储结构一般只用于完全二叉树。
链式结构
既然顺序存储适用性不强,我们就要考虑链式存储结构。二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域 是比较自然的想法,我们称这样的链表叫做二叉链表。
结点结构图如下表所示。
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
其中,data 是数据域;left 和 right 都是指针域,分别存放指向左孩子和右孩子的指针。
逻辑图如下:
2.构建一颗二叉树
在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,当代码需要调式改错时,也可以手搓一个二叉树。
代码示例:
BTNode* Buynode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
{
perror("malloc tail");
exit(-1);
}
node->data = x;
node->left = NULL;
node->right = NULL;
}
BTNode* BinaryTreeCreate()
{
BTNode* node1 = Buynode(1);
BTNode* node2 = Buynode(2);
BTNode* node3 = Buynode(3);
BTNode* node4 = Buynode(4);
BTNode* node5 = Buynode(5);
BTNode* node6 = Buynode(6);
//手动连接节点
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1;
}
我们一般二叉树真正的创建方式是
通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
上面逻辑图是根据分治思路,递归实现的。其中限制条件是:对于当前节点,如果它是空节点或者数组已经遍历完,那么返回NULL。然后创建新节点赋初值当作根,接着递归创建左右子树。
代码示例:
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
//其中,a表示前序遍历的数组,n表示数组的长度,pi表示数组的当前位置。
if (*pi >= n || a[*pi] == '#')
{
(*pi)++;
return NULL;
}
//如果数组已经遍历完或者当前节点是空节点,那么返回空指针。
//否则,创建一个新节点,并将当前位置向后移动一位,然后递归构建左子树和右子树。
//最终返回根节点,即构建好的二叉树。
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
if (root == NULL)
{
perror("malloc fail");
exit(-1);
}
root->data = a[*(pi)++];
root->left = BinaryTreeCreate(a, n, pi);
root->right = BinaryTreeCreate(a, n, pi);
return root;
}
再看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:
- 空树
- 非空:根结点,根结点的左子树、根结点的右子树组成的。
从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。
3.二叉树的遍历
在计算机程序中,遍历本⾝是⼀个线性操作。所以遍历同样具有线性结构的数组或链表,是⼀件轻⽽易举的事情。
对于⼆叉树来说,是典型的⾮线性数据结构,遍历时需要把⾮线性关联的节点转化成⼀个线性的序列,以不同的⽅式来遍历,遍历出的序列顺序也不同.
从节点之间位置关系的⾓度来看,⼆叉树的遍历分为 4 种:
1)前序遍历
2)中序遍历
3)后序遍历
4)层序遍历
从宏观的⾓度来看,⼆叉树的遍历归结为两⼤类:
1)深度优先遍历 (前序遍历、中序遍历、后序遍历)。
2)⼴度优先遍历 (层序遍历)。
深度优先遍历
深度优先和⼴度优先这两个概念不⽌局限于⼆叉树,它们更是⼀种抽象的算法思想,决定了访问某些复杂数据结构的顺序。在访问树、图,或其他⼀些复杂数据结构时,这两个概念常常被使⽤到。
所谓深度优先,顾名思义,就是偏向于纵深,“⼀头扎到底” 的访问⽅式。可能这种说法有些抽象,下⾯就通过⼆叉树的前序遍历、中序遍历、后序遍历 ,来看⼀看深度优先是怎么回事吧。
前序遍历
⼆叉树的前序遍历,输出顺序是:根节点、左⼦树、右⼦树。
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。
代码示例:
void BinaryTreePrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
中序遍历
⼆叉树的中序遍历,输出顺序是:左⼦树、根节点、右⼦树。
规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。
⾸先访问根节点的左孩子,如果这个左孩子还拥有左孩子,则继续深⼊访问下去,⼀直找到不再有左孩子的节点,然后递归打印根节点。显然,第⼀个没有左孩子的节点是节点 4,然后继续递归找以4为根节点的左子树。一层一层往上回归。
代码示例:
void BinaryTreeInOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
BinaryTreePrevOrder(root->left);
printf("%d ", root->data);
BinaryTreePrevOrder(root->right);
}
后序遍历
⼆叉树的后序遍历,输出顺序是:左子树、右子树、根节点。
规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。
代码示例:
void BinaryTreePostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
printf("%d ", root->data);
}
广度优先遍历
如果说深度优先遍历是在一个方向上 “一头扎到底”,那么广度优先遍历则恰恰相反:先在各个方向上各走出 1 步,再在各个方向上走出第 2 步、第 3 步…一直到各个方向全部走完。
层序遍历
层序遍历:设二叉树的根结点所在层数为1,层序遍历就是从所在二叉树的根结点出发,首先访问第一层的树根结点,然后从左到右访问第2层上的结点,接着是第三层的结点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
上图就是⼀个⼆叉树的层序遍历,每个节点左侧的序号代表该节点的输出顺序。
这里我们需要借助之前学过的一个数据结构 队列 来实现层序遍历。
1)先把根入队列,借助队列,先进先出的性质。
2)上一层的节点出的时候,带下一层的节点进去。
3)一直重复,直到队列为空。
先入根节点,此时队列里面是有一个节点的,判断队列不为空,把根节点的数据取出来,然后在把根节点的左孩子节点和右孩子入进去,依次循环。
注意:上一个节点出来以后,把它下面的左右节点带进去。
代码示例:
//层序遍历
void LevelOrder(BTNode* root) {
Queue q;
QueueInit(&q);
//队列不为空,入队列
if (root) {
QueuePush(&q, root);
}
//队列不为空,出队头的数据
while (!QueueEmpty(&q)) {
BTNode* front = QueueFront(&q); //取队头的数据
QueuePop(&q); //然后Pop掉队列里面存的这个节点的指针
printf("%d ", front->data); //然后访问data
if (front->left) { //如果front的左子树不为空,那么就入队列
QueuePush(&q, front->left);
}
if (front->right) { //如果front的右子树不为空,那么就入队列
QueuePush(&q, front->right);
}
}
}
4. 二叉树节点个数
这里可以采用 分治思路, 把复杂的问题,分成更小规模的子问题,子问题再分成更小规模的子问题,直到子问题不可再分割,直接能出结果。
思路:
1)如果是空树,那么就返回 0。
2)如果不是空树,那么就等于:求左子树节点个数+求右子树节点个数+1(这个 1 就是自己)。
代码示例:
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
5.二叉树叶子节点个数
所谓叶子节点,就是 没有任何 子节点 的节点称为叶子节点,也就是度为 0 的节点。
这里还是可以采用分治的思路:
1)若为空树,则叶子结点个数为 0。
2)若结点的左指针和右指针均为空,则叶子结点个数为 1(只剩一个根节点)。
3)除上述两种情况外,说明该树存在子树,其叶子结点个数 = 左子树的叶子结点个数 + 右子树的叶子结点个数。
代码示例:
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);
}
6.二叉树第 k 层节点个数
这里是求第 k 层的节点的个数,k > = 1 (k 从 1 开始的)
思路:
1)空树,返回 0。
2)不是空树,且 k==1,返回 1(求第一层的节点数,那么第一层只有一个根节点)。
3)不是空树,且 k>1,就转换成:左子树 k-1 层节点个数 + 右子树 k-1 层节点个数。
nt 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);
}
7.二叉树的深度
求二叉树的深度,还是采用分治的思想:
1)若为空树,则深度为 0。
2)若不为空树,则深度 = 左右子树中深度最大的值 + 1(为什么要加1呢?因为还有第一层,也就是根节点这一层!)
注意要通过变量来记录左右子树递归的结果,否则后面代码会重复计算,导致效率低下,甚至程序崩溃。
代码示例:
nt TreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
int leftHeight = TreeHeight(root->left);
int rightHeight = TreeHeight(root->right);
return leftHeight > rightHeight ?
leftHeight + 1 : rightHeight + 1;
}
8.二叉树查找值为 x 的节点
二叉树查找值为 x 的节点,然后返回节点的指针。
思路:
1)如果是空树,那么直接返回空。
2)先判断根节点的值是不是要查找的x,如果是,直接返回。
3)如果根节点不是,那么去左子树中查找。
4)如果左子树找不到,那么再去右子树中查找。
代码示例:
//二叉树查找值为x的节点
BTNode* BTreeFind(BTNode* root, BTDataType x) {
//如果根节点为空树,那么就直接返回空
if (root == NULL) {
return NULL;
}
//如果当前的data等于x,那么就返回当前节点指针
if (root->data == x) {
return root;
}
//如果当前节点不是,那么就去左边找,找到了就返回
BTNode* ret1 = BTreeFind(root->left, x);
if (ret1) {
return ret1;
}
//如果左边找不到,就去右边找,找到了就返回
BTNode* ret2 = BTreeFind(root->right, x);
if (ret2) {
return ret2;
}
//如果左边和右边都没有找到,那么就返回空
return NULL;
}
9.二叉树的销毁
二叉树需要注意销毁结点的顺序,遍历时我们选用后序遍历,也就是说,销毁顺序应该为:左子树、右子树、根。
我们必须先将左右子树销毁,最后再销毁根结点;若先销毁根结点,那么其左右子树就无法找到,也就无法销毁了。
代码示例:
void BinaryTreeDestory(BTNode* root)
{
if (root == NULL);
{
return;
}
BinaryTreeDestory(root->left);
BinaryTreeDestory(root->right);
free(root);
}
10.判断二叉树是否是完全二叉树
什么是完全二叉树?
前 n-1 层都是满的,最后一层不满,但是从左到右依次是连续的。
这里还是采用 队列 的思路:
1)层序遍历,空节点也进队列。
2)出到空节点以后,出队列中的所有数据,如果全是空,就是完全二叉树,如果有非空,就不是。
完全二叉树:非空节点都出完了,那么遇到空以后,队列后面肯定全是空。
代码示例:
//判断二叉树是否是完全二叉树
bool BTreeComplete(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);
}
//出break以后,去判断
while (!QueueEmpty(&q)) {
BTNode* front = QueueFront(&q); //取队头的数据
QueuePop(&q);
//如果出到非空,那么说明不是完全二叉树
if (front) {
return false;
}
}
//销毁队列
QueueDestory(&q);
return true;
}
总结
以上就是关于初阶二叉树的基本操作,那么这些的学习都是为了后面的二叉查找树、平衡二叉树、红黑树、B 树以及B+树打基础。