hi~大家好呀,欢迎点进我的文章!❥(ゝω・✿ฺ)这篇讲的是一个如何用c语言简单实现数据结构中的一个基础二叉树的结构以及相关功能!我会把我的思路和过程展示出来,这本身也是一份学习笔记,我们一起进步吧~

---------------------------------------------------------------------------------------------------------------------------------
目录
在了解二叉树之前,我们首先来介绍一下树这个数据结构:

树是区别于线性表的另一种数据结构,是结点的有限集合。
如上图所示,这就是一个数的结构。每一个圆圈就可以代表一个结点。这个结点里面自然就可以储存数据。我们常见文件系统(磁盘下的文件)就是这个数据结构哦~
既然有这么一种数据结构,那么我们也会根据结点的性质来对其起名。
就上面的图而言,圆圈均可代表结点,其中黑色的就是叶子结点。
对树的基本认知:
1)节点的度:一个节点含有的子树个数 (2)叶节点或者终端节点:度为0的节点为叶节点 (3)非终端节点或者分支节点:度不为0的节点 (4)双亲节点或父节点:若一个节点带有子节点则就是其的父节点 (5)孩子节点或者子节点 一个节点含有的子节点就是(6)兄弟节点 具有相同父节点(7)树的度 一棵树中最大节点的度就是(8)节点的层次: 从根开始定义起根为第一层 其子节点为第二层,依次类推 (9)双亲在同一层的节点为堂兄弟节点 (10)节点的祖先 从根到该节点所经分支上的所有节点 (11)子孙 某节点为根的子树的任意节点为该节点的子孙 ()12 由大于零棵树互不相交的集合叫森林 多棵没有交叉的树(大后期 并查集)。空树的高度就是0节点没有
在对树有了一定认识之后,那么我们就可以来了解二叉树啦~
根据名字就可以知道,二~即计划生育后的树--
二叉树:一个结点的有限集合。每个结点最多有两个子树 。

如上图,这就是三个二叉树。
相对的,根据二叉树每层的结点顺序也就有了如下区分:
满二叉树:每一层都是满的(最后一层全是叶节点) 每层满足 2^(k-1)次方 (k代表层数,从1开始) 节点总数就是 2^(k) - 1
如上图黑树。
完全二叉树:前k-1层满的,最后一层是从左到右是连续的(即在k-1层满足度为1的最多只有一个)
由满二叉树可以推出 高度为h的完全二叉树的节点范围是 2^(h - 1) ~ 2^(h) - 1
如上图蓝树。
同样的,任何一个二叉树都满足如下规则:
设满足度为0的节点数为n1,满足度为2的节点数为n2,则有n2 = n1 + 1
(记忆方法:三个结点的满二叉树作为记忆)
那么,现在针对于完全二叉树,我们能否编程让这个数据结构实现储存数据并且简单建立起树的结构呢?答案是可以,建立这个结构同样的有两种方式:一个是数组,另一个就是链表。
数组虽然是线性储存的,但是我们只需要规定插入和取出时的算法即可。(比如父结点和子结点之间的关系:parent = (child - 1) / 2(不分左右结点),leftchild = parent*2+1,rightchild = parent * 2 + 2)但是会对空间造成一定的浪费,所以我们这里实现就用链表,每个结点用两个指针用来指向左孩子和右孩子。
上面我们明确了一个二叉树,现在我们需要通过链表来简单实现这个数据结构。
首先明确结构就是一个结点,一个结点里面存放着数据、指向左孩子的指针、指向右孩子的指针。

然后通过结点的集合(即创建多个此结构的对象),手动(手动指向结点的左右孩子)或者根据前序(遍历二叉树的顺序)创造一个完全二叉树。

黑色圆圈内就代表左右孩子指向NULL。
结构创建完毕后就要实现具体的一些简单功能:
1.遍历:
根据树的特性:前序遍历、中序遍历、后序遍历、层序遍历
前序遍历:
先访问此结点的数据,然后访问左节点,最后访问右节点。

如上图,数字表示访问结点数据,其中4 5 7 8....表示访问的NULL,然后返回。
中序遍历:
先访问左结点,然后访问此结点数据,最后访问右节点。

同理。
后序遍历:
先访问左结点,然后访问右结点,最后访问此结点数据。

