我们在上一章中,介绍了二叉树的数组实现方式以及堆的各种操作,但是对于数组存储而擦函数而言,可能会存在很多的空间浪费,所以今天我们来了解下二叉树的链式存储方式以及递归实现二叉树的各种操作
二叉树链式结构的实现
二叉树链式结构的遍历
所谓遍历 (Traversal) 是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问 题。 遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础
对于我们的二叉树而言,最重要的就是遍历操作,那么二叉树结构相对复杂,到底应该怎么遍历呢?
这里就有了我们许多的遍历方式,有层次遍历,前序遍历,中序遍历,后序遍历
前序 / 中序 / 后序的递归结构遍历 :是根据访问结点操作发生位置命名1. NLR :前序遍历 (Preorder Traversal 亦称先序遍历 )—— 访问根结点的操作发生在遍历其左右子树之前。2. LNR :中序遍历 (Inorder Traversal)—— 访问根结点的操作发生在遍历其左右子树之中(间)。3. LRN :后序遍历 (Postorder Traversal)—— 访问根结点的操作发生在遍历其左右子树之后。由于被访问的结点必是某子树的根, 所以 N(Node )、 L(Left subtree )和 R(Right subtree )又可解释为根、根的左子树和根的右子树 。 NLR 、 LNR 和 LRN 分别又称为先根遍历、中根遍历和后根遍历。
这便是我们的三序遍历,先序遍历也可以称之为先根遍历,意思是只要碰到可以视为根的结点,就输出,而后向左子树递归遍历,左子树递归完毕之后再转向右子树,所以对于我们这个树而言,就是遍历A然后转向A的左子树,发现B为根,遍历B,再转向B的左子树D,遍历D,发现D没有左子树,发现D没有右子树,返回到B,转向B的右子树,遍历E,发现E没有左子树,发现E没有右子树,返回到B,返回到A,转向A的右子树,遍历C,发现C没有左子树,发现C没有右子树,返回A,结束
中序遍历与后序遍历同理,差别在于根的遍历顺序
层序遍历 :除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1 ,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第 2 层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
层序遍历顾名思义就是一层一层的遍历
先是第一层A,第二层B,C,其次第三层D,E,F以此类推
那么首先我们先来了解下二叉树的存储结构
typedef char BTDataType;//节点数据类型
typedef struct BinaryTreeNode
{
struct BinaryTreeNode*left;//左子树
struct BinaryTreeNode*right;//右子树
BTDataType data;//数据域
}BTNode;
void PrevOrder(BTNode*root);//前序遍历
void InOrder(BTNode*root);//中序遍历
void PostOrder(BTNode*root);//后序遍历
int TreeSize(BTNode* root);//计算节点个数
int LeafSize(BTNode* root);//计算叶子节点个数
BTNode* TreeFind(BTNode* root, BTDataType x);//查找
void BinaryTreeDestory(BTNode* root);//销毁
void TreeLevelOrder(BTNode* root);//层次遍历
bool BinaryTreeComplete(BTNode* root);//判断是否为完全二叉树
void PrevOrder(BTNode*root)//先序遍历
{
if (root == NULL)
{
printf("NULL");
return;
}
printf("%c ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
void InOrder(BTNode*root)//中序遍历
{
if (root == NULL)
{
printf("NULL");
return;
}
PrevOrder(root->left);
printf("%c ", root->data);
PrevOrder(root->right);
}
void PostOrder(BTNode*root)//后序遍历
{
if (root == NULL)
{
printf("NULL");
return;
}
PrevOrder(root->left);
PrevOrder(root->right);
printf("%c ", root->data);
}
我们可以看到,三序遍历代码很简单,但是并不容易理解,接下来我们用画图的方式去剖析如何使用递归对上述二叉树进行前序遍历
整个树的递归可以总结为这32步,前序遍历总是先输出根,再向左递归直到为NULL,而后向右递归,最后返回上层重复,直至遍历完所有节点
中序遍历我们对其进行省略绘制
中序遍历就是先去遍历左子树,存在左子树就去遍历,直到遇到NULL,再返回上层输出根节点,而后向右继续重复,中序遍历的过程可以参考这30步,我们可以发现,所有输出结点的操作都是在左子树返回之后完成的,这便是中序遍历
后序遍历可以类比,所有输出节点都是在右子树返回后输出的
这便是二叉树的三序遍历递归写法
2.计算节点个数
int TreeSize(BTNode* root)
{
return root = NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
我们在这里只详细绘制A的左子树递归图,采用后序遍历的思想,先将左子树与右子树结点的个数计数再加上自己,然后一同带给上一层,直到最后返回给A,完成所有节点的计数
3.求叶子结点的个数
int LeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (LeafSize(root->left) == NULL&&LeafSize(root->right))
{
return 1;
}
return LeafSize(root->left) + LeafSize(root->right);
}
我们参考上面的求节点个数,对于求叶子节点而言,需要满足的是左右子树都为空,则返回1,其他情况返回0,而最后整棵树是需要统计左右相加,所以得出以上代码,注意:书写递归代码时一定要考虑出递归的情况,以及子问题的统一性
4.二叉树的查找
BTNode* TreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)//空树
{
return NULL;
}
if (root->data == x)//根为目标
{
return root;
}
BTNode* lret = TreeFind(root->left, x);//向左递归查找
if (lret)
return lret;
BTNode* rret = TreeFind(root->right, x);//向右递归查找
if (rret)
return rret;
return NULL;//未找到
}
我们可以发现,在二叉树的查找算法中,当结点不为目标值时,向左递归,找到返回,否则向右,最后返回
5.二叉树销毁
void BinaryTreeDestory(BTNode* root)
{
if (root == NULL)
{
return;
}
BinaryTreeDestory(root->left);
BinaryTreeDestory(root->right);
free(root);
}
我们二叉树的销毁采用后序思想,先销毁左右子树,其次再销毁根,若直接销毁根,则找不到左右子树了,无法后续操作,不过这个函数最后需要在主函数外将根节点置空,内部一级指针无法置空,又为尽量保持接口的一致性,所以使用过后需要置空root
6.层次遍历
void TreeLevelOrder(BTNode* root)
{
Queue q;//创建队列
QueueInit(&q);
if (root)//当根节点不为空入队
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))//当队列不空时
{
BTNode*front = QueueFront(&q);//将队头数据赋给front结点
QueuePop(&q);//出队
printf("%d ", front->data);//打印节点数据
if (front->left)//当左子树不为空
{
QueuePush(&q, front->left);//左结点入队
}
if (front->right)//当右子树不为空
{
QueuePush(&q, front->right);//右节点入队
}
}
QueueDestory(&q);
}
在我们二叉树中还有一种遍历方式,为层序遍历(bfs),我们需要按照层次,第一层,第二层依次遍历,但是我们二叉树的结构并没有同层次间的指针,所以我们需要借助队列,先将第一层入队列,然后出队列,并将第一层的子树入队列,其次出队,以此类推,一层进一层出,便可完成层次遍历
7.判断是否为完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode*front = QueueFront(&q);
QueuePop(&q);
if (front==NULL)//当节点为空时打断(碰到第一个NULL)
{
break;
}
QueuePush(&q, front->left);//节点不为空左右树都入队列
QueuePush(&q, front->right);
}
while (!QueueEmpty(&q))
{
BTNode*front = QueueFront(&q);
QueuePop(&q);
if (!front)//再碰到不为NULL的值则不为完全树
return false;
}
QueueDestory(&q);
return true;
}
我们利用之前层次遍历的思路来判断是否为完全二叉树,当树为完全二叉树时,层次遍历直到遍历出一个NULL,打断,去检查队列剩下的值,当有不为NULL的值时,则不为完全二叉树
我们的二叉树在这里就叙述了一小半内容了,还有一半内容在后面C++部分再做了解