typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
再看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:
BTNode* BuyNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
{
perror("malloc fail");
return NULL;
}
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
malloc
函数为新节点分配内存,其大小为 BTNode
结构体的大小malloc
返回 NULL
,表示内存分配失败,使用 perror
函数输出错误信息,并返回 NULL
x
赋值给新节点的 data
成员left
和 right
都初始化为 NULL
,表示该节点暂时没有左右子节点在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。
BTNode* CreatTree()
{
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;
}
由于现在对二叉树结构掌握还不够深入,为了降低学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
前序遍历
依据根
、左子树
、右子树
的顺序,前序遍历
又叫做深度优先遍历(DFS)
其本质上是一个有限递归
的过程,当左节点
递归到最后一个叶节点
时,其子节点
为 NULL
,向下递归
就结束,然后开始回退遍历右节点
🔥值得注意的是: root == NULL
的 if
条件句既是为了表示空树的情况,也是为了结束向下递归的过程
💻测试结果:
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;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
后序遍历
依据左子树
、右子树
、根
的顺序
其余操作和前序遍历一致
💻测试结果:
void TreeSize(BTNode* root, int* psize)
{
if (root == NULL)
{
return;
}
++(*psize);
TreeSize(root->left, psize);
TreeSize(root->right, psize);
}
获取节点个数首先想到的肯定是遍历二叉树,如上代码所示的方法,用一个移动指针遍历,每到一个节点就 ++ ,这固然可行,但是有更直观简洁的方法
⌨️优化代码:
int TreeSize(BTNode* root)
{
return root == NULL ? 0 :
TreeSize(root->left)
+ TreeSize(root->right)
+ 1;
}
这种模式就像学校里的人数统计一样,假设想要统计全校寝室人数
这是一种清晰的统计流程,二叉树结点个数就是从最底下逐渐往上加和进行统计
int TreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
return TreeHeight(root->left) > TreeHeight(root->right)
? TreeHeight(root->left) + 1 : TreeHeight(root->right) + 1;
}
二叉树高度获取
的思想和二叉树结点个数
是一致的,高度取决于左子树
和右子树
路径较长的那条,直接比较然后递归取值即可
但是这段代码有个问题,比较时需要递归一次
,比较完并没有保存每个节点高度的结果,获取结果的时候又要再递归一次
,导致代码效率减慢
⌨️优化代码:
int 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;
}
所以每次向上回退时,保存每个节点的高度结果即可
int TreeKLevel(BTNode* root, int k)
{
if (root == NULL)
return 0;
if (k == 1)
return 1;
int leftK = TreeKLevel(root->left, k - 1);
int rightK = TreeKLevel(root->right, k - 1);
return leftK + rightK;
}
还是利用递归的思想,假设要获取第三层的节点个数
,那么需要向上递归两次
到达根节点,所以是 k - 1
次
从下往上数层数
1
层,对于根节点的左子节点和右子节点,它们处于第 2
层,距离第 3
层还有 1
层,所以递归调用 TreeKLevel(root->left, 2)
和 TreeKLevel(root->right, 2)
2
层的节点时,对于它们的子节点(即第 3
层的节点),再次递归调用时传入 k - 1
即 1
,此时满足 k == 1
的条件,返回 1
表示找到了一个第 3
层的节点
k - 1
作为新的层数参数,最终可以准确计算出二叉树中第 k
层的节点总数
BTNode* TreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BTNode* lret = TreeFind(root->left, x);
if (lret)
return lret;
BTNode* rret = TreeFind(root->right, x);
if (rret)
return rret;
return NULL;
}
if
条件都不满足,说明当前节点不是目标节点,需要继续在其左子树中查找。递归调用 TreeFind(root->left, x)
来查找左子树中是否存在值为 x
的节点,并将返回结果存储在 lret
中。如果 lret
不为 NULL
,说明在左子树中找到了目标节点,直接返回 lret
lret
为 NULL
),则继续在右子树中查找。递归调用 TreeFind(root->right, x)
来查找右子树中是否存在值为 x
的节点,并将返回结果存储在 rret
中。如果 rret
不为 NULL
,说明在右子树中找到了目标节点,直接返回 rret
x
的节点,说明整个二叉树中不存在该节点,函数最终返回 NULL
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
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);
}
QueueDestroy(&q);
}
层序遍历
就是从上到下
,从左到右
进行遍历,层序遍历
也叫作广度优先遍历(BFS)
这里需要利用队列的特性
来进行,出上一层,带入下一层(每出一个节点,就插入该节点的子节点)引入队列的头文件和源文件
🔥值得注意的是: BTNode* front = QueueFront(&q)
保存了二叉树的节点作为队列的头节点,释放时并不会影响到二叉树本身,而是释放队列头节点
bool TreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)
break;
else
{
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
}
while (QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front)
{
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
我们要知道一个特性:完全二叉树的非空节点一定是连续的
第一个循环是层序遍历二叉树
,直到遇到第一个空就停下来,第二个循环是检查队列中剩余的节点是否都为空
,继续遍历队列中剩余的节点,如果遇到非空节点,说明该二叉树不是完全二叉树,返回 false
;如果队列中剩余的节点都为空,说明该二叉树是完全二叉树,返回 true
void TreeDestroy(BTNode* root)
{
if (root == NULL)
return;
TreeDestroy(root->left);
TreeDestroy(root->right);
free(root);
root == NULL;
}