本文主要讲解链式二叉树的遍历和创建等知识点
文章目录
前言
我们在学习链表的时候,我们都学习他的增删查改,但是我们的二叉树,对于这种复杂的结构,我们的增删查改就没有意义,你要怎么插入,插入那个还是不是二叉树,这是一个很大的问题,但是我们的遍历是有意义的。
1、链式二叉树的遍历的介绍
链式二叉树的遍历一共有四种
- 前序遍历
- 中序遍历
- 后序遍历
- 层序
(1)前序遍历
又称为先根遍历,遵循的原则就是访问根结点的操作发生在遍历其左右子树之前。
例如:采用前序遍历下面的树
前序遍历结果:
1 2 3 NULL NULL NULL 4 5 NULL NULL 6 NULL NULL
首先我们是根结点,第一个根是1,然后左子树,对于左子树的根是2,然后再分,2的左子树是的根是3,3的左子树是NULL,然后再进行3的右子树也是NULL,当2的左子树遍历完,就该遍历2的右子树,而2的右子树是NULL,然后将1的左子树遍历完了,就该遍历1的右子树,1的右子树的根是4,然后4的左子树的5,5的左子树是NULL,右子树是NULL,然后再遍历4的右子树的根是6,他的左右子树也是NULL。
这样就遍历完了
前序遍历的递归图解
(2)中序遍历
遵循的原则就是访问根结点的操作发生在遍历其左右子树之中(间)
例如:采用中序遍历下面的树
前序遍历结果:
NULL 3 NULL 2 NULL 1 NULL 5 NULL 4 NULL 6 NULL
因为先遍历左子树,对于1来说,他是整个数的根,所以先遍历他的坐姿是,但是对于左子树来说,2就是他的根,对于2 这颗树他的左子树是3,3的左子树是NULL,所以第一个就是NULL,然后是他的根3,然后就是他的右子树NULL,当2的左子树的遍历完,然后就是根2,然后就是2的右子树NULL,这时就把1的左子树遍历完了,就是根1,然后就是1的右子树,下面的思路就不写了
(3)后序遍历
遵循的原则就是访问根结点的操作发生在遍历其左右子树之后。
例如:采用后序遍历下面的树
前序遍历结果:
NULL NULL 3 NULL 2 NULL NULL 5 NULL NULL 6 4 1
(3)层序遍历
层序就是一层一层遍历,就是1,2,4,3,5,6
用队列的方式实现层序
注:这一部分可以先跳过,等后面前中后以及二叉树的问题结束后再去理解这个
其大思路就是,先将根节点放入队列,然后将根节点出队列,再把根节点的左右节点放入队列,即:出一层,带入下一层。
// 二叉树层序遍历
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root != NULL)
{
QueuePush(&q, root); //将根节点放入队列
}
while (!QueueEmpty(&q))
{
QDataType front = QueueFront(&q); //获取对头,front不可能为空,因为我们只把非空的传到队列里
QueuePop(&q); //删除对头
printf("%d ", front->data);
if (front->left) //将当前根节点的左节点放入队列
{
QueuePush(&q, front->left);
}
if (front->right)//将当前根节点的右节点放入队列
{
QueuePush(&q, front->right);
}
}
QueueDestroy(&q);
}
用队列判断二叉树是否为完全二叉树
完全二叉树:前h-1层为满二叉树,最后可以不满,但是必须连续
我们可以使用层序遍历去解决这个问题,我们先将根节点放入队列,然后获取队头,出队列,判断队头是不是空,队头不是空就继续循环把他的左右节点入队列,如果队头是空的时候。我们结束循环,判断队列里面的是不是全部为NULL,如果是那就是完全二叉树,反之则否
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root != NULL)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
QDataType front = QueueFront(&q); //front不可能为空,因为我们只把非空的传到队列里
QueuePop(&q);
if (front == NULL)
{
break;
}
else
{
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
}
//将剩余的继续出队列,如果后面的全部为空,那就是完全,否则就不是
while (!QueueEmpty(&q))
{
QDataType front = QueueFront(&q); //front不可能为空,因为我们只把非空的传到队列里
QueuePop(&q);
if (front != NULL)
{
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
总结
我们的前面三个遍历,前序,中序,后序,三个遍历。
我们可以发现我们的遍历,都是将大问题转换为小问题,然后等我们遇到空指针就是结束,这是不是就把我们实现递归的两个条件实现了。
2、递归解决二叉树的问题
我们首先自己创建一个二叉树,来实现二叉树的各种遍历,最后我们再写一个真正的二叉树创建函数(将数组转化为二叉树)
(1)二叉树节点的创建
我们知道一个链式二叉包含
data,left right
typedef char BTDataType;
//节点的结构
typedef struct BTNode
{
BTDataType data;
struct BTNode* left;
struct BTNode* right;
}BTNode;
(2)为每个节点开辟一块空间
//开辟一个节点空间
BTNode* BuyNode(BTDataType x)
{
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->left = NULL;
newnode->right = NULL;
return newnode;
}
(3)手动链接一个二叉树
我们创建6个节点,然后自己手动链接起来成为一个二叉树
//自己创建一个二叉树
BTNode* CreatBinaryTree()
{
BTNode* n1 = BuyNode(1);
BTNode* n2 = BuyNode(2);
BTNode* n3 = BuyNode(3);
BTNode* n4 = BuyNode(4);
BTNode* n5 = BuyNode(5);
BTNode* n6 = BuyNode(6);
//BTNode* n7 = BuyNode(7);
n1->left = n2;
n1->right = n4;
n2->left = n3;
//n2->right = n7;
n4->left = n5;
n4->right = n6;
return n1;
}
(4)二叉树前序遍历
前序遍历就是先打印根,再打印左子树,再打印右子树
// 二叉树前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
printf("%c ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
完成递归的两个条件
1.有结束递归截至条件
2.每次递归都要接近这个条件,将我们大问题转换为小问题
首先我们问一下,我们的遍历满不满足递归的两个条件
将大问题转换为小问题,
我们要遍历一整棵树=>
遍历左子树和右子树,对于左子树和右子树来说,他也有自己的左右子树,又分为了更小的问题,知道遇到了NULL
就结束,这个就是它们的结束条件。
(5)二叉树中序遍历
前序遍历就是先打印左子树,再打印根,再打印右子树
// 二叉树中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
InOrder(root->left);
printf("%c ", root->data);
InOrder(root->right);
}
(6)二叉树后序遍历
// 二叉树后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
(7)求二叉树的结点个数
还是大问题转换为小问题,求节点的个数=左子树的结点个数+右子树的结点个数+1(表示自身节点个数)。遇到空指针,然回0,表示个数为0.
// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
(8)求叶子结点的个数
叶节点:左右子树都为NULL的节点
我们还是跟上面差不多,遇到空就返回0,遇到叶子就返回1,然后将左右子树的叶子加在一起就行。
但是注意的点是防止空指针的解引用,我们要加一个判断
// 二叉树叶子节点个数
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);
}
(9)求树的高度
错误代码展示:
//求树的高度
int TreeHeight(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int height = TreeHeight(root->left) > TreeHeight(root->right) ?
TreeHeight(root->left) + 1 : TreeHeight(root->right) + 1;
return height;
}
其实这个会造成极大的浪费,我们光看代码没有任何的问题,但是我们再算出左面和右面的高度,并没有保存,然后后面在比较的时候有进行了依次递归,然后才取出我们的高度。大家不要小看这个,高度越高,我们的最后一层进行的次数越多,改进方法就是将我们的高度保留下来,在比较。
具体的可以画一下递归展开图
正确代码:
// 二叉树的高度
int BinaryTreeHeight(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int leftHeight = BinaryTreeHeight(root->left); //递归求左子树的高度并且保存
int rightHeight = BinaryTreeHeight(root->right); //递归求左子树的高度并且保存
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
(9)第K层的节点个数 k >= 1
分解成子问题,就是第k层=左子树第k-1层的个数+右子树的第k-1层的个数
知道k==1,并且不等于NULL返回1.
// 二叉树第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);
}
(10)二叉树查找值为x的结点
这种查找的同样需要保存,因为递归是一层一层递归的,我们需要保存返回值!!
错误代码:
//二叉树查找值为x的结点
BTNode* TreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
TreeFind(root->left, x);
TreeFind(root->right, x);
return NULL;
}
解释:为什们会写出这样的代码,首先遇到空指针,就说明找不到我们就不找了,返回,遇到等于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;
}
(11)二叉树的销毁
用后序遍历销毁,我们先将左右子树销毁,然后再销毁根结点。因为如果先销毁根节点,那么就找不到左右子树了
// 二叉树销毁
void BinaryTreeDestory(BTNode* root)
{
if (root == NULL)
{
return;
}
BinaryTreeDestory(root->left);
BinaryTreeDestory(root->right);
free(root);
}
3、二叉树的创建
这个其实就是传给你一个数组,你要帮我变成链式二叉树
例如:通过前序遍历的数组"ABD##E#H##CF##G##"
构建二叉树
//通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
if (a[*pi] == '#')
{
(*pi)++;
return NULL;
}
//构建节点并连接
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = a[(*pi)++]; //将数组元素放入。(*pi)++,继续遍历
newnode->left = BinaryTreeCreate(a, n, pi); //递归创建下一个节点,链接在左子树上
newnode->right = BinaryTreeCreate(a, n, pi);
return newnode;
}