目录
二叉树
为了了解二叉树,首先要了解树的概念。
树的概念和存储结构
树的概念
节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I等节点为叶节点
非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G等节点为分支节点
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图,A是B的父节点
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
森林:由m(m>0)棵互不相交的树的集合称为森林.
注意: 子树直接是不相交的。任何一颗树都能被拆为,根--子树
除了根节点之外,所有节点都只有一个父节点。
树的存储结构
第一种,用一个顺序表来存储,有几个孩子就扩容几个_childSL,这种方式比较麻烦,不常用。
第二种,左孩子右兄弟表示法(更优于第一种)。左孩子:指向第一个孩子,右兄弟:指向自己同个母亲节点的兄弟。这种表示方法比起顺序表更清晰,容易实现,更常使用。
二叉树的概念和结构
二叉树的概念
不存在度大于2的节点的数被称为二叉树,二叉树由一个根节点,一个左子树和一个右子树组成。
满二叉树
满二叉树的总节点数为:2^h-1,h为树的高度。所以我们假设一颗满二叉树的总节点数为N,那么可以得到:2^h-1=N,转换一下就可以得到N=log2(h+1),可以省略1,得到N=log2h,这就是我们遍历一遍满二叉树的时间复杂度。
完全二叉树
满二叉树就是完全二叉树的一种。对于完全二叉树其前h-1层都是满的,最后一层不一定是满的(满二叉树就一种特殊完全的二叉树),但是最后一层从左到右是连续的,不能跳着存放。
所以完全二叉树的节点个数就是:[2^(h-1),2^h-1]
二叉树性质
1. 若规定根节点的层数为1(二叉树的前提),则一棵非空二叉树的第i层上最多有2(i-1)个结点。
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2h-1。(由等比数列求得)
3. 对任何一棵二叉树, 如果度为0其叶结点个数为a,度为2的分支结点个数为b,则有a=b+1,即:度为0的节点个数比度为2的节点个数多一个。
4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log2(n+1)(注:是log以2
为底,n+1为对数。因为n=2h-1)
5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
1. 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
2. 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
3. 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
二叉树的存储结构
顺序表二叉树
由于顺序表存储是不能空着的,所以若用顺序表存储二叉树的话,最好存储完全二叉树。虽然用顺序表存储树的条件比较苛刻,但这种存储方式还是有个很好的优点的,用顺序表存储完全二叉树就可以通过对下标的+-*/就可以从孩子节点访问到父亲节点或者从父亲节点访问到孩子节点。以下是用顺序表存储完全二叉树时下标的变化方式:
左孩子的下标=父亲节点下标*2+1; leftchild=parent*2+1;
右孩子的下标=父亲节点下标*2+2; rightchild=parent*2+2;
父亲节点下标=(孩子节点-1)/2; parent=(child-1)/2;
如果不是完全二叉树,用顺序表存储则会有些麻烦
所以完全二叉树更适合顺序表存储,如果不是完全二叉树则更适合链式存储。
链式二叉树
普通的二叉树(不是完全二叉树),一般可以用链式结构。链式结构的二叉树通常应用于搜索二叉树。
搜索二叉树:左边的节点值比根小,右边节点值比根大。如下图所示:
若在搜索二叉树中查找值,我们最多需要找高度次,时间复杂度也就是log2N(理想情况下),实际上会比log2N更高,这时我们就需要AVL树和红黑树来减少我们的时间复杂度,那些知识点比较复杂,这里就不做讲解。
注:任何一颗二叉树都要拆为根、左子树、右子树来看,这种思想可以提高我们对二叉树递归方面的理解。
链式二叉树的遍历
二叉树的遍历分为4种:
前序遍历:根-->左子树-->右子树;根在左子树右子树之前遍历。
中序遍历:左子树-->跟-->右子树;根在左子树右子树之间遍历。
后续遍历:左子树-->右子树-->跟;根在左子树右子树之后遍历。
层序遍历:通过使用队列来实现一层一层访问。
链式二叉树的遍历最好都用递归来进行,我们拿一组链式二叉树的前序遍历来举例。
前序遍历代码如下:
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
//如果检测到NULL的节点,就要返回递归上一层
if (root == NULL)
{
printf("# ");
return;
}
//可以不是打印,这里当做访问当前节点的例子
printf("%c ", root->_data);
//上面访问完当前节点,就要递归左子树和右子树,依次往下遍历
BinaryTreePrevOrder(root->_left);
BinaryTreePrevOrder(root->_right);
}
前序遍历递归内存分配过程模拟:
前序遍历,又称为深度优先遍历(dfs)
层序遍历,又称为广度优先遍历(bfs)。层序遍历案例:扫雷递归扩展或qq共同好友推荐。
二叉树实现案例
Tree.h代码:
#include <stdio.h>
#include <assert.h>
#include <string.h>
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType _data;
struct BinaryTreeNode* _left;
struct BinaryTreeNode* _right;
}BTNode;
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate1(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
//二叉树层数
int leveSize(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);
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root);
我们用链式结构来组成一颗二叉树。用ABD##E#H##CF##G##(前序遍历)组成的二叉树结构如图所示:
tree.h文件设置完成后,我们要根据ABD##E#H##CF##G##来创建树。
tree.c讲解:
用BinaryTreeCreate1函数来创建树遇到‘#’就返回NULL,否则就创建一个树节点,存储当前字符,然后对这个树节点的左右子树进行递归。
用BinaryTreeDestory实现树的销毁,树的销毁就要用到后续遍历了,只有我们在后序遍历时,左右子树都返回NULL,(代表当前节点是叶子节点)我们才free当前节点。
返回二叉树节点个数很简单,只要不为NULL,就返回递归左子树+递归右子树+1。
返回叶子节点个数要根据叶子节点特性(左子树=右子树=NULL),判断递归时遇到的节点是否是叶子节点,如果是,返回1,然后返回左右子树的叶子数加起来的总和。
返回K层节点个数就需要先往下递归K-1次,直到K=1时,就说明我们递归到了我们原输入的K层,这时候如果当前层的节点不为NULL就返回1,然后还是返回左右子树的K层节点数量。
返回二叉树层数的话就需要把后续递归的值,存储到一个变量中返回,然后返回左右子树中更深的那颗子树+1;+1是代表加上自己这层。
查找二叉树值为X的节点,也需要把递归返回的节点存储起来。正常情况若没有找到节点,就会递归到root的NULL中,就会返回NULL,若在某次递归途中找到等于X的节点,就返回当前节点的地址,然后对左右子树返回的地址进行判断,若返回的其中一个不是NULL就说明找到了,直接返回找到的节点给上一个递归,从而实现层层返回。
前序中序后序都比较相似也比较容易实现,前面已经对前序遍历进行过讲解,这里就不再赘述。
层序遍历需要用到队列,用到队列的先进先出原则。
首先,我们要先把第一个节点存入队列中,然后再进循环(因为循环继续条件是队列不为空)。进循环后,先把队首的树节点存储起来,再pop节点,(我们pop的节点时队列节点,树节点并没有动,所以我们还是能够通过pop前存储的front查找我们应该访问的节点)然后通过我们存储的front去寻找左右非空节点。总结来说,就是pop一个节点的同时push被pop节点的左右非空节点。
注意:队列里的节点类型要改成树的节点类型,即队列里的每个元素存储的是树的节点。这样的好处在于我们可以通过提前储存要pop的节点,然后我们pop元素出队列后还可以访问到。
层序遍历如果加个leveSize就可以实现一层一层打印。先把leveSize设置成1,代表树的第一个节点,然后pop元素 leveSize次,就说明把当前层的数据全出完了,然后只要队列不为空,就再重新给leveSize赋值队列当前元素个数,再一层一层的pop,就可以实现一层一层访问打印。
判断二叉树是否是完全二叉树的前提也是层序遍历。(我们要对层序遍历进行改变,不管front的左右子树是否是NULL,都push进队列中)完全二叉树通过层序遍历的话一定是连续的(NULL一定在最后,NULL后续一定没有元素),如果当我们层序遍历遇到NULL时,退出循环,这时候若我们队列中除去NULL如果还有其他树节点,那就说明当前二叉树不是完全二叉树。
tree.c代码:
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate1(BTDataType* a, int n, int* pi)
{
if ((*pi) >= n)
{
printf("访问超出数组\n");
exit(-1);
}
if (a[(*pi)] == '#')
{
(*pi)++;
return NULL;
}
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
{
perror("malloc fail");
exit(-1);
}
node->_data = a[(*pi)++];
node->_left = BinaryTreeCreate1(a, n, pi);
node->_right = BinaryTreeCreate1(a, n, pi);
return node;
}
// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
if (*root == NULL)
return;
//用后续遍历,直到左右子树都返回NULL才free节点
BinaryTreeDestory((*root)->_left);
BinaryTreeDestory((*root)->_right);
free(*root);
}
// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
return 0;
return BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1;
}
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
return 0;
if (root&&root->_left == NULL&&root->_right == NULL)
return 1;
return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);
}
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
assert(k > 0);
if (root == NULL)
return 0;
if (k == 1 && root != NULL)
return 1;
return BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1);
}
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->_data == x)
return root;
//把左右存储起来,方便后续判断然后返回地址
//单纯的||只能返回true和false,无法返回地址
BTNode* left = BinaryTreeFind(root->_left, x);
BTNode* right = BinaryTreeFind(root->_right, x);
//如果返回的left和right中有一个不是NULL,就进入判断,返回不是NULL的地址
if (left || right)
{
if (left)
return left;
else
return right;
}
}
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
printf("%c ", root->_data);
BinaryTreePrevOrder(root->_left);
BinaryTreePrevOrder(root->_right);
}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
BinaryTreeInOrder(root->_left);
printf("%c ", root->_data);
BinaryTreeInOrder(root->_right);
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
if (root == NULL)
{
printf("# ");
return;
}
BinaryTreePostOrder(root->_left);
BinaryTreePostOrder(root->_right);
printf("%c ", root->_data);
}
// 层序遍历
//用队列实现
void BinaryTreeLevelOrder1(BTNode* root)
{
//创建一个队列
Queue ql;
QueueInit(&ql);
//先提前放个元素进去,方便后面进入while循环
//把队列传输元素的变量类型改为树的结点类型,这样好方便传输值(值就是树的结点)
Queuepush(&ql, root);
//由于后面需要用到BinaryTreeFind函数,先提前备份root
while (!QueueEmpty(&ql))
{
//用树结构体的指针存放要出列的数据
BTNode* front = Queuetop(&ql);
//pop是队列里的,树里的节点还在,所以front还能访问到被pop出来的树的结点
Queuepop(&ql);
printf("%c ", front->_data);
if (front->_left != NULL)
{
Queuepush(&ql, front->_left);
}
if (front->_right != NULL)
{
Queuepush(&ql, front->_right);
}
}
QueueDestory(&ql);
}
//一层一层访问打印
void BinaryTreeLevelOrder1(BTNode* root)
{
//创建一个队列
Queue ql;
QueueInit(&ql);
//先提前放个元素进去,方便后面进入while循环
//把队列传输元素的变量类型改为树的结点类型,这样好方便传输值(值就是树的结点)
Queuepush(&ql, root);
//由于后面需要用到BinaryTreeFind函数,先提前备份root
int leveSize = 1;
while (!QueueEmpty(&ql))
{
while (leveSize--)
{
//用树结构体的指针存放要出列的数据
BTNode* front = Queuetop(&ql);
//pop是队列里的,树里的节点还在,所以front还能访问到被pop出来的树的结点
Queuepop(&ql);
printf("%c ", front->_data);
if (front->_left != NULL)
{
Queuepush(&ql, front->_left);
}
if (front->_right != NULL)
{
Queuepush(&ql, front->_right);
}
}
printf("\n");
leveSize = QueueSize(&ql);
}
QueueDestory(&ql);
}
// 判断二叉树是否是完全二叉树 返回1 是完全二叉树 0不是
int BinaryTreeComplete(BTNode* root)
{
Queue ql;
QueueInit(&ql);
Queuepush(&ql, root);
while (!QueueEmpty(&ql))
{
BTNode* front = Queuetop(&ql);
Queuepop(&ql);
//遇到NULL就退出循环
if (front == NULL)
break;
//不管是不是NULL都加进队列
Queuepush(&ql, front->_left);
Queuepush(&ql, front->_right);
}
while (!QueueEmpty(&ql))
{
BTNode* front = Queuetop(&ql);
Queuepop(&ql);
if (front != NULL)
return 0;
}
return 1;
QueueDestory(&ql);
}
//二叉树层数
int leveSize(BTNode* root)
{
if (root == NULL)
return 0;
int leftnum = leveSize(root->_left);
int rightnum = leveSize(root->_right);
return leftnum>rightnum ? leftnum + 1 : rightnum + 1;
}