1.前置说明
在学习二叉树链式结构的基本操作前,需要创建一颗链式二叉树,然后才能学习其基本操作
由于现在对于二叉树结构掌握还不够深入,为了降低学习成本,此处手动构建一颗简单的链式二叉树,方便快速进入链式二叉树的操作和学习,等二叉树结构了解的差不多的时候,再回头研究二叉树的创建方式
本处创建简易二叉树实现代码(注意:此代码并不是创建二叉树的方式,真正创建二叉树方式后序详解重点讲解。)
typedef int BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType data;
}BTNode;
BTNode* BuyBTNode(BTDataType x)
{
BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));
assert(newNode);
newNode->data = x;
newNode->left = newNode->right = NULL;
}
BTNode* CreatBinaryTree()
{
BTNode* node1 = BuyBTNode(1);
BTNode* node2 = BuyBTNode(2);
BTNode* node3 = BuyBTNode(3);
BTNode* node4 = BuyBTNode(4);
BTNode* node5 = BuyBTNode(5);
BTNode* node6 = BuyBTNode(6);
node1->left = node2;
node2->left = node3;
node1->right = node4;
node4->left = node5;
node4->right = node6;
return node1;
}
再看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:
1. 空树
2. 非空:根节点,根节点的左子树、根节点的右子树组成的。
从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。
2.二叉树的遍历
2.1 前序、中序、后序遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
2.1.1 前序遍历
前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
通俗一点讲,就是先访问根节点,再去访问左子树,最后才是右子树
换成遍历角度来说,遍历打印,先打印根节点的数据,再打印其左子树的数据,再打印其右子树的数据,在实现中就要把每个节点都当成是根节点遍历,遍历完根节点后再把其左右子树作为独立的根节点进行遍历,直到遍历完整颗树
实现代码:
前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
如果root!= NULL 打印root的值,并且把左右子节点进行递归
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
实现步骤图
2.1.2 中序遍历
中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
换个角度讲,就是先访问左子树,直到访问到左子树的叶子节点后再访问(打印根节点)后再访问其右子树
每颗子树的展开都当做一个独立的树,这颗独立的树由根节点、左子树、右子树组成
实现代码:
中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
先访问数的左子树,直到左子树访问完
InOrder(root->left);
再返回访问根节点
printf("%d ", root->data);
最终再去访问右子树
InOrder(root->right);
}
实现步骤图
2.1.3 后序遍历
后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
思想与上面相同,执行逻辑发生变化
实现代码
后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
实现步骤图
2.2 层序遍历
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
示意图↓
实现思想:
要实现链式二叉树的层序遍历,对于其基本结构的理解必不可少,每个节点中,都会存有其左子树和其右子树的地址,以这个特性,再结合利用队列先进先出的特性先把树的起始根节点放入队列,然后每次从队列中出数据时,把Pop的这个数据中的左子树和右子树入到队列中
即实现每出一个根节点,就带入它的子节点,每个节点又可以看成是独立的根节点,每次Pop的都是先进的根节点
实现思想动图化
实现代码
队列代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
struct BinaryTreeNode;
typedef struct BinaryTreeNode* QDataType;
typedef struct QueueNode
{
QDataType val;
struct QueueNode* next;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
}Queue;
//初始化
void QueueInit(Queue* ps);
//销毁
void QueueDestory(Queue* ps);
//插入数据
void QueuePush(Queue* ps, QDataType x);
//出数据
void QueuePop(Queue* ps);
//获取队头数据
QDataType QueueFront(Queue* ps);
//获取队尾数据
QDataType QueueBack(Queue* ps);
//队列中的数据数量
size_t QueueSize(Queue* ps);
//队列是否为空
bool QueueEmpty(Queue* ps);
void QueuePrint(Queue* ps);
#define _CRT_SECURE_NO_WARNINGS 1
#include "Queue.h"
void QueuePrint(Queue* ps)
{
assert(ps);
QNode* cur = ps->head;
while (cur)
{
QNode* next = cur->next;
printf("%d ", cur->val);
cur = next;
}
printf("\n\n");
}
//初始化
void QueueInit(Queue* ps)
{
assert(ps);
ps->head = ps->tail = NULL;
}
//销毁
void QueueDestory(Queue* ps)
{
assert(ps);
QNode* cur = ps->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
ps->head = ps->tail = NULL;
}
//插入数据
void QueuePush(Queue* ps, QDataType x)
{
assert(ps);
QNode* newNode = (QNode*)malloc(sizeof(QNode));
assert(newNode);
newNode->val = x;
newNode->next = NULL;
if (ps->head == NULL)
{
ps->head = ps->tail = newNode;
}
else
{
ps->tail->next = newNode;
ps->tail = ps->tail->next;
}
}
//出数据
void QueuePop(Queue* ps)
{
assert(ps);
assert(ps->head);
QNode* delt = ps->head;
ps->head = ps->head->next;
free(delt);
delt = NULL;
}
//获取队头数据
QDataType QueueFront(Queue* ps)
{
assert(ps);
assert(ps->head != NULL);
return ps->head->val;
}
//获取队尾数据
QDataType QueueBack(Queue* ps)
{
assert(ps);
assert(ps->head != NULL);
return ps->tail->val;
}
//队列中的数据数量
size_t QueueSize(Queue* ps)
{
assert(ps);
size_t ts = 0;
QNode* size = ps->head;
while (size)
{
ts++;
size = size->next;
}
return ts;
}
//队列是否为空
bool QueueEmpty(Queue* ps)
{
assert(&ps);
return ps->head == NULL;
}
层序遍历实现
void LevelOrder(BTNode* root)
{
Queue qt;
QueueInit(&qt);
初始化队列中的头数据为树的根
if (root)
{
QueuePush(&qt, root);
}
如果队列不为空,则一直出数据,出数据的同时入其左右子节点
如果子节点不为空,则入队列,为空则不入
while (!QueueEmpty(&qt))
{
BTNode* front = QueueFront(&qt);
QueuePop(&qt);
if (front->left)
QueuePush(&qt, front->left);
if (front->right)
QueuePush(&qt, front->right);
printf("%d ", front->data);
}
printf("\n");
QueueDestory(&qt);
}
3.链式二叉树的功能函数
3.1 二叉树的节点个数
实现思想:
方法1.在调用函数的接口中,传一个计数变量指针过来,通过递归对个数进行累积
方法2.利用函数递归本身进行计数,累加后作为返回值返回一个二叉树的个数
注意:不推荐使用静态局部变量,或者全局变量,这样在多线程中,造成线程安全问题
静态局部变量哪怕在递归中写成 static int count = 0; 这个初始化只会在这个程序中这个递归第一次运行时才会初始化,并且后续运行中不再进行任何初始化,并且这个变量存在于静态区,在多线程中多个线程调用会导致出现线程安全问题
全局变量虽然随时可以重置,但是如果在多线程中可能也会出现线程安全问题
实现代码(这里使用后序遍历进行实现):
1int BinaryTreeSize(BTNode* root)
{
if(root == NULL)
return 0;
使用后序遍历,走到这一步自身肯定不为空,自身就已经计数1
把左右子树统计个数加上自身就是这颗树的节点数量
return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
3.2 二叉树叶子节点个数
实现思想:
在前序遍历的方法下进行变形,如果这个根节点的度不为0,那这个节点就不是叶子节点,则对它的左右子树进行访问,并将接下来的每个子树依次当作根节点进行判断其是否是叶子节点
访问到NULL,则返回0,访问到度为0的节点则返回1,并将结果相加后返回
实现代码:
int BinaryTreeLeafSize(BTNode* root)
{
if(root == NULL)
return 0;
如果这个节点的度为0,也就是左右子树都是空,则返回1
if(root->left == NULL && root->right == NULL)
return 1;
左右子树计算结果相加
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
3.3 二叉树第k层节点个数
实现思想:
求第K层节点就很简单了,递归时进行递减,只要k==1 并且第k层的根节点不等于空 则为一个有效个数
实现代码:
二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
如果root==NULL 返回0;
if (root == NULL)
return 0;
走到这一步代表root不是空,判断是否是第k层,如果是那就返回1;
k不用额外判断会不会小于等于0 因为k每次是递减1,只要节点不等于空,迟早会等于1
if (k == 1)
return 1;
如果都不满足,那就继续走
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
3.4 二叉树查找值为x的节点
实现思想:
走到每个根节点时都进行判断,如果这就是值为x的节点,则结束遍历查找,直接返回该值的地址
实现代码:
二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
如果节点是空则返回空
if (root == NULL)
return NULL;
if (root->data == x)
return root;
走到这一步则不是空,先从左子树里找,如果找到了直接返回结束程序不用再去右子树找
BTNode* BTleft = BinaryTreeFind(root->left, x);
if (BTleft != NULL)
{
return BTleft;
}
如果左子树找不到,再去右子树找
BTNode* BTright = BinaryTreeFind(root->right, x);
if (BTright != NULL)
{
return BTright;
}
如果都没找到 就返回空
return NULL;
}
4.二叉树的创建和销毁
4.1 二叉树的构建
二叉树的构建其实是通过遍历数组创建
我现在要创建一个二叉树,给出一个char类型的数组,对其进行先序遍历构建
例如如下的先序遍历字符串: ABC##DE#G##F### 其中“#”表示的是空格,空格字符代表空树。
实现思想:
每次进入判断指针指向的数据是否为 '#' 如果是'#' 则代表是空树直接返回空
如果不是'#' 则malloc一个节点,并把指针指向的元素赋予节点的data
并对其左子树和右子树进行同样以上的步骤赋值
实现代码:
#include <stdio.h>
#include <stdlib.h>
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
char data;
}BTNode;
//创建二叉树
BTNode* BinaryTreeCreate(char* a, int* pi)
{
如果*pi是# 则代表空树
if(a[*pi] == '#')
{
(*pi)++;
return NULL;
}
走到这代表*pi肯定不是空树,则创建一个节点放入
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
root->data = a[*pi];
(*pi)++;
此时数组中仍然可能还有其他数据,在数组走完之前
对左右子树进行遍历构建并赋值给上一颗树的左右子树指针
root->left = BinaryTreeCreate(a,pi);
root->right = BinaryTreeCreate(a,pi);
return root;
}
int main()
{
char a[100];
scanf("%s",a);
int count = 0;
BTNode* root = BinaryTreeCreate(a,&count);
return 0;
}
4.2 二叉树的销毁
太容易实现了,直接上代码,自行体会~
二叉树销毁
void BinaryTreeDestory(BTNode* root)
{
if (root == NULL)
return;
BinaryTreeDestory(root->left);
BinaryTreeDestory(root->right);
free(root);
}
4.3 判断二叉树是否是完全二叉树
实现思想:
用层序遍历,但是这次把空一起入进去,如果队列Pop到空节点时,则判队列是否为空,如果为空则是完全二叉树
如果不是空,则把队列中剩下的数据出完,这些数据中如果有非空节点则不是完全二叉树
实现代码:
判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root)
{
Queue qt;
QueueInit(&qt);
if (root)
{
QueuePush(&qt, root);
}
while (!QueueEmpty(&qt))
{
如果front!= NULL 则继续push
如果front == NULL 则出去判断
BTNode* front = QueueFront(&qt);
QueuePop(&qt);
if (front == NULL)
break;
QueuePush(&qt, front->left);
QueuePush(&qt, front->right);
}
判断队列是否为空
while (!QueueEmpty(&qt))
{
走到这里肯定不为空,此时如果树是完全二叉树,队列中剩余的就全都会是空
一旦出现非空,则这棵树不为完全二叉树
BTNode* front = QueueFront(&qt);
QueuePop(&qt);
如果front为非空,返回false
if (front)
{
QueueDestory(&qt);
return false;
}
}
走到这里则代表上一步已经把队列中剩余的元素都pop干净,并且都是NULL 这棵树则为完全二叉树
QueueDestory(&qt);
return true;
}