同理。可以发现差异巨大,这些均可以通过递归进行实现,即先处理什么,在处理什么,最后处理什么的思路。
层序遍历:
从上到下,从左到右依次访问结点数据。

层序遍历遵循线性表的顺序,数组容易实现,对于链表结点时可以采用数据结构中的队列(先进先出的性质)作为辅助实现。
2.其他功能:
新建二叉树(根据前序顺序进行构造一个树)、摧毁二叉树(将树结点每一个从堆那里申请的给回收)、结点个数、叶子结点个数、第k层结点、寻找值为x的结点。
废话不多说,我们直接代码实现吧:
结点结构就是一个存放数据一个存放孩子的指针哦(有两个)另外,二叉树英文名为binary tree,所以就简写为BT啦:
- typedef char BTDataType;
- typedef struct BTNode
- {
- BTDataType x;
- struct BTNode* left;
- struct BTNode* right;
- }BTNode;
(数据类型为char,为了配合之后的利用前序创建一个二叉树(会用到符号识别,比如NULL用#代替))
首先就是自然的去堆中申请,检查是否申请到,然后将传过来的数据存入此结点,在将此结点的左右指针置为空,等待外界去连接。
- BTNode* BuyNode(BTDataType x)
- {
- BTNode* temp = (BTNode*)malloc(sizeof(BTNode));
- if (temp == NULL)
- {
- perror("BuyNode::malloc");
- exit(-1);
- }
- temp->x = x;
- temp->left = temp->right = NULL;
- return temp;
- }
根据前序遍历,首先访问此结点的数据,然后访问左结点,最后访问右结点。注意这里访问左结点不是访问左结点的数据,而是进入左结点重复上述步骤。然后进入右结点重复上述步骤。直到这两步进行完此函数栈帧就执行完毕。
对此,程序可如下写:
- void BTPrevOrder(BTNode* pn)
- {
- if (pn == NULL)
- {
- printf("# ");
- return;
- }
- printf("%c ", pn->x);
- BTPrevOrder(pn->left);
- BTPrevOrder(pn->right);
- }
可以发现,通过递归调用,进入左结点后,继续进行访问此结点数据,然后在进入左结点,直到遇到此为空,就输出#并且返回。返回的上一步就去右结点,同样也是遇到NULL就输出#并且返回。
具体如下二叉树进行表示:

中序遍历思路和前序遍历类似,只不过顺序变了,先访问左结点,在访问此结点的数据,最后访问右结点:
- void BTInOrder(BTNode* pn)
- {
- if (pn == NULL)
- {
- printf("# ");
- return;
- }
- BTInOrder(pn->left);
- printf("%c ", pn->x);
- BTInOrder(pn->right);
- }
后序先访问左结点,然后访问右结点,最后访问此结点数据:
- void BTPostOrder(BTNode* pn)
- {
- if (pn == NULL)
- {
- printf("# ");
- return;
- }
- BTPostOrder(pn->left);
- BTPostOrder(pn->right);
- printf("%c ", pn->x);
- }
层序遍历遵循的是从上到下,从左到右,普通的遍历的话一般只能把一边的先遍历完,比如想要推进的话通常就会将其的子结点赋给下一个要输出的结点,而不会找兄弟结点。即重点就是每次能够和兄弟结点一起成为循环。那么不妨就采用队列这个数据结构。
- ///
-
- //下面定义链表结构 -- 单链表 单链表的话需要单独定义两个指针一个指向头一个指向尾,方便进行插入和出列
- typedef BTNode* QueueDateType;
- typedef struct QListNode
- {
- QueueDateType x;
- struct QListNode* next;//存放下一指针的
- }QNode;
-
- typedef struct Queue
- {
- QNode* head;//存放队首
- QNode* tail;//存放队尾
- }Queue;
-
- //队列初始化,让队列的头和尾指向空
- void QueueInit(Queue* pq)
- {
- assert(pq);//防止空指针引用
- pq->head = pq->tail = NULL;
- }
-
- //队列摧毁 将堆内存申请的空间回收
- void QueueDestroy(Queue* pq)
- {
- assert(pq);//查看是否空指针引用
- while (pq->head)
- {
- QNode* temp = pq->head;
- pq->head = temp->next;
- free(temp);
- }
- pq->tail = NULL;
- }
-
- //入队 普通单链表进行尾插
- void QueuePush(Queue* pq, QueueDateType x)
- {
- assert(pq);
- QNode* newNode = (QNode*)malloc(sizeof(QNode));
- if (newNode == NULL)
- {
- perror("QueuePush");
- exit(-1);//不正常情况退出进程
- }
- newNode->x = x;
- newNode->next = NULL;
- if (pq->head == NULL)//第一次进
- {
- pq->head = pq->tail = newNode;
- }
- else//正常入队
- {
- pq->tail->next = newNode;
- pq->tail = newNode;//保存尾元素
- }
- }
-
- //判断是否存在元素
- bool QueueEmpty(Queue* pq)
- {
- assert(pq);
- return pq->head == NULL;//是空表明没有元素,返回真,否则为假
- }
- //出元素:
- QueueDateType QueueTop(Queue* pq)
- {
- assert(pq && !QueueEmpty(pq));
- return pq->head->x;
- }
- //出队 先进先出出头结点,然后将头结点更换成下一个结点
- void QueuePop(Queue* pq)
- {
- assert(pq && !QueueEmpty(pq));//判断是否空指针引用或者是否空队列
- if (pq->head == pq->tail)//只有一个元素
- {
- free(pq->head);
- pq->head = pq->tail = NULL;
- }
- else
- {
- QNode* temp = pq->head->next;
- free(pq->head);
- pq->head = temp;
- }
- }
- /
-
-
-
- //层序遍历 利用队列的性质 首先进根结点,打印出此值后就出队列,如果孩子不为空就进队列,此时顺序就是左 右,之后就打印左孩子的值,然后进左右,重复上述
- void BTLevelOrder(BTNode* pn)
- {
- assert(pn);
- Queue e;
- QueueInit(&e);
- QueuePush(&e, pn);
- while (!QueueEmpty(&e))//队列没有空就打印哦
- {
- BTNode* temp = QueueTop(&e);
- printf("%c ", temp->x);
- QueuePop(&e);
- if (temp->left)
- QueuePush(&e, temp->left);
- if (temp->right)
- QueuePush(&e, temp->right);
- }
- printf("\n");
- QueueDestroy(&e);
- }
如上,先是实现了队列这个数据结构 ,将储存的数据定为结点类型。根据特性,先进先出,我们现在需要保证出是按照从上到下从左到右的结点顺序,那么入队列的顺序就自然就是从上到下,从左到右。那么,每一次入的时候两个孩子只要不为空就入,下一次第一个就是左孩子,入左孩子的左右孩子,第三次就是右孩子,入右孩子的左右孩子....依次进行,就可以发现入的时候就是从上到下从左到右了。
如下图,参照左边二叉树,右边动画演示插入:


既然是摧毁二叉树,那么就需要将每个结点清理调,就需要进行遍历。我这里采用后序遍历的方式,将每个结点都free调,并且需要将指针置为空。因为需要修改指针,所以需要传二级指针。
- void BTDestroy(BTNode** ppn)
- {
- assert(ppn);
- //利用后序遍历进行全盘清理
- if (*ppn == NULL)
- return;
- BTDestroy(&((*ppn)->left));//特别注意传的是指针的地址 -- 二级指针
- BTDestroy(&((*ppn)->right));
- free(*ppn);
- *ppn = NULL;
- }
这里根据前序新建二叉树,那么只需要和前序遍历的逻辑跟着走,利用函数BuyNode进行先创建结点,然后将左结点和右结点的创建进入递归即可,遇到‘#’就返回NULL即可。需要注意的是传入的数组,外面必须传一个能累加的i值,这样递归时才能统一,当然全局变量也可,用于推进数组:
- BTNode* BTCreate(char* arr, int* pi)
- {
- assert(arr);
- if (arr[*(pi)] == '#')
- {
- (*pi)++;
- return NULL;
- }
-
- BTNode* node = BuyNode(arr[(*pi)++]);
- node->left = BTCreate(arr, pi);
- node->right = BTCreate(arr, pi);
- return node;
- }
既然是统计结点个数,同样的利用递归,判断当前结点是否为NULL,如果为NULL就返回0,否则就返回其左结点递归后的个数加上右结点递归后的个数再加1。
- int BTSize(BTNode* pn)
- {
- if (pn == NULL)
- {
- return 0;
- }
- return BTSize(pn->left) + BTSize(pn->right) + 1;
- }
叶子结点无疑需要有条件,即其左右孩子结点必须为空,就返回1,如果为空就返回0,均不满足就递归下去,查找左结点的叶子结点个数和右结点的叶子结点个数:
- int BTLeafSize(BTNode* pn)
- {
- if (pn == NULL)
- {
- return 0;
- }
- if (pn->left == NULL && pn->right == NULL)
- return 1;
- return BTLeafSize(pn->left) + BTLeafSize(pn->right);
- }
让我们寻找第k层的结点个数。我们知道我们递归到孩子结点的时候,均为下一层,那么每次递归时减去1即可,当k==1的时候,返回1,当然,如果为空就返回0,如果不等于,就返回左孩子和右孩子相加的结果:
- int BTLevelKSize(BTNode* pn, int k)
- {
- if (pn == NULL)
- return 0;
- if (k == 1)
- return 1;
- return BTLevelKSize(pn->left, k - 1) + BTLevelKSize(pn->right, k - 1);
- }
利用遍历去寻找即可,前序遍历:先查看当前结点是否为该x值,是就返回此结点,否则就让左右孩子去找,接收他们的返回值,判断是否为空(当然,结点为空就返回NULL),如果有一个不为空就返回对应的,均为空就表明没有找到了,返回空即可。
- BTNode* BTFind(BTNode* pn, BTDataType x)
- {
- if (pn == NULL)
- return NULL;
- if (pn->x == x)
- return pn;
- BTNode* left = BTFind(pn->left, x);
- if (left)
- return left;
- BTNode* right = BTFind(pn->right, x);
- if (right)
- return right;
- return NULL;
- }
判断是否满二叉树的条件:该结点的左右结点是否均为空,如果有一个不为空就返回false,否则返回true,如果均不为空,就返回左结点和右结点是否均为true,均为true就表明是满二叉树,就返回true,否则就返回false。(当然,最后可以直接返回两者且的结果)
- bool BTComplete(BTNode* pn)
- {
- if (pn == NULL)
- return false;
- if (pn->left == NULL && pn->right == NULL)
- return true;
- //if (BTComplete(pn->left) && BTComplete(pn->right))
- // return true;
- //return false;
- return BTComplete(pn->left) && BTComplete(pn->right);
- }
综上,代码实现如下!
- #pragma once
- #define _CRT_SECURE_NO_WARNINGS 1
-
- #include
- #include
- #include
- #include
- #include
-
- /*<链表二叉树> 链表: 数据、左结点、右结点
- * 功能:新建结点 -- 手动连接
- * 遍历 -- 前序、中序、后序、层序
- * 新建二叉树 -- 根据数组 由前序遍历进行构建
- * 摧毁二叉树、结点个数、叶结点个数、第k层结点、寻找值为x的结点
- */
-
- typedef char BTDataType;
- typedef struct BTNode
- {
- BTDataType x;
- struct BTNode* left;
- struct BTNode* right;
- }BTNode;
-
- //建立二叉树结点
- BTNode* BuyNode(BTDataType x);
- //二叉树摧毁 -- 要修改原地址,传二级指针
- void BTDestroy(BTNode** ppn);
- //前序遍历
- void BTPrevOrder(BTNode* pn);
- //中序遍历
- void BTInOrder(BTNode* pn);
- //后序遍历
- void BTPostOrder(BTNode* pn);
- //层序遍历
- void BTLevelOrder(BTNode* pn);
- //新建二叉树 根据前序遍历进行建立: -- 字符类型
- BTNode* BTCreate(char* arr, int* pi);
- //结点个数
- int BTSize(BTNode* pn);
- //叶子结点个数
- int BTLeafSize(BTNode* pn);
- //第k层结点
- int BTLevelKSize(BTNode* pn, int k);
- //寻找值为x的结点:
- BTNode* BTFind(BTNode* pn, BTDataType x);
- //判断是否为满二叉树
- bool BTComplete(BTNode* pn);
- #include"BTNode.h"
-
- BTNode* BuyNode(BTDataType x)
- {
- BTNode* temp = (BTNode*)malloc(sizeof(BTNode));
- if (temp == NULL)
- {
- perror("BuyNode::malloc");
- exit(-1);
- }
- temp->x = x;
- temp->left = temp->right = NULL;
- return temp;
- }
-
- void BTDestroy(BTNode** ppn)
- {
- assert(ppn);
- //利用后序遍历进行全盘清理
- if (*ppn == NULL)
- return;
- BTDestroy(&((*ppn)->left));//特别注意传的是指针的地址 -- 二级指针
- BTDestroy(&((*ppn)->right));
- free(*ppn);
- *ppn = NULL;
- }
-
- //前序遍历
- void BTPrevOrder(BTNode* pn)
- {
- if (pn == NULL)
- {
- printf("# ");
- return;
- }
- printf("%c ", pn->x);
- BTPrevOrder(pn->left);
- BTPrevOrder(pn->right);
- }
-
- //中序遍历
- void BTInOrder(BTNode* pn)
- {
- if (pn == NULL)
- {
- printf("# ");
- return;
- }
- BTInOrder(pn->left);
- printf("%c ", pn->x);
- BTInOrder(pn->right);
- }
-
- //后序遍历
- void BTPostOrder(BTNode* pn)
- {
- if (pn == NULL)
- {
- printf("# ");
- return;
- }
- BTPostOrder(pn->left);
- BTPostOrder(pn->right);
- printf("%c ", pn->x);
- }
-
-
-
-
- ///
-
- //下面定义链表结构 -- 单链表 单链表的话需要单独定义两个指针一个指向头一个指向尾,方便进行插入和出列
- typedef BTNode* QueueDateType;
- typedef struct QListNode
- {
- QueueDateType x;
- struct QListNode* next;//存放下一指针的
- }QNode;
-
- typedef struct Queue
- {
- QNode* head;//存放队首
- QNode* tail;//存放队尾
- }Queue;
-
- //队列初始化,让队列的头和尾指向空
- void QueueInit(Queue* pq)
- {
- assert(pq);//防止空指针引用
- pq->head = pq->tail = NULL;
- }
-
- //队列摧毁 将堆内存申请的空间回收
- void QueueDestroy(Queue* pq)
- {
- assert(pq);//查看是否空指针引用
- while (pq->head)
- {
- QNode* temp = pq->head;
- pq->head = temp->next;
- free(temp);
- }
- pq->tail = NULL;
- }
-
- //入队 普通单链表进行尾插
- void QueuePush(Queue* pq, QueueDateType x)
- {
- assert(pq);
- QNode* newNode = (QNode*)malloc(sizeof(QNode));
- if (newNode == NULL)
- {
- perror("QueuePush");
- exit(-1);//不正常情况退出进程
- }
- newNode->x = x;
- newNode->next = NULL;
- if (pq->head == NULL)//第一次进
- {
- pq->head = pq->tail = newNode;
- }
- else//正常入队
- {
- pq->tail->next = newNode;
- pq->tail = newNode;//保存尾元素
- }
- }
-
- //判断是否存在元素
- bool QueueEmpty(Queue* pq)
- {
- assert(pq);
- return pq->head == NULL;//是空表明没有元素,返回真,否则为假
- }
- //出元素:
- QueueDateType QueueTop(Queue* pq)
- {
- assert(pq && !QueueEmpty(pq));
- return pq->head->x;
- }
- //出队 先进先出出头结点,然后将头结点更换成下一个结点
- void QueuePop(Queue* pq)
- {
- assert(pq && !QueueEmpty(pq));//判断是否空指针引用或者是否空队列
- if (pq->head == pq->tail)//只有一个元素
- {
- free(pq->head);
- pq->head = pq->tail = NULL;
- }
- else
- {
- QNode* temp = pq->head->next;
- free(pq->head);
- pq->head = temp;
- }
- }
- /
-
-
-
- //层序遍历 利用队列的性质 首先进根结点,打印出此值后就出队列,如果孩子不为空就进队列,此时顺序就是左 右,之后就打印左孩子的值,然后进左右,重复上述
- void BTLevelOrder(BTNode* pn)
- {
- assert(pn);
- Queue e;
- QueueInit(&e);
- QueuePush(&e, pn);
- while (!QueueEmpty(&e))//队列没有空就打印哦
- {
- BTNode* temp = QueueTop(&e);
- printf("%c ", temp->x);
- QueuePop(&e);
- if (temp->left)
- QueuePush(&e, temp->left);
- if (temp->right)
- QueuePush(&e, temp->right);
- }
- printf("\n");
- QueueDestroy(&e);
- }
-
-
- //根据字符数组 #不开 其余开结点 根据前序遍历进行创建
- BTNode* BTCreate(char* arr, int* pi)
- {
- assert(arr);
- if (arr[*(pi)] == '#')
- {
- (*pi)++;
- return NULL;
- }
-
- BTNode* node = BuyNode(arr[(*pi)++]);
- node->left = BTCreate(arr, pi);
- node->right = BTCreate(arr, pi);
- return node;
- }
-
- //计算结点个数
- int BTSize(BTNode* pn)
- {
- if (pn == NULL)
- {
- return 0;
- }
- return BTSize(pn->left) + BTSize(pn->right) + 1;
- }
-
- //计算叶子结点个数
- int BTLeafSize(BTNode* pn)
- {
- if (pn == NULL)
- {
- return 0;
- }
- if (pn->left == NULL && pn->right == NULL)
- return 1;
- return BTLeafSize(pn->left) + BTLeafSize(pn->right);
- }
-
- //第K层结点个数
- int BTLevelKSize(BTNode* pn, int k)
- {
- if (pn == NULL)
- return 0;
- if (k == 1)
- return 1;
- return BTLevelKSize(pn->left, k - 1) + BTLevelKSize(pn->right, k - 1);
- }
-
- //寻找值为x的结点
- BTNode* BTFind(BTNode* pn, BTDataType x)
- {
- if (pn == NULL)
- return NULL;
- if (pn->x == x)
- return pn;
- BTNode* left = BTFind(pn->left, x);
- if (left)
- return left;
- BTNode* right = BTFind(pn->right, x);
- if (right)
- return right;
- return NULL;
- }
-
-
- //判断是否为完全二叉树
- bool BTComplete(BTNode* pn)
- {
- if (pn == NULL)
- return false;
- if (pn->left == NULL && pn->right == NULL)
- return true;
- //if (BTComplete(pn->left) && BTComplete(pn->right))
- // return true;
- //return false;
- return BTComplete(pn->left) && BTComplete(pn->right);
- }
测试代码可以自己写哦~
- #include"BTNode.h"
-
- void test()
- {
- //测试前序、中序、后序、层序遍历
- //BTNode* node1 = BuyNode('A');
- //BTNode* node2 = BuyNode('B');
- //BTNode* node3 = BuyNode('C');
- //BTNode* node4 = BuyNode('D');
- //node1->left = node2;
- //node1->right = node3;
- //node2->left = node4;
- //BTPrevOrder(node1);//A B D # # # C # #
- //printf("\n");
- //BTInOrder(node1);//# D # B # A # C #
- //printf("\n");
- //BTPostOrder(node1);// # # D # B # # C A
- //printf("\n");
- //BTLevelOrder(node1);//A B C D
- //BTDestroy(&node1);
-
- //测试根据前序进行创建链表二叉树
- char arr[] = "AB##c##";
- int i = 0;
- BTNode* node = BTCreate(arr, &i);
- BTPrevOrder(node);
- printf("\n");
- //测试节点数、叶子结点、第k层结点、查找结点
- printf("结点数:%d\n", BTSize(node));
- printf("叶子结点数:%d\n", BTLeafSize(node));
- printf("第1层结点数:%d\n", BTLevelKSize(node, 1));
- printf("第2层结点数:%d\n", BTLevelKSize(node, 2));
- //printf("第3层结点数:%d\n", BTLevelKSize(node, 3));
- printf("寻找A:%c\n", BTFind(node, 'A')->x);
- if (BTComplete(node))
- printf("满二叉树\n");
- else
- printf("完全二叉树\n");
- BTDestroy(&node);
- }
-
- int main()
- {
- test();
- return 0;
- }
好啦~以上就是C语言实现简单的链式二叉树啦,欢迎补充哦~( • ̀ω•́ )✧(〃'▽'〃)