目
目录
前言:在前一篇文章 树(Tree) 中我们提到了什么是二叉树的链式结构,但是并没有进行实现,这篇文章就是为了实现二叉树
一、二叉树建立:
目标:
1、通过前序遍历将一个数组创建成二叉树
2、前、中、后、层序遍历
3、销毁
4、计算结点个数
5、计算第k层的节点个数
6、计算高度/深度
7、求叶子结点个数
8、判断二叉树是否是完全二叉树
9、查找值为X的结点
如下:二叉树的结构(底层逻辑就是链表)
二、分步解决所有问题:
1、创建二叉树:
我们提供一个数组,对这个数据通过前序遍历来实现;将如下这个数组变成一个二叉树
// 通过前序遍历的数组"1,2,3,0,0,4,0,5,0,0,6,7,0,0,8,0,0"构建二叉树
TrNode* TrCreate(TrDataType* a, int n, int* pi)
{
//判断是否为NULL
if (a[(*pi)] == 0)
{
//下标要加一
(*pi)++;
return NULL;
}
if (*pi == n)
{
return NULL;
}
//创建节点
TrNode* node = (TrNode*)malloc(sizeof(TrNode));
if (node == NULL)
{
perror("malloc");
return NULL;
}
node->data = a[(*pi)++];
node->left = TrCreate(a,n,pi);
node->right = TrCreate(a, n, pi);
return node;
}
结束条件就是数组元素为0时或者下表和pi相等时,这里用到了指针来表示下标,为了保证下标会发生改变(在函数改变形参不影响实参,要指针才能通过改变实参影响形参)
2、前、中、后序遍历
放到一起就是因为他们的遍历顺序,其实就是打印位置的区别,不就根的位置决定了遍历顺序么
//前序遍历:根 左 右
void TrInOrder1(TrNode* root)
{
//递归结束条件:若是为空,结束
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
TrInOrder1(root->left);
TrInOrder1(root->right);
}
//中序遍历:左 根 右
void TrInOrder2(TrNode* root)
{
//递归结束条件:若是为空,结束
if (root == NULL)
{
printf("N ");
return;
}
TrInOrder2(root->left);
printf("%d ", root->data);
TrInOrder2(root->right);
}
//后序遍历:左 右 根
void TrInOrder3(TrNode* root)
{
//递归结束条件:若是为空,结束
if (root == NULL)
{
printf("N ");
return;
}
TrInOrder3(root->left);
TrInOrder3(root->right);
printf("%d ", root->data);
}
这个就是递归图,:展开图,尝试画一下
3、销毁:
也是递归实现,问题可以化为,先左右子树,在根结点进行销毁,转换为子问题:一般我直接就是去尾部来确认递归结束条件和里面要进行的操作,可以看见当3的左右子树不为NULL就销毁,若是为NULL就不要管,
进行销毁的前提是根不为NULL,当根结点为NULL时结束递归,开始回推
//销毁:先销毁左孩子和右孩子
void TrDestroy(TrNode** root)
{
if (*root != NULL)
{
if ((*root)->left)
{
TrDestroy(&(*root)->left);
}
if ((*root)->right)
{
TrDestroy(&(*root)->right);
}
//最后销毁根结点
free(*root);
*root = NULL;
}
}
//销毁:先销毁左孩子和右孩子
//void TrDestroy(TrNode** root)
//{
// if (*root == NULL)
// return;
// //左孩子
// if ((*root)->left)
// TrDestroy(&(*root)->left);
// //右孩子
// if ((*root)->right)
// TrDestroy(&(*root)->right);
// //销毁
// free(*root);
// *root = NULL;
//}
4、计算结点个数:
问题转化:根+左子树的结点个数+右子树的结点个数 == 总结点个数
根据树的根可以细化很多个,子问题:计算左、右子树的结点+1
如下代码即可,你可以想一下当递归到最3的左子树是不是就是NULL,返回0个,右子树也是0个
然后加上1就是3的节点数,回推就计算好了结点个数,看上图
//求结点个数:大事化小->将树看成 子树和根 子树节点数+1
int TrSize(TrNode* root)
{
//结束条件:NULL时
if (root == NULL)
return 0;
return TrSize(root->left) + TrSize(root->right) + 1;
}
看不懂可以看下图,画了一部分递归展开图如下
5、计算第k层的节点个数:
逆着来,不就是对每个孩子找自己的爹一次,当k==1时,返回1,这个是我们能够利用的条件
int TrKSize(TrNode* root, int k)
{
//空树:下限(结束条件)
if (root == NULL)
{
return 0;
}
//当只有根
if (k == 1)
{
return 1;
}
return TrKSize(root->left, k - 1) + TrKSize(root->right, k - 1);
}
6、计算高度/深度:
明确问题:高度怎么算???可以将高度分为2部分,左子树的高度 和 右子树的高度高的子树 +根
子问题:左右谁高返回谁,然后加 根
//求高度:左右子树高的那棵树+1;
int TrHigt(TrNode* root)
{
if (root == NULL)
{
return 0;
}
int Hleft = TrHigt(root->left);
int Hright = TrHigt(root->right);
return Hleft > Hright ? Hleft + 1 : Hright + 1;
}
错误示范:这个程序能运行但是会不断重复,就是不记事;三目操作(条件操作符),先判断,判断完以后是不是要调用后面的函数,调进去右会走一遍三目操作符,所以说他没有记住,要重复好几次才能带值回来,就像老师找班长查本班在校人数,记录了值去和隔壁班比较,比完以后,又忘了我们班学生多少人,又回去让班长调查,班长又去查,查了又去比较,又会忘记;反反复复才记起
7、求叶子结点个数:
求叶子左右为空树就是叶子结点,即没有孩子的结点;
//求叶子结点个数:
int TrLeafSize(TrNode* root)
{
if (root == NULL)
return 0;
else if (root->left == NULL && root->right == NULL)
return 1;
else
return TrLeafSize(root->left) + TrLeafSize(root->right);
}
8、 查找值为x的结点:
可以通过前序遍历去遍历一遍当找到和x相同的值时,返回该结点即可;当然了,当我们遍历找到了以后就不需要继续去找右子树了,所以用到了一个if语句,当左子树为NULL,就去找右子树找结点;
// 二叉树查找值为x的节点
TrNode* TrFind(TrNode* root, TrDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
TrNode* ret = TrFind(root->left, x);
if (ret == NULL)
{
return TrFind(root->right, x);
}
else
{
return ret;
}
}
9、层序遍历:
就是将每一层打印出来,也就是打印每一层的结点:
这里我们不用递归的思想,需要以每行进行打印,有堂兄也有亲兄弟,这个关系和之前我们使用递归不一样,之前左右子树直接就可以联系到一起;要用队列的思想来实现:先进先出的好处,先进来的结点 1 入队,然后先判断其左、再判断其右孩子是否存在,若是左孩子存在则让其做孩子入队,右孩子存在则让右孩子入队列,然后将1Pop掉(出队),大致流程如下:
这里还要涉及到队的建立、插入、删除和销毁,最后会给出整个代码,先看一下层序遍历怎么实现的;队列里面的数据类型就变成了二叉树的节点类型,要存的是TrNode*;我们根节点就是*root的形式传进来的,我们会引用Queue的头文件来实现队列创建,注意数据元素要修改
//层序遍历:目的是打印每一层 递归实现不了,队列:先进先出的原则 void TrLevelOrder(TrNode* root) { if (root == NULL) return; Queue q; //初始化 QuInto(&q); QuPush(&q, root); while (!QuEmpty(&q)) { //队头的元素 TrNode* nwnode = QuFront(&q); printf("%d ", nwnode->data); if (nwnode->left != NULL) { QuPush(&q, nwnode->left); } if (nwnode->right != NULL) { QuPush(&q, nwnode->right); } //删除头结点! QuPop(&q); } QuDestroy(&q); }
10、判断二叉树是否为完全二叉树:
当层序遍历以后发现可以继续用队列的思想来做,首先是明白什么是完全二叉树,前k-1是节点是满的,第k可以是不满的,但必须从左往右连续
前面的思路是不为空入队这次不管是不是空,将结点的左、右孩子都放队里头,发现了么,当左子树的叶子结点到NULL时,如果第k-1层的右孩子或者左孩子为NULL,就会出现一个N在里面,那么只要判断N后面是否全是NULL还是存在结点,如果存在结点:说明不是完全二叉树,因为子叶节点不连续(通过入队来间接实现的顺序结构,之前的就讲过完全二叉树用顺序存储时是不会浪费空间,这里就发生了浪费空间的现象)思路和层序大差不差,这里我省略了步骤,直接画的结果图
// 判断二叉树是否是完全二叉树:
int TrComplete(TrNode* root)
{
Queue q;
QuInto(&q);
QuPush(&q, root);
TrNode* nwnode = q.head->a;
while (!QuEmpty(&q))
{
nwnode = QuFront(&q);
if (nwnode == NULL)
break;
QuPush(&q,nwnode->left);
QuPush(&q,nwnode->right);
QuPop(&q);
}
while (!QuEmpty(&q))
{
nwnode = QuFront(&q);
QuPop(&q);
if (nwnode != NULL)
{
QuDestroy(&q);
return 0;
}
}
QuDestroy(&q);
return 1;
}
三、代码分装实现:
1、Tree.h头文件(声明)
#pragma once
#include<assert.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<stdbool.h>
typedef int TrDataType;
//二叉树的结点
typedef struct TreeNode {
//数据
TrDataType data;
//左结点
struct TreeNode* left;
//右结点
struct TreeNode* right;
}TrNode;
typedef TrNode* QuDataType;
//队列的结点
typedef struct QueueNode {
QuDataType a;
struct QueueNode* next;
}QuNode;
//队列
typedef struct Queue {
//记录头
QuNode* head;
//记录尾
QuNode* tail;
int size;
}Queue;
//初始化头尾节点
void QuInto(Queue* q);
//尾插
void QuPush(Queue* q, QuDataType x);
//头删
void QuPop(Queue* q);
//判空
bool QuEmpty(Queue* q);
//销毁队列
void QuDestroy(Queue* q);
//前序遍历:
void TrInOrder1(TrNode* root);
//中序遍历:
void TrInOrder2(TrNode* root);
//后序遍历:
void TrInOrder3(TrNode* root);
//求结点个数:
int TrSize(TrNode* root);
//求叶子结点个数:
int TrLeafSize(TrNode* root);
//求高度:
int TrHigt(TrNode* root);
//求第k层结点个数:
int TrKSize(TrNode* root, int k);
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
TrNode* TrCreate(TrDataType* a, int n, int* pi);
// 层序遍历:
void TrLevelOrder(TrNode* root);
// 判断二叉树是否是完全二叉树:
int TrComplete(TrNode* root);
//销毁
void TrDestroy(TrNode** root);
// 二叉树查找值为x的节点
TrNode* TrFind(TrNode* root, TrDataType x);
2、Tree.c源文件(实现)
#define _CRT_SECURE_NO_WARNINGS 3
#include"Tree.h"
//前序遍历:根 左 右
void TrInOrder1(TrNode* root)
{
//递归结束条件:若是为空,结束
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
TrInOrder1(root->left);
TrInOrder1(root->right);
}
//中序遍历:左 根 右
void TrInOrder2(TrNode* root)
{
//递归结束条件:若是为空,结束
if (root == NULL)
{
printf("N ");
return;
}
TrInOrder2(root->left);
printf("%d ", root->data);
TrInOrder2(root->right);
}
//后序遍历:左 右 根
void TrInOrder3(TrNode* root)
{
//递归结束条件:若是为空,结束
if (root == NULL)
{
printf("N ");
return;
}
TrInOrder3(root->left);
TrInOrder3(root->right);
printf("%d ", root->data);
}
//求结点个数:大事化小->将树看成 子树和根 子树节点数+1
int TrSize(TrNode* root)
{
//结束条件:NULL时
if (root == NULL)
return 0;
return TrSize(root->left) + TrSize(root->right) + 1;
}
//用于求高度式,判断左右子树
int MAX(int x, int y)
{
return x > y ? x : y;
}
//求叶子结点个数:
int TrLeafSize(TrNode* root)
{
if (root == NULL)
return 0;
else if (root->left == NULL && root->right == NULL)
return 1;
else
return TrLeafSize(root->left) + TrLeafSize(root->right);
}
//求高度:左右子树高的那棵树+1;
//求高度:左右子树高的那棵树+1;
int TrHigt(TrNode* root)
{
if (root == NULL)
{
return 0;
}
int Hleft = TrHigt(root->left);
int Hright = TrHigt(root->right);
return Hleft > Hright ? Hleft + 1 : Hright + 1;
}
//求第k层结点个数:问题分为。第k层的节点数就是k-1层的父亲有几个孩子的问题;
// 第一层的根节点,石头蹦出来的k==1
// 左孩子的爹+右孩子的爹就等于这层的个数也就是左子树的k-1层加右子树的k-1层
int TrKSize(TrNode* root, int k)
{
//空树:下限(结束条件)
if (root == NULL)
{
return 0;
}
//当只有根
if (k == 1)
{
return 1;
}
return TrKSize(root->left, k - 1) + TrKSize(root->right, k - 1);
}
// 通过前序遍历的数组"1,2,3,0,0,4,0,5,0,0,6,7,0,0,8,0,0"构建二叉树
TrNode* TrCreate(TrDataType* a, int n, int* pi)
{
//判断是否为NULL
if (a[(*pi)] == 0)
{
//下标要加一
(*pi)++;
return NULL;
}
if (*pi == n)
{
return NULL;
}
//创建节点
TrNode* node = (TrNode*)malloc(sizeof(TrNode));
if (node == NULL)
{
perror("malloc");
return NULL;
}
node->data = a[(*pi)++];
node->left = TrCreate(a,n,pi);
node->right = TrCreate(a, n, pi);
return node;
}
//销毁:先销毁左孩子和右孩子
void TrDestroy(TrNode** root)
{
if (*root == NULL)
return;
//左孩子
if ((*root)->left)
TrDestroy(&(*root)->left);
//右孩子
if ((*root)->right)
TrDestroy(&(*root)->right);
//销毁
free(*root);
*root = NULL;
}
// 二叉树查找值为x的节点
TrNode* TrFind(TrNode* root, TrDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
TrNode* ret = TrFind(root->left, x);
if (ret == NULL)
{
ret = TrFind(root->right, x);
}
else
{
return ret;
}
}
// 判断二叉树是否是完全二叉树:
int TrComplete(TrNode* root)
{
Queue q;
QuInto(&q);
QuPush(&q, root);
TrNode* nwnode = q.head->a;
while (!QuEmpty(&q))
{
nwnode = q.head->a;
if (nwnode == NULL)
break;
QuPush(&q,nwnode->left);
QuPush(&q,nwnode->right);
QuPop(&q);
}
while (!QuEmpty(&q))
{
nwnode = q.head->a;
QuPop(&q);
if (nwnode != NULL)
{
QuDestroy(&q);
return 0;
}
}
QuDestroy(&q);
return 1;
}
//层序遍历:目的是打印每一层 递归实现不了,队列:先进先出的原则
void TrLevelOrder(TrNode* root)
{
if (root == NULL)
return;
Queue q;
//初始化
QuInto(&q);
QuPush(&q, root);
while (!QuEmpty(&q))
{
//队头的元素
TrNode* nwnode = q.head->a;
printf("%d ", nwnode->data);
if (nwnode->left != NULL)
{
QuPush(&q, nwnode->left);
}
if (nwnode->right != NULL)
{
QuPush(&q, nwnode->right);
}
//删除头结点!
QuPop(&q);
}
QuDestroy(&q);
}
//队列
//初始化头尾节点
void QuInto(Queue* q)
{
q->head = q->tail = NULL;
q->size = 0;
}
//尾插
void QuPush(Queue* q, QuDataType x)
{
assert(q);
//申请看空间:
QuNode* node = (QuNode*)malloc(sizeof(QuNode));
if (node == NULL)
{
perror("malloc");
return;
}
node->a = x;
node->next = NULL;
//当头尾都在初始处
if (q->tail == NULL)
{
q->head = q->tail = node;
}
else
{
q->tail->next = node;
q->tail = node;
}
q->size++;
}
//头删
void QuPop(Queue* q)
{
assert(q);
assert(q->size > 0);
QuNode* ptal = q->head->next;
if (q->head == q->tail)
{
free(q->head);
q->head = q->tail = NULL;
}
else
{
free(q->head);
q->head = ptal;
}
q->size--;
}
//判空
bool QuEmpty(Queue* q)
{
assert(q);
return q->size == 0;
}
//销毁
//销毁空间(写进数据,想要一次性释放完就来用)
void QuDestroy(Queue* q)
{
assert(q);
QuNode* cur = q->head;
while (cur)
{
QuNode* next = cur->next;
free(cur);
cur = next;
}
q->head = q->tail = NULL;
q->size = 0;
}
3、test.c源文件(测试)
#define _CRT_SECURE_NO_WARNINGS 3
#define _CRT_SECURE_NO_WARNINGS 3
#include"Tree.h"
//手搓建立二叉树
TrNode* TrInt(TrDataType x)
{
TrNode* pts = (TrNode*)malloc(sizeof(TrNode));
if (pts == NULL)
{
perror("malloc");
return;
}
pts->data = x;
pts->left = NULL;
pts->right = NULL;
return pts;
}
//二叉树:
void Game()
{
//将数组建立成二叉树
int i = 0;
//1,2,3,0,0,4,0,5,0,0,6,7,0,0,8,0,0
int a[] = { 1,2,3,0,0,0,4,5,0,0,6,0,0 };
int n = sizeof(a) / sizeof(a[0]);
TrNode* root =TrCreate(a, n, &i);
//前序遍历;
TrInOrder1(root);
putchar('\n');
//中序遍历:
TrInOrder2(root);
putchar('\n');
//后序遍历;
TrInOrder3(root);
putchar('\n');
//结点个数:
int ret = TrSize(root);
printf("结点个数:%d\n", ret);
ret = TrLeafSize(root);
printf("叶子节点个数:%d\n", ret);
ret = TrHigt(root);
printf("高度:%d\n", ret);
ret = TrKSize(root, 3);
printf("第k层结点个数:%d\n", ret);
TrNode* node10 = TrFind(root, 7);
if (node10)
{
printf("%d \n", node10->data);
}
TrLevelOrder(root);
putchar('\n');
ret = TrComplete(root);
if (ret == 0)
{
printf("不是完全二叉树\n");
}
//销毁:
TrDestroy(&root);
}
int main()
{
Game();
return 0;
}
4、队列的头文件:Queue.h
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdbool.h>
#include<stdlib.h>
//前置声明
typedef struct TreeNode TrNode ;
typedef TrNode* QUDataType;
//节点
typedef struct QueueNode{
QUDataType a;
//一定要用指针,不然结构体的大小就无法确定了
struct QueueNode *next;
}QuNode;
//再建立一个保存头和尾的结构体
typedef struct Queue {
QuNode* head;
QuNode* tail;
int size;
}Queue;
//初始化头尾节点
void QuInto(Queue* q);
//尾插
void QuPush(Queue* q,QUDataType x);
//头删
void QuPop(Queue* q);
//判空
bool QuEmpty(Queue* q);
//数据个数
int QuSize(Queue* q);
//取头数据
QUDataType QuFront(Queue* q);
//取尾数据
QUDataType QuBack(Queue* q);
//销毁空间(写进数据,想要一次性释放完就来用)
void QuDestroy(Queue* q);
5、队列的源文件:Queue.c
#define _CRT_SECURE_NO_WARNINGS 3
#include"Queue.h"
//初始化头尾节点
void QuInto(Queue* q)
{
//不能传空指针 即(q=NULL)
assert(q);
q->head = NULL;
q->tail = NULL;
q->size = 0;
}
//尾插(2种情况:1.头尾都为NUL 2.又数据入队列了)
void QuPush(Queue* q, QUDataType x)
{
//申请空间
QuNode* newnode = (QuNode*)malloc(sizeof(QuNode));
//判空
if (newnode == NULL)
{
perror("malloc");
return;
}
newnode->a = x;
newnode->next = NULL;//节点创建完成
//判断尾的位置
if (q->tail == NULL)
{
q->head = q->tail = newnode;
}
else
{
q->tail->next = newnode;
q->tail = newnode;
}
q->size++;
}
//头删(删除到尾以后,就不能再删了)
void QuPop(Queue* q)
{
assert(q);
//头的位置也不能为空
assert(q->size!=0);
//此时数据个数为1(也就是最后一个节点)
if (q->head->next==NULL)
{
free(q->head);
q->head = q->tail = NULL;
}
else//q->head != q->tail;
{
QuNode* next = q->head->next;
free(q->head);
q->head = next;
}
//数据个数也要减去
q->size--;
}
//判空
bool QuEmpty(Queue* q)
{
assert(q);
//头尾都相等时到达同一个位置,此时就为真,其他的情况都为假;
return q->size==0;
}
//取头数据
QUDataType QuFront(Queue* q)
{
assert(q);
assert(q->head);
return q->head->a;
}
//取尾数据
QUDataType QuBack(Queue* q)
{
assert(q);
//队尾都为空了,已经没数据了
assert(q->tail);
return q->tail->a;
}
//数据个数
int QuSize(Queue* q)
{
assert(q);
return q->size;
}
//销毁空间(写进数据,想要一次性释放完就来用)
void QuDestroy(Queue* q)
{
assert(q);
QuNode* cur = q->head;
while (cur)
{
QuNode* next = cur->next;
free(cur);
cur = next;
}
q->head = q->tail =NULL;
q->size = 0;
}
如果有不理解的或者看不懂的地方,希望能留下你的评论;
完 结 撒 花