一、相关头文件
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<stdbool.h>
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root);
和完全二叉树不同,一般得二叉树不能通过下标得算数关系来找到子节点或者父节点,所以利用链表来存储更好;left指向根节点的左子树,right指向根节点的右子树;
二叉树的实现设计到递归,并且一般都是左子树和右子树两个递归;
二、函数定义
为了方便理解,首先从二叉树的前、中、后三种遍历说起;
首先,手动创建一个简单的二叉树作为测试用例;
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType _data;
struct BinaryTreeNode* _left;
struct BinaryTreeNode* _right;
}BTNode;
BTNode* CreatBinaryTree()
{
BTNode* node1 = BuyNode(1);
BTNode* node2 = BuyNode(2);
BTNode* node3 = BuyNode(3);
BTNode* node4 = BuyNode(4);
BTNode* node5 = BuyNode(5);
BTNode* node6 = BuyNode(6);
node1->_left = node2;
node1->_right = node4;
node2->_left = node3;
node4->_left = node5;
node4->_right = node6;
return node1;
}
申请节点:
BTNode* BuyNode(BTDataType x)
{
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
if (newnode == NULL)
{
perror("malloc failed");
exit(1);
}
newnode->data = x;
newnode->right = newnode->left = NULL;
return newnode;
}
那么此时该二叉树就是这样的:
叶子节点(3、5、6所在的节点)的左右子树都是NULL;
1、前序遍历
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
else
{
printf("%c ", root->data);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
}
什么是前序遍历?
前序遍历:先访问根节点,再访问左子树,再访问右子树;
看到上面的测试用例,那么就是先是 根节点1 再是1的左子树,在1的左子树里面又有根节点2,那么就先是2 再是2的左子树;2的左子树是3,3作为新的根节点,先访问3,再访问3的左子树和右子树都是NULL,就返回,此时2的左子树访问完了,接着访问2的右子树,是NULL,返回,此时1的左子树访问完了,去访问1的右子树;
右边也是同样的道理:先将4作为新的根节点,访问4,再去访问4的左子树,4的左子树是5,将5作为新的根节点,访问5之后去访问5的左子树,是NULL,返回,再去访问5的右子树,也是NULL,也返回;那么4的左子树就访问完了;再去访问4的右子树,4的右子树的根节点是6,先访问6,之后访问6的左子树是NULL,返回,再去访问6的右子树,是NULL,也返回,那么4的右子树也访问完了。此时,整棵树的前序遍历就完成了.
打印出来就是 1 2 3 N N N 4 5 N N 6 N N (N代表是NULL);
代码:这里递归的思想很明显:递归是将大的问题拆分成子问题,再去一个一个解决;在使用递归的时候要找到结束条件和每个子问题的基本解决思路,这些子问题的解决思路都一样,只不过子问题的差别在于大小,而每一个大一点的子问题都是由小的子问题组成的;
##代码思路:通过观察,可以发现每个子问题都是先访问根节点,再去访问左子树和右子树;结束条件就是访问到空节点时就结束访问,因为此时没有节点去访问;
走一遍代码:先判断根节点是不是NULL,是的就直接结束函数;不是就访问根节点,打印出根节点root的data值,此时打印的是 1;接着去访问左子树,左子树里面2又是根节点,和之前同样的操作,打印2,再去访问2的左子树,打印3,访问3的左子树,为空,返回,那么此时3的左子树调用的函数就结束了,去访问3的右子树,为NULL,同样地,也结束;都打印N;那么此时2的遍历左子树的函数就结束了,再去看2的右子树,为空,打印N,结束2遍历右子树的函数,那么1的遍历左子树就完了,再去看1的右子树。同样的思路。
红线代表递,绿线代表归;
2、中序遍历
先访问左子树,再访问根节点,再访问右子树;思路和前序遍历一样;
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
else
{
BinaryTreeInOrder(root->left);
printf("%c ", root->data);
BinaryTreeInOrder(root->right);
}
}
打印出来就是 N 3 N 2 N 1 N 5 N 4 N 6 N ;
3、后序遍历
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
else
{
BinaryTreePostOrder(root->left);
BinaryTreePostOrder(root->right);
printf("%c ", root->data);
}
}
先访问左子树,再访问右子树,最后访问根节点;
打印出来就是:N N 3 N 2 N N 5 N N 6 4 1 ;
4、二叉树的节点个数
// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : BinaryTreeSize(root->left)
+ BinaryTreeSize(root->right) + 1;
}
找到结束条件是节点为NULL,就结束,返回0,代表没有节点;
不为空就返回左子树的节点个数加上右子树的节点个数再加上1(就是此时的根节点);这是每一个子问题的解决思路,看到只有一个节点的情况:根节点不是NULL,返回左子树的节点个数,w为NULL,返回0,右子树也是返回0,最后加上1,是根节点自己的个数,那么这颗树的节点个数就是1;推广到用例的二叉树也是这样的;
5、二叉树叶子节点个数
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
叶子节点的特点就是左右子树都是NULL,将这个作为结束条件,也就是当一个节点的左右子树都是NULL的时候,就判定这个节点是叶子节点,此时返回1(个数);当根节点是NULL的时候就返回0,此时代表没有叶子节点;
子问题的思路:根节点不是NULL,就在左子树和右子树里面找,找到左右子树都是NULL的节点就返回1;叶子节点的个数应该是左右子树的叶子节点相加,所以返回的时候要返回左子树加上右子树的叶子节点个数。
6、k层节点个数
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (root == NULL)
{
return 0;
}
if (k == 1 )
{
return 1;
}
return BinaryTreeLevelKSize(root->left, k - 1)
+ BinaryTreeLevelKSize(root->right, k - 1);
}
结束条件:寻找第k层的节点个数,将k利用起来,每次递的时候就将k-1,因为k是从第一层开始减起的,所以当k减到1的时候恰好就是第k层,那么就将k=1,作为结束条件,k=1就返回1,代表第k层的一个节点;当然,跟节点不能为NULL;
思路:返回左子树和右子树各自第k层的节点个数;
7、寻找二叉树中值为x的节点
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
BTNode* ret1 = BinaryTreeFind(root->left, x);
if (ret1)
{
return ret1;
}
BTNode* ret2 = BinaryTreeFind(root->right, x);
if (ret2)
{
return ret2;
}
return NULL;
}
当递归调用到的函数立面的根节点的data值为x的时候就返回;当递归到根节点为NULL的时候就返回NULL,代表没有找到;
先判断根节点是不是,若是就直接返回根节点;若不是就先从左子树开始找起,若找到了那么ret1就是这个节点,并且每次递归回来的ret1收到的都是找到的节点,此时也不用遍历右子树;若左子树里面没有再去右子树里面去找,没有就返回NULL;
在二叉树里面利用到递归的时候,可以先从只含有一个节点的二叉树入手去分析问题;