本文重点知识:二叉树的遍历,求二叉树节点个数,叶子节点个数,树的高度,第k层节点个数,查找值为x的节点并返回该节点地址
二叉树的价值~
普通链式二叉树的增删查改价值不大,只能单纯的存储数据~,因此普通的二叉树价值并不大,某些特殊的二叉树树有其特定的用途
比如搜索二叉树就可以用来快速查找特定数据(最多只需要查找高度次,搜索二叉树就是每个节点的左子树比自己小,右子树比自己大~,当然搜索二叉树也是有其缺陷的,后面会引出AVL树,红黑树,B树等等~
但是学习控制普通二叉树的结构对我们后序学习有很大帮助~,因此本篇文章重点介绍普通二叉树
二叉树的遍历
二叉树的遍历方式有4种,之前提到过每一棵树都可以看成3个部分:根,左子树,右子树。根据遍历的顺序分为前序,中序,后序和层序4种~
前序:根 左子树 右子树
中序:左子树 根 右子树
后序:左子树 右子树 根
层序:一层一层访问
例子如下:
我们现在需要遍历这个二叉树,前序,中序,后序,层序的遍历顺序如下~
前序遍历:
关键就是每一棵树的访问顺序都是 根 左子树 右子树, 因此第一棵树的访问顺序是 根 左子树 右子树, 而左子树的访问顺序又是根,左子树,右子树,依次类推,直到访问的树左右子树都是空树就结束了~,由此便得到了整棵树的访问顺序~
这不就是大事化小,小事化了吗?不错,这就是递归思想,把大规模问题化解成与之类型相同但规模更小的子问题,达到递归终止条件时递归停止)
中序、后序和层序~
按照前序的思路,中序后序和层序依次类推,这里就不赘述了~
遍历代码实现~
为了便于快速实现我们要进行的二叉树的各种操作,我们选择手动快速创建一棵二叉树~
#include<stdio.h>
#include<stdlib.h>
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
BTNode* BuyNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
{
perror("malloc fail\n");
return NULL;
}
node->data = x;
node->left = node->right = NULL;
return node;
}
BTNode* CreateBinaryTree()
{
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;
}
前序中序后序遍历代码实现
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
int main()
{
BTNode* root = CreateBinaryTree();
PrevOrder(root);
printf("\n");
InOrder(root);
printf("\n");
PostOrder(root);
printf("\n");
}
可以看到,采用递归算法很好的实现了前序中序后序的遍历~
下面以前序遍历代码为例,我们通过画图深入理解一下二叉树递归遍历的执行过程~
上面这张图就展示了前序遍历递归算法的一个具体过程(只画了遍历根1, 1的左子树的逻辑),每调用一次自己,其实都是在创建该函数的函数栈帧~,所以递归代码虽少,执行起来还是很复杂的~
中序和后序的执行过程不过是改变了遍历根,左子树和右子树的顺序,逻辑类似,就不赘述了~
实现起来较为特殊的遍历:层序遍历
为什么说层序遍历比较特殊呢?因为前中后序的实现都是用递归来实现的,而层序是用非递归来实现的~
具体过程如下所示
层序遍历代码实现
由于需要用到队列,直接把之前写过的代码copy过来
Queue.h
#pragma once
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef struct BinaryTreeNode* QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
void QueuePush(Queue* pq, QDataType x);
void QueuePop(Queue* pq);
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
int QueueSize(Queue* pq);
bool QueueEmpty(Queue* pq);
Queue.c
#include"Queue.h"
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->phead;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail\n");
return;
}
newnode->data = x;
newnode->next = NULL;
if (pq->ptail == NULL)
{
assert(pq->phead == NULL);
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
// 1、一个节点
// 2、多个节点
if (pq->phead->next == NULL)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else
{
// 头删
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
pq->size--;
}
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->phead->data;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->ptail->data;
}
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
/*return pq->phead == NULL
&& pq->ptail == NULL;*/
return pq->size == 0;
}
test.c
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include"Queue.h"
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;
BTNode* BuyNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
if (node == NULL)
{
perror("malloc fail\n");
return NULL;
}
node->data = x;
node->left = node->right = NULL;
return node;
}
BTNode* CreateBinaryTree()
{
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;
}
//层序遍历
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
{
QueuePush(&q, root);
}
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%d ", front->data);
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
printf("\n");
QueueDestroy(&q);
}
int main()
{
BTNode* root = CreateBinaryTree();
LevelOrder(root);
}
大家可以画图理解一下各个指针之间的指向关系,这样会清楚很多~~~
求二叉树节点个数
int BTreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return BTreeSize(root->left) + BTreeSize(root->right) + 1;
}
这节玩的就是递归~
大事化小:求整棵二叉树的节点个数可以转化成求左子树的节点个数+右子树的节点个数+1,而左子树的节点个数和右子树的节点个数仍然可以继续分解成相同类型的问题~
终止条件:当树为空时,返回0
具体执行过程如下:
还是重点理解递归的具体过程,大家把握住下面的这个关键点~
就好比一位老师去教室上课,准备讲解二叉树的代码实现发现没有粉笔了,然后需要去办公室拿几盒粉笔,老师走到了办公室发现粉笔还是不够,于是把班长叫了过来,让班长去教务处拿粉笔,老师这时在办公室等待,等班长拿回粉笔给老师后,老师回到了教室继续讲解二叉树的代码实现~
求二叉树叶子节点个数
int BTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return BTreeLeafSize(root->left) +
BTreeLeafSize(root->right);
}
求二叉树高度
我们先来看这样一种写法~
void BTreeHeight(BTNode* root)
{
if (root == NULL)
{
return 0;
}
return BTreeHeight(root->left) > BTreeHeight(root->right) ?
BTreeHeight(root->left) + 1 : BTreeHeight(root->right) + 1;
}
二叉树的高度其实就是从根节点到叶子节点最长的一条路径上的节点个数~
思路就是比较每个根节点的左子树和右子树的高度,直接返回高度高的树的高度+1(因为要加上根节点自身)
然后依次拆分成子问题,递归终止条件为根为空
这段代码思路是没有问题的,但是有一个致命的缺点就是效率太低了,因此可以发现当比较完左右子树的高度之后,返回高度较高的树的高度+1时,还得重新调用该函数重新计算较高树的高度,这就导致函数被重复执行了很多次,效率极其低下(事实上,树的高度每多一层,最底层函数的调用次数就会变为原来2倍,呈指数形式增长~)
求高度改进写法~
int BTreeHeight(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int leftHeight = BTreeHeight(root->left);
int rightHeight = BTreeHeight(root->right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
用变量保存计算出的子树的高度,这样返回高度+1时,就不必重新调用函数了~
改进写法执行的逻辑图
求二叉树第k层节点个数
思路:转化成求左子树的第k-1层节点个数+右子树的第k-1层节点个数
结束条件:
情况1:k==1 说明已经到了第k层,返回1即可(就是该节点本身)
情况2:root为NULL,此时不管k是几,直接返回0(当前层往下已经没有节点了~)
int BTreeLevelKSize(BTNode* root, int k)
{
assert(k > 0);
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BTreeLevelKSize(root->left, k - 1)
+ BTreeLevelKSize(root->right, k - 1);
}
查找二叉树值为k的节点
思路:按照 根,左子树,右子树这样的前序进行遍
根为NULL,就返回NULL,如果找到了值为k的节点,就返回该节点地址,否则就再找左子树,右子树,依次遍历~~~,左子树和右子树都没找到,就返回NULL;
BTNode* BTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
{
return root;
}
BTNode* ret1 = BTreeFind(root->left, x);
if(ret1)
return ret1;
BTNode* ret2 = BTreeFind(root->right, x);
if (ret2)
return ret2;
return NULL;
}
当然最后两句代码也可以合并~
BTNode* BTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
{
return root;
}
BTNode* ret1 = BTreeFind(root->left, x);
if(ret1)
return ret1;
return BTreeFind(root->right, x);
}
递归展开图如下:
销毁二叉树
销毁二叉树的本质就是销毁一个个节点,还是需要遍历的;那采用哪种遍历方式呢?
最佳方式是后序遍历销毁,因为如果采用前序遍历,先销毁根的话,还得找左子树和右子树,就得在销毁根之前先保存左右子节点地址,比较麻烦~
ps: root在函数内部置空毫无意义(形参的改变不会影响实参),可以在主函数中手动置空~
//销毁二叉树
void BTreeDestroy(BTNode* root)
{
//空树不需要销毁
if (root == NULL)
{
return;
}
//销毁左树
BTreeDestroy(root->left);
//销毁右树
BTreeDestroy(root->right);
//销毁根
free(root);
}
大家可以自行画一下递归展开图或者在原二叉树图上操作一遍~
二叉树的相关概念及代码实现就介绍到这了,二叉树的精髓就是递归,因此大家重点感悟递归的精妙之处,体会如何大事化小,小事化了,并且要注意二叉树中诸多终止条件都是空树~
欢迎大家交流指正~