前面的文章的内容是堆,它的结构基础是完全二叉树,那么假如我们现在给的是随意的一棵二叉树呢,结构随意,那么再用数组存储就有些位置是空的,会被浪费掉,这就有些不礼貌了,所以我们引出今天要谈的链式二叉树。
ps:(这里脱离一下主题:我们现在谈的是普通二叉树,作用不大,因为如果你单纯用来存储数据,它的结构太复杂,完全不如顺序表和链表,所以我们在研究二叉树时,不再研究它的增删查改,而是赋予它一个新的特点:搜索二叉树,这棵树的特点就是,它的所有子树都满足左子树的值小于右子树的值,这时它的价值就体现在搜索上了,非常适合查找数据。)
目录
二叉树的遍历
每棵树我们都可以按照根,左子树,右子树把它拆解成三个部分,根据三者访问的顺序我们分成前序遍历,中序遍历,后序遍历三种,外加一个层序遍历(顾名思义就是按照层进行遍历)。首先看前序遍历:
前序遍历(顺序:根 左子树 右子树)
以上图为例,按照规则,它的顺序应该是:
1 2 3 NULL NULL NULL 4 5 NULL NULL 6 NULL NULL
注意这里一定要严格按照顺序,我们将上述遍历结果画图理解一下:
通过图我们可以更直观的发现这里由于根,左右子树都是相对的,所以出现递归现象,所以在代码实现上主要是递归思想
下面我们就可以代码实现一下了,首先给出二叉树结构
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");
return NULL;
}
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
BTNode* CreatTree()
{
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 PreOrder(BTNode* root) {
if (root == NULL) {
printf("NULL ");
return;
}
printf("%d ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
这里的代码由于使用了递归思想所以代码其实很简洁,我们重点通过代码理解其含义
我们通过画递归展开图理解其含义
(ps:作者不才,这里的图画的些许抽象,色彩搭配也有些辣眼,希望可以帮助读者理解,顺序已通过箭头和序号的方式标好)
代码运行起来打印结果发现没有问题,这就是前序遍历。
中序遍历(顺序:左子树 根 右子树)
有了上面的前序遍历代码的基础,我们直接给出中序遍历的代码:
void InOrder(BTNode* root) {
if (root == NULL) {
printf("NULL ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
这里就不再赘述递归展开图了,不明白的老铁可自行画一下递归展开图帮助理解。
后序遍历(顺序: 左子树 右子树 根)
后序遍历代码如下:
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%d ", root->data);
}
这里再给出中序遍历和后序遍历的测试结果,验证代码的正确性。
求这棵树结点个数
注意这里可能有人会在TreeSize这个函数中定义一个局部变量size统计结点个数,这样做是错误的,因为函数每递归一次都会产生一个size变量,但是函数结束就会销毁,所以,不可能统计到总数,所以,这里需要用全局变量size
int size = 0;
int TreeSize(BTNode* root)
{
if (root == NULL)
{
return;
}
++size;
TreeSize(root->left);
TreeSize(root->right);
}
这里计算size依然是运用了前序遍历的思想
但回头看这里,仔细思考,你会发现这里的全局变量是很危险的,比如本来这里的调用TreeSize计算的结果是6,但是如果你调用两次,结果就成了12了,因为size不会重置,所以为了避免出现这样的问题,需要你每调用一次,重置size为0
所以这里还有一种写法是直接再加一个参数,用来计算个数,当然需要传一个指针,因为为了避免一个问题就是:形参的改变不改变实参
int TreeSize(BTNode* root,int* psize)
{
if (root == NULL)
{
return;
}
++*psize;
TreeSize(root->left,psize);
TreeSize(root->right,psize);
}
这里再提供第三种解决方法:采用分治思想,就是我要统计这个树的结点个数,那么我可以统计子树下的结点个数,子树下的结点个数又可以继续统计子树的子树的结点个数,一直往下走,直到NULL,其实就是递归带返回值,这样处理你会发现代码十分简洁,这也是递归的特点。
int TreeSize(BTNode* root)
{
return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}
求这棵树的高度
我们依然采用递归的思想,要求这棵树的高度,可以先求出它的左右子树中较高的那棵树的高度,当前树的高度等于它左右子树中高度大的那棵加一
int TreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
int leftHeight = TreeHeight(root->left);
int rightHeight = TreeHeight(root->right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
求第k层的结点个数
思路:根的第k层个数=左子树的第k-1层个数+=右子树的第k-1层个数
int TreeKLevel(BTNode* root, int k)
{
assert(k>0);
if (root == NULL)
return 0;
if (k == 1)//k==1时,没有必要继续下面各层的计算了,无关了
return 1;
int leftK = TreeKLevel(root->left, k - 1);
int rightK = TreeKLevel(root->right, k - 1);
return leftK + rightK;
}
查找值为x的结点
这里依然是先给出代码,然后画递归展开图
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BTNode* lret = BinaryTreeFind(root->left, x);
if (lret)
return lret;
BTNode* rret = BinaryTreeFind(root->right, x);
if (rret)
return rret;
return NULL;
}
棕色为调用函数,绿色为返回函数
层序遍历
层序遍历是按层进行遍历,这里用队列很简单就实现了,还是上图中的那棵树,我们建一个队列,让根1入队,出队时,将它的孩子带入,即2 4,按照队列先进先出原则,此时让2出队,同时将2的孩子带入,此时队中数据为4 3,让4出队并把它的孩子带入,此时队中数据为3 5 6,依然是上面的思路,但因为它们的孩子都是NULL,所以直接出,最后结果就是1 2 4 3 5 6.
关键在于出一层,带入下一层
由于这里我们需要队列,所以我们直接将之前写好的队列导入到现在的文件中,由于这里队列中的数据传入的是结点,所以我们需要将之前定义的DataType修改成这里的结构体指针,指针指向的是结点
typedef struct BinaryTreeNode* QDataType;
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(front);
printf("%d ", front->data);
if (front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
QueueDestroy(&q);
}
判断一棵树是否是完全二叉树
利用层序遍历,但是需要NULL也进队列,这样通过观察NULL的位置,如果第一个NULL后面全是NULL则为完全二叉树,否则就是普通二叉树
完全二叉树按层序遍历,非空结点一定连续 。
//判断一棵树是否是完全二叉树
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)
{
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
二叉树的销毁
销毁最好使用后序遍历,因为假设你使用前序遍历,先处理根,那么还得先保存左右指针,否则会找不到左右子树
//二叉树的销毁
void TreeDestroy(BTNode* root)
{
if (root == NULL)
return;
TreeDestroy(root->left);
TreeDestroy(root->right);
free(root);
}
到这二叉树就基本结束了,下一篇文章是关于二叉树的练习,我们下一篇见~