二叉树链式结构的实现
1.二叉树
在看二叉树基本操作前,首先回顾下二叉树的概念,二叉树是:
- 空树
- 非空:根节点,根节点的左子树、根节点的右子树组成的。
从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。
2.二叉搜索树
二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。
3. 二叉树的遍历
3.1 前序、中序、后序遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。按照规则,二叉树的遍历有:前序/中序/后序/层序的递归结构遍历:
- 前序遍历(Preorder Traversal 亦称先根遍历)——访问根结点的操作发生在遍历其左右子树之前。(根、左子树、右子树)。
- 中序遍历(Inorder Traversal 亦称中根遍历)——访问根结点的操作发生在遍历其左右子树之中(间)。(左子树、根、右子树)。
- 后序遍历(Postorder Traversal 亦称后根遍历)——访问根结点的操作发生在遍历其左右子树之后。(左子树、右子树、根)。
// 二叉树前序遍历
void PreOrder(BTNode* root);
// 二叉树中序遍历
void InOrder(BTNode* root);
// 二叉树后序遍历
void PostOrder(BTNode* root);
下面主要分析前序递归遍历,中序与后序图解类似。
前序遍历递归图解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BIr3HkKm-1680322513229)(null)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8cy7fBGY-1680322513211)(null)]
// 二叉树前序遍历
void PrevOrder(BTNode* root)
{
if (root == NULL){
printf("NULL ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(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;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
遍历结果:
// 前序遍历结果
1 2 3 NULL NULL NULL 4 5 NULL NULL 6 NULL NULL
// 中序遍历结果
NULL 3 NULL 2 NULL 1 NULL 5 NULL 4 NULL 6 NULL
// 后序遍历结果
NULL NULL 3 NULL 2 NULL NULL 5 NULL NULL 6 4 1
3.2 层序遍历
层序遍历:除了前序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。
设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
// 层序遍历
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
QueuePop(&q);
if(front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
QueueDestroy(&q);
}
4. 二叉树节点个数
求二叉树节点个数,我们会想到递归这棵树
int TreeSize(BTNode* root)
{
if(root == NULL)
return;
int size = 0;
size++;
TreeSize(root->left);
TreeSize(root->right);
return size;
}
但是,这样写是正确的吗?我们知道每一个函数都建立在一个栈帧上,函数执行完毕,栈帧销毁。我们在函数中定义了size
,但每次进入函数遍历时,都会重新定义size
,所以我们无法通过这种方式来计算节点个数,而需要将size
定义为全局变量。
int size = 0;
void TreeSize(BTNode* root)
{
if(root == NULL)
return;
size++;
TreeSize(root->left);
TreeSize(root->right);
}
注意:将size
定义为static
静态变量也无法计数,因为静态变量在第二次初始化时并不会执行,当多次调用该函数size
会不断地累加。
5. 二叉树的叶子节点个数
在节点不为空的情况下,判断该节点是否为叶子节点(即该节点的左右子树都为空),如果是叶子节点则+1,递归实现。
int TreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
return (root->left == NULL && root->right == NULL) ?
1 : TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
6. 二叉树的高度/深度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UCpdZkqg-1680322513189)(null)]
我们可以发现,一棵树的高度 = 左子树和右子树高度较大值 + 1。当遇到空的时候,对应的高度就是0,递归就可以返回。
拿上图举例子,这棵树的高度 = 两棵子树中较大的高度(4)+ 1 == 5
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;
}
需要注意的是:要将左右子树的高度定义,这样可以提高效率,如果直接返回结果,每一次都需要重新计算。
7. 求第k层节点个数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VdVrNDuK-1680322513200)(null)]
首先,我们从上往下依次设置为第1,2,3,4层。
假如我们要求解第1层,k == 1的节点个数,我们可以清楚地直到只有1个,也就是根节点。
那求解第2层,k == 2时呢?第2层是相对于对于根节点来说的,但假如把它们也看作树,它们就是自己树的根节点,对应的是它们自己的第1层。依次递归,将每一层都看作自己的根节点,也就是1
因此,第k层的节点个数 = 左子树第k-1层的个数 + 右子树第k-1层的个数。(k>1)
int TreeLevelSize(BTNode* root, int k)
{
if (root == NULL)
return 0;
if (k == 1)
return 1;
return TreeLevelSize(root->left, k - 1) + TreeLevelSize(root->right, k - 1);
}
8. 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BTNode* ret1 = TreeFind(root->left, x);
if (ret1)
return ret1;
BTNode* ret2 = TreeFind(root->right, x);
if (ret2)
return ret2;
}