前言
引言
在数据结构的浩瀚星空中,二叉树如同一颗璀璨的明珠,其优雅的结构和强大的功能使其成为计算机科学中不可或缺的工具。从数据库索引到编译器的语法树,二叉树以其独特的方式支撑着许多核心算法与数据处理。本文将深入探讨如何使用C语言实现二叉树的链式结构,并详细讲解各个部分的实现。
一、二叉树的结构
1.1结构
二叉树(binary tree)是一种非线性数据结构,代表“祖先”与“后代”之间的派生关系,体现了“一分为二” 的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含值、左子节点引用和右子节点引用。
//二叉树节点结构 typedef int DataType; typedef struct BinaryTreeNode { int data;//值域 struct BinaryTreeNode* left;//指向当前节点左孩子 struct BinaryTreeNode* right;//指向当前节点右孩子 }BTNode;
每个节点都有两个引用(指针),分别指向左子节点(left‑child node)和右子节点(right‑child node),该节点被称为这两个子节点的父节点(parent node)。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的左子树(left subtree),同理可得右子树(right subtree)。
编辑
1.2链式实现的优势
选择链式实现二叉树有几个显著的优点:
- 动态大小:链式结构能够根据需要动态分配内存,避免了固定大小的限制,适合处理不确定数量的数据。
- 节省空间:链式结构只为实际存在的节点分配内存,减少了空间浪费,相比数组实现更加高效。
- 简化操作:插入和删除操作无需移动其他元素,操作更为高效,使得二叉树在频繁变动的数据环境中表现出色。
二、二叉树的基本操作
⼆叉树的创建⽅式⽐较复杂,为了更好的步⼊到⼆叉树内容中,我们先⼿动创建⼀棵链式⼆叉树:
BTNode* buyNode(DataType x) { BTNode* newnode = (BTNode*)malloc(sizeof(BTNode)); if (newnode == NULL) { perror("malloc fail!"); exit(1); } newnode->data = x; newnode->left=newnode->right = NULL; } BTNode* CreateTree() { BTNode* n1 = buyNode(1); BTNode* n2 = buyNode(2); BTNode* n3 = buyNode(3); BTNode* n4 = buyNode(4); BTNode* n5 = buyNode(5); BTNode* n6 = buyNode(6); n1->left = n2; n1->right = n4; n2->left = n3; n4->left = n5; n4->right = n6; return n1; }
编辑
2.1前序遍历
前序遍历是一种深度优先遍历方式,按照“根节点 -> 左子树 -> 右子树”的顺序访问树中的节点。它是树结构中非常重要的一种遍历方式,常用于复制树结构和表达式树的解析等场景。
访问顺序
- 首先访问当前节点(根节点)。
- 然后递归访问左子树。
- 最后递归访问右子树。
应用场景
- 生成树的序列化表示。
- 复制树结构。
- 在表达式树中用于构建前缀表达式。
递归实现:
//前序遍历 //访问优先级:根节点 -> 左子树 -> 右子树 void PreOrder(BTNode* root) { if (root == NULL) { return; } printf("%d ", root->data); PreOrder(root->left); PreOrder(root->right); }
编辑
在这个实现中,我们首先检查当前节点是否为空。如果不为空,我们打印当前节点的数据,然后递归地访问左子节点和右子节点。这种顺序确保了根节点总是最早被访问,对于许多应用场景非常有效。
2.2中序遍历
中序遍历是一种深度优先遍历方式,按照“左子树 -> 根节点 -> 右子树”的顺序访问树中的节点。它在树结构中扮演着重要的角色,常用于生成排序的节点序列以及表达式树的解析。
访问顺序
- 首先递归访问左子树。
- 然后访问当前节点(根节点)。
- 最后递归访问右子树。
应用场景
- 生成有序的节点序列。
- 在表达式树中用于构建中缀表达式。
递归实现:
//中序遍历 //访问优先级:左子树 -> 根节点 -> 右子树 void InOrder(BTNode* root) { if (root == NULL) { return; } InOrder(root->left); printf("%d ", root->data); InOrder(root->right); }
编辑
在这个实现中,我们首先检查当前节点是否为空。如果不为空,我们递归地访问左子节点,接着打印当前节点的数据,最后递归地访问右子节点。这种顺序确保了我们在访问节点时能够得到有序的结果。
2.3后序遍历
后序遍历是一种深度优先遍历方式,按照“左子树 -> 右子树 -> 根节点”的顺序访问树中的节点。这种遍历方式在某些情况下非常有用,尤其是在需要先处理子节点再处理父节点的场景中。
访问顺序
- 首先递归访问左子树。
- 然后递归访问右子树。
- 最后访问当前节点(根节点)。
应用场景
- 计算树的高度或大小。
- 删除树结构时,确保先删除子节点。
- 在表达式树中用于构建后缀表达式。
递归实现:
//后序遍历 //访问优先级:左子树 -> 右子树 -> 根节点 void PostOrder(BTNode* root) { if (root == NULL) { return; } PostOrder(root->left); PostOrder(root->right); printf("%d ", root->data); }
编辑
在这个实现中,我们首先检查当前节点是否为空。如果不为空,我们递归地访问左子节点,然后递归地访问右子节点,最后打印当前节点的数据。这种顺序确保了我们在访问节点时能够正确处理所有子节点之后再处理父节点,适用于需要反向处理的场景。
2.4层序遍历
层序遍历是一种广度优先遍历方式,按照“从上到下、从左到右”的顺序访问树中的节点。它在需要逐层处理节点的场景中非常有用,例如按层打印树或计算树的深度。
步骤如下:
1.初始化一个队列,将根节点入队。
2.当队列不为空,重复以下操作:
- 从队列中出队一个节点并访问(打印或记录)。
- 将该节点的左子节点和右子节点(如果存在)入队。
编辑
代码实现:
//层序遍历 借助数据结构--队列 void LevelOrder(BTNode* root) { QU q;//创建一个队列 QueueInit(&q); QueuePush(&q, root);//根节点入队,成为队头 while (!QueueEmpty(&q))//队列不为空 { //取队头,打印 BTNode*Qhead=QueueFront(&q); printf("%d ", Qhead->data); QueuePop(&q);//队头出队 //队头节点左孩子入队 if (Qhead->left) { QueuePush(&q, Qhead->left); } //队头节点右孩子入队 if (Qhead->right) { QueuePush(&q, Qhead->right); } } QueueInit(&q); QueueDestroy(&q); }
2.5求二叉树节点个数
步骤如下:
1.如果当前节点为空,返回0(没有节点)。
2.如果当前节点不为空,返回1(当前节点)加上左子树和右子树的节点个数。
代码实现:
int BTSize(BTNode* root) { if (root == NULL) { return 0; } return BTSize(root->left) + BTSize(root->right) + 1; }
2.6求二叉树叶子节点数
- 叶子节点:没有子节点的节点。
- 非叶子节点:至少有一个子节点的节点。
步骤如下:
1.如果当前节点为空,返回0(没有叶子节点)。
2.如果当前节点是叶子节点(左右子节点均为空),返回1。
3.如果当前节点不是叶子节点,递归计算左子树和右子树的叶子节点个数,并将结果相加。
代码实现:
//⼆叉树叶子结点个数 叶子节点:度为0的节点 int BTLeafSize(BTNode* root) { if (root == NULL) { return 0; } if (root->left == NULL && root->right == NULL) { return 1; } return BTLeafSize(root->left) + BTLeafSize(root->right); }
2.7求⼆叉树第k层结点个数
步骤如下:
1.如果当前节点为空,返回0。
2.如果当前层数等于k,返回1。
3.如果当前层数小于k,递归计算左子树和右子树第k层的节点个数。
代码实现:
//⼆叉树第k层结点个数 int BTLevelKSize(BTNode* root, int k) { if (root == NULL) { return 0; } if (k == 1) { return 1; } return BTLevelKSize(root->left, k - 1) + BTLevelKSize(root->right, k - 1); }
2.8求⼆叉树的高度
步骤如下:
1.如果当前节点为空,返回0(树的高度为0)。
2.递归计算左子树和右子树的高度。
3.返回左子树和右子树高度中的较大值,加1(表示当前节点的高度)。
代码实现:
//⼆叉树的深度/高度 int BTDepth(BTNode* root) { if (root == NULL) { return 0; } //返回左右子树最深的那个 return BTDepth(root->left) > BTDepth(root->right) ? BTDepth(root->left) + 1 : BTDepth(root->right) + 1; }
2.9二叉树查值
步骤如下:
1.如果当前节点为空,返回 NULL(未找到)。
2.如果当前节点的值等于 x,返回当前节点。
3.如果当前节点的值不等于 x,递归查找左子树和右子树。
4.如果在左子树中找到,返回该节点;否则,继续在右子树中查找。
代码实现:
//⼆叉树查找值为x的结点 BTNode* BTFind(BTNode* root, DataType x) { if (root == NULL) { return NULL; } if (root->data == x) { return root; } BTNode* leftFind=BTFind(root->left, x);//左子树中找 if (leftFind) { return leftFind;//左子树找到了,返回 } BTNode* rightFind=BTFind(root->right, x);//右子树中找 if (rightFind) { return rightFind;//右子树找到了,返回 } return NULL; }
2.9判断完全二叉树
方法概述
- 队列初始化:使用队列来进行层序遍历。
- 遍历过程:
- 将根节点入队,开始遍历。
- 从队列中出队节点,检查其是否为空。
- 对于每个非空节点,将其左右孩子入队。
- 一旦遇到空节点,标记后续节点必须都是空节点。
- 最终验证:继续遍历队列,确保所有后续节点均为空。
代码如下:
//判断二叉树是否为完全二叉树--借助队列 bool BTComplete(BTNode* root) { QU q;//创建一个队列 QueueInit(&q); QueuePush(&q, root);//根节点入队,成为队头 while (!QueueEmpty(&q))//队列不为空 { //取队头出队 BTNode* Qhead = QueueFront(&q); QueuePop(&q); if (Qhead == NULL) { break; } //左右孩子都入队 QueuePush(&q, Qhead->left); QueuePush(&q, Qhead->right); } //空节点入对头是跳出循环,此时队列不一定为空 while (!QueueEmpty(&q))//队列不为空 { //取队头,出队 BTNode* Qhead = QueueFront(&q); QueuePop(&q); if (Qhead != NULL) { return false;//此时取到非空节点说明不是完全二叉树 } } QueueDestroy(&q); return true;//是完全二叉树 }
2.10二叉树销毁
代码实现:
//⼆叉树销毁 //后序遍历销毁:先销毁左子树,再右子树,最后根节点 void BTDestroy(BTNode** root) { if (*root == NULL) { return; } BTDestory(&((*root)->left)); BTDestory(&((*root)->right)); free(*root); *root = NULL; } //层序遍历 借助数据结构--队列 void LevelOrder(BTNode* root) { QU q;//创建一个队列 QueueInit(&q); QueuePush(&q, root);//根节点入队,成为队头 while (!QueueEmpty(&q))//队列不为空 { //取队头,打印 BTNode*Qhead=QueueFront(&q); printf("%d ", Qhead->data); QueuePop(&q);//队头出队 //队头节点左孩子入队 if (Qhead->left) { QueuePush(&q, Qhead->left); } //队头节点右孩子入队 if (Qhead->right) { QueuePush(&q, Qhead->right); } } QueueInit(&q); QueueDestroy(&q); }
三、完整代码
Queue.h
队列的声明
#pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h> //typedef int DataType; typedef struct BinaryTreeNode* QDataType; //定义节点结构体 typedef struct Node { QDataType data;//数据域 struct Node* next;//指针域 }Node; //定义队列结构体 typedef struct Queue { Node* phead;//队头 Node* ptail;//队尾 int size;//队列长度 }QU; //初始化队列 void QueueInit(QU* p); //入队列,队尾 void QueuePush(QU* p, QDataType x); //队列判空 bool QueueEmpty(QU* p); //出队列,队头 void QueuePop(QU* p); //取队头数据 QDataType QueueFront(QU* p); //取队尾数据 QDataType QueueBack(QU* p); //队列长度 int QueueSize(QU* p); //销毁队列 void QueueDestroy(QU* p);
Queue.c
队列的定义
#define _CRT_SECURE_NO_WARNINGS #include"Queue.h" // 初始化队列 void QueueInit(QU* p) { assert(p); p->phead = p->ptail = NULL; // 头尾指针初始化为 NULL p->size = 0; // 队列大小初始化为 0 } // 创建新节点 Node* CreateNode(QDataType x) { Node* newnode = (Node*)malloc(sizeof(Node)); if (newnode == NULL) { perror("malloc fail"); exit(1); // 分配失败,退出程序 } newnode->data = x; // 设置节点数据 newnode->next = NULL; // 初始化后继指针为 NULL return newnode; // 返回新节点 } // 入队操作(从队尾插入) void QueuePush(QU* p, QDataType x) { assert(p); Node* newnode = CreateNode(x); if (p->phead == NULL) // 队列为空时 { p->phead = p->ptail = newnode; // 头尾指针都指向新节点 } else // 队列不为空时 { p->ptail->next = newnode; // 尾节点的 next 指向新节点 p->ptail = newnode; // 更新尾指针为新节点 } ++p->size; // 队列大小加 1 } // 判断队列是否为空 bool QueueEmpty(QU* p) { assert(p); return p->phead == NULL; // 头指针为 NULL 表示队列为空 } // 出队操作(从队头删除) void QueuePop(QU* p) { assert(p); assert(!QueueEmpty(p)); // 确保队列不为空 if (p->phead == p->ptail) // 只有一个元素时 { free(p->phead); // 释放头节点 p->phead = p->ptail = NULL; // 更新头尾指针为 NULL } else { Node* del = p->phead; // 保存要删除的节点 p->phead = p->phead->next; // 更新头指针为下一个节点 free(del); // 释放删除的节点 } --p->size; // 队列大小减 1 } // 获取队头数据 QDataType QueueFront(QU* p) { assert(p); assert(!QueueEmpty(p)); // 确保队列不为空 return p->phead->data; // 返回头节点的数据 } // 获取队尾数据 QDataType QueueBack(QU* p) { assert(p); assert(!QueueEmpty(p)); // 确保队列不为空 return p->ptail->data; // 返回尾节点的数据 } // 获取队列长度 int QueueSize(QU* p) { assert(p); return p->size; // 返回队列的大小 } // 销毁队列 void QueueDestroy(QU* p) { assert(p); Node* pcur = p->phead; while (pcur) // 遍历所有节点并释放内存 { Node* next = pcur->next; free(pcur); pcur = next; } p->phead = p->ptail = NULL; // 头尾指针置为 NULL p->size = 0; // 队列大小置为 0 }
Tree.h
二叉树的声明
#pragma once #include<stdio.h> #include<assert.h> #include<stdlib.h> #include<stdbool.h> //定义二叉树链式结构 //二叉树节点结构 typedef int DataType; typedef struct BinaryTreeNode { int data;//值域 struct BinaryTreeNode* left;//指向当前节点左孩子 struct BinaryTreeNode* right;//指向当前节点右孩子 }BTNode; //前序遍历--根左右 void PreOrder(BTNode* root); //中序遍历--左根右 void InOrder(BTNode* root); //二叉树结点个数 int BTSize(BTNode* root); //二叉树叶子结点个数 int BTLeafSize(BTNode* root); //二叉树第k层结点个数 int BTLevelKSize(BTNode* root, int k); //二叉树的深度/高度 int BTDepth(BTNode* root); //二叉树查找值为x的结点 BTNode* BTFind(BTNode* root, DataType x); //二叉树销毁 void BTDestroy(BTNode** root); //层序遍历--借助队列 void LevelOrder(BTNode* root); //判断二叉树是否为完全二叉树--借助队列 bool BTComplete(BTNode* root);
Tree.c
二叉树的实现
#define _CRT_SECURE_NO_WARNINGS #include"Tree.h" #include"Queue.h" //前序遍历 //访问优先级:根节点 -> 左子树 -> 右子树 void PreOrder(BTNode* root) { if (root == NULL) { return; } printf("%d ", root->data); PreOrder(root->left); PreOrder(root->right); } //中序遍历 //访问优先级:左子树 -> 根节点 -> 右子树 void InOrder(BTNode* root) { if (root == NULL) { return; } InOrder(root->left); printf("%d ", root->data); InOrder(root->right); } //后序遍历 //访问优先级:左子树 -> 右子树 -> 根节点 void PostOrder(BTNode* root) { if (root == NULL) { return; } PostOrder(root->left); PostOrder(root->right); printf("%d ", root->data); } //⼆叉树结点个数 int BTSize(BTNode* root) { if (root == NULL) { return 0; } return BTSize(root->left) + BTSize(root->right) + 1; } //⼆叉树叶子结点个数 叶子节点:度为0的节点 int BTLeafSize(BTNode* root) { if (root == NULL) { return 0; } if (root->left == NULL && root->right == NULL) { return 1; } return BTLeafSize(root->left) + BTLeafSize(root->right); } //⼆叉树第k层结点个数 int BTLevelKSize(BTNode* root, int k) { if (root == NULL) { return 0; } if (k == 1) { return 1; } return BTLevelKSize(root->left, k - 1) + BTLevelKSize(root->right, k - 1); } //⼆叉树的深度/高度 int BTDepth(BTNode* root) { if (root == NULL) { return 0; } //返回左右子树最深的那个 return BTDepth(root->left) > BTDepth(root->right) ? BTDepth(root->left) + 1 : BTDepth(root->right) + 1; } //⼆叉树查找值为x的结点 BTNode* BTFind(BTNode* root, DataType x) { if (root == NULL) { return NULL; } if (root->data == x) { return root; } BTNode* leftFind=BTFind(root->left, x);//左子树中找 if (leftFind) { return leftFind;//左子树找到了,返回 } BTNode* rightFind=BTFind(root->right, x);//右子树中找 if (rightFind) { return rightFind;//右子树找到了,返回 } return NULL; } //⼆叉树销毁 //后序遍历销毁:先销毁左子树,再右子树,最后根节点 void BTDestroy(BTNode** root) { if (*root == NULL) { return; } BTDestroy(&((*root)->left)); BTDestroy(&((*root)->right)); free(*root); *root = NULL; } //层序遍历 借助数据结构--队列 void LevelOrder(BTNode* root) { QU q;//创建一个队列 QueueInit(&q); QueuePush(&q, root);//根节点入队,成为队头 while (!QueueEmpty(&q))//队列不为空 { //取队头,打印 BTNode*Qhead=QueueFront(&q); printf("%d ", Qhead->data); QueuePop(&q);//队头出队 //队头节点左孩子入队 if (Qhead->left) { QueuePush(&q, Qhead->left); } //队头节点右孩子入队 if (Qhead->right) { QueuePush(&q, Qhead->right); } } QueueInit(&q); QueueDestroy(&q); } //判断二叉树是否为完全二叉树--借助队列 bool BTComplete(BTNode* root) { QU q;//创建一个队列 QueueInit(&q); QueuePush(&q, root);//根节点入队,成为队头 while (!QueueEmpty(&q))//队列不为空 { //取队头出队 BTNode* Qhead = QueueFront(&q); QueuePop(&q); if (Qhead == NULL) { break; } //左右孩子都入队 QueuePush(&q, Qhead->left); QueuePush(&q, Qhead->right); } //空节点入对头是跳出循环,此时队列不一定为空 while (!QueueEmpty(&q))//队列不为空 { //取队头,出队 BTNode* Qhead = QueueFront(&q); QueuePop(&q); if (Qhead != NULL) { return false;//此时取到非空节点说明不是完全二叉树 } } QueueDestroy(&q); return true;//是完全二叉树 }
main.c
测试
#define _CRT_SECURE_NO_WARNINGS #include"Tree.h" BTNode* buyNode(DataType x) { BTNode* newnode = (BTNode*)malloc(sizeof(BTNode)); if (newnode == NULL) { perror("malloc fail!"); exit(1); } newnode->data = x; newnode->left=newnode->right = NULL; } BTNode* CreateTree() { BTNode* n1 = buyNode(1); BTNode* n2 = buyNode(2); BTNode* n3 = buyNode(3); BTNode* n4 = buyNode(4); BTNode* n5 = buyNode(5); BTNode* n6 = buyNode(6); n1->left = n2; n1->right = n4; n2->left = n3; n4->left = n5; n4->right = n6; return n1; } void Test() { BTNode* n1 = CreateTree(); printf("\n前序遍历:"); PreOrder(n1); printf("\n中序遍历:"); InOrder(n1); printf("\n后续遍历:"); PostOrder(n1); //打印节点数 printf("\nsize=%d\n", BTSize(n1)); //打印叶子节点个数 printf("left_size=%d\n", BTLeafSize(n1)); int k = 2; printf("第%d层节点个数:%d\n", k,BTLevelKSize(n1,k)); printf("Depth:%d\n", BTDepth(n1)); //查找数字4 BTNode* find = BTFind(n1, 4); printf("%s\n", find == NULL ? "未找到" : "找到了!"); printf("\n层序遍历:"); LevelOrder(n1); bool ret= BTComplete(n1); printf("\n%s\n",ret==false ?"不是完全二叉树" : "是完全二叉树!"); BTDestroy(&n1); } int main() { Test(); return 0; }
结果如下:
编辑
四、总结
在这篇文章中,我们深入探讨了二叉树的链式结构及其在C语言中的实现。我们首先了解了二叉树的基本结构及其动态大小的优势,然后介绍了多种遍历方式,包括前序、中序、后序和层序遍历,以及相关的节点统计函数。接着,文章详细阐述了如何判断二叉树是否为完全二叉树,并提供了二叉树的销毁函数。最后,通过实例测试了这些操作,展示了二叉树在数据处理中的重要性和灵活性,为读者提供了全面的理解和实用的代码实现。