前言
我们在学习链表的时候,我们都学习他的增删查改,但是我们的二叉树,对于这种复杂的结构,我们的增删查改就没有意义,你要怎么插入,插入那个还是不是二叉树,这是一个很大的问题,但是我们的遍历是有意义的。
链式二叉树的遍历
链式二叉树的遍历一共有四种
- 前序遍历
- 中序遍历
- 后序遍历
- 层序
我们就来依次看看我们的遍历
前序遍历
遵循的原则就是
根,左,右
,就是先是根结点,然后左子树,右子树。
我先给一个树,然后在遍历。大家看看怎么遍历。
我们先给答案:
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
。
这样就遍历完了
中序遍历
遵循的原则就是
左,根,右
,就是先是然后左子树,根结点,右子树。
我先给一个树,然后在遍历。大家看看怎么遍历。
我们先给答案:
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的右子树,下面的思路就不写了
后序遍历
遵循的原则就是
左,右,根
,就是先是然后左子树,右子树,根结点。
我先给一个树,然后在遍历。大家看看怎么遍历。
我们先给答案:
NULL NULL 3 NULL 2 NULL NULL 5 NULL NULL 6 4 1
层序
层序就是一层一层遍历,这个跟前三个完全不一样,
我直接写完就是1,2,4,3,5,6
用队列的方式实现层序
其大思路就是,我们队列进一个元素就是按照根结点进,但是出队列,出一个队列进他的根节点所对应的左右子节点。就这样依次进行就可以将我们想要的数打印出来。
//二叉树的平序遍历
void LeaveOrede(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
QueuePop(&q);
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
printf("\n");
QueueDestroy(&q);
}
总结
我们的前面三个遍历,前序,中序,后序,三个遍历。
我们可以发现我们的遍历,都是将大问题转换为小问题,然后等我们遇到空指针就是结束,这是不是就把我们实现递归的两个条件实现了。
用队列判断二叉树是否为完全二叉树
首先我们层序就是用队列写的,就是按照顺序将我们的二叉树的节点打印出来,当我们遇到空指针,后面还打印数,就说明他不是完全二叉树。否则就是完全二叉树。
//判断二叉树是否为完全二叉树
bool TreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front == NULL)
{
//就检查我们检查的函数放在外面
break;
}
//否则就将删除的左右子树的根入队列
else
{
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
}
//遇到空就会出来,然后我们进行检查
//如果空之后还是空,那么就是完全二叉树
//否则就不是完全二叉树
//检查方法就是继续我们的队列,知道结束看我们后面能不能遇到空
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front != NULL)
{
//return 前我们要销毁队列
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
递归解决二叉树的问题
既然我们不进行二叉树的增删查改,但是我们要实现他的前序中序和后序,我们可以自己将创建几个结点,然后自己让他们手动链接起来.
自己创建一个二叉树
我们创建一个n个二叉树的结构体的结点,然后自己链接起来
请看下面代码
//开辟n个结点
BTNode* n1 = (BTNode*)malloc(sizeof(BTNode));
BTNode* n2 = (BTNode*)malloc(sizeof(BTNode));
BTNode* n3 = (BTNode*)malloc(sizeof(BTNode));
BTNode* n4 = (BTNode*)malloc(sizeof(BTNode));
BTNode* n5 = (BTNode*)malloc(sizeof(BTNode));
BTNode* n6 = (BTNode*)malloc(sizeof(BTNode));
if (n1 == NULL && n2 == NULL && n3 == NULL && n4 == NULL && n5 == NULL && n6 == NULL)
{
perror("malloc");
exit(-1);
}
//然后根据图将他们链接起来
n1->data = 1;
n2->data = 2;
n3->data = 3;
n4->data = 4;
n5->data = 5;
n6->data = 6;
n1->left = n2;
n1->right = n4;
n2->left = n3;
n2->right = NULL;
n3->left = NULL;
n3->right = NULL;
n4->left = n5;
n4->right = n6;
n5->left = NULL;
n5->right = NULL;
n6->right = NULL;
n6->left = NULL;
现在我们就有了一个简易的二叉树,我们就用它分析就可以了。
(1)前序遍历画图解释递归
我们直接看代码
// 二叉树前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
-
完成递归的两个条件
-
1.能够有条件让循环停下来
2.将我们大问题转换为小问题
首先我们问一下,我们的遍历满不满足递归的两个条件
将大问题转换为小问题,
我们要遍历一整棵树=>遍历左子树和右子树,对于左子树和右子树来说,他也有自己的左右子树,又分为了更小的问题,知道遇到了NULL就结束,这个就是它们的结束条件。
(2)中序遍历代码
而中序就是先走左子树,左子树走完然后再打印根,所以打印根的代码应该放到走左子树的下面。
// 二叉树中序遍历
void InOrder(BTNode* root)
{
if (root == NULL) {
printf("NULL ");
return;
}
PreOrder(root->left);
printf("%d ", root->data);
PreOrder(root->right);
}
(2)后序遍历代码
// 二叉树后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL) {
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
(4)求二叉树的结点个数
还是大问题转换为小问题,求节点的个数=左子树的结点个数+右子树的结点个数+1(表示自身节点个数)。遇到空指针,然回0,表示个数为0.
//求二叉树结点的个数
int TreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return TreeSize(root->left) + TreeSize(root->right) + 1;
}
(5)求叶子结点的个数
我们还是跟上面差不多,就是遇到叶子结点+1,就可以了。
但是注意的点是防止空指针的解引用,我们要加一个判断
//求叶子结点的个数
int TreeLeafSize(BTNode* root)
{
//防止空指针的解引用
if (root == NULL)
{
return 0;
}
if (root->left==NULL&&root->right==NULL)
{
return 1;
}
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
(6)求树的高度
对于这个高度,大家掌握了前面的,大部人人第一回写会写成这样
//求树的高度
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 TreeHeight(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int height1 = TreeHeight(root->left);
int height2 = TreeHeight(root->right);
return height1 > height2 ? height1 + 1 : height2 + 1;
}
(7)第K层的节点个数 k >= 1
分解成子问题,就是第k层=左子树第k-1层的个数+右子树的第k-1层的个数
知道k==1,并且不等于NULL返回1.
//求第K层的节点个数 k >= 1
int TreeKLevelSize(BTNode* root, int k)
{
if (root == NULL)
{
return 0;
}
if (k == 1 && root != NULL)
{
return 1;
}
return TreeKLevelSize(root->left, k - 1) + TreeKLevelSize(root->right, k - 1);
}
(8)二叉树查找值为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* TreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
BTNode* ret1 = TreeFind(root->left, x);
if (ret1)
return ret1;
BTNode* ret2 = TreeFind(root->right, x);
if (ret2)
return ret2;
return NULL;
}
(9)二叉树的销毁
用后序遍历销毁,我们先将左右子树销毁,然后再销毁根结点。要明白我在说的是们遍历一遍后序遍历就可以了,然后我们看销毁的顺序。
void TreeDestory(BTNode* root)
{
if (root == NULL)
{
return;
}
TreeDestory(root->left);
TreeDestory(root->right);
free(root);
//制空不制空无所谓,形参的改变不影响实参
}