树是一种数据结构,其表现形式有很多种,可以是顺序表,也可以是链表。然而树中应用最多的那便是二叉树了,下面我就来总结一下二叉树。
一. 二叉树-堆
首先是二叉树-堆。堆是一种完全二叉树,分为大堆和小堆,大堆是父节点都大于子节点的二叉树,小堆是父节点小于子节点的二叉树。建堆的过程我们需要用向上调整法。以下我们都以大堆为例:
void AdjustUp(int* a,int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parent])
swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
}
这就是向上调整的过程,其中父节点的下标(parent)一定是(chlid-1)/ 2。相反,左子节点的下标是parent*2+1,右节点的下标是parent*2+2,这个会在下下面用到。
下面是建堆的结果:
前一行是一个无序数组,后一行是建成的大堆。当然,遍历建堆的过程我就省略了,主要是介绍向上调整法。显然我们这样就已经建成了一个大堆。
建堆看起来很容易,那么删除堆元素呢?为了考虑堆的实用性,我们删除堆的元素,一般都是删除堆顶元素。所以我们就要采用向下调整法。其思路与向上调整法差不多,只不过是将最后一个元素与第一个元素调换,再将数组长度-1,然后堆顶元素向下调整。核心代码如下:
void AdjustDwon(int* a, int n, int root)
{
int child = root * 2 + 1;
while (child < n)
{
if (a[child] < a[child + 1]&&child+1<n)
child++;
if (a[root] < a[child])
swap(&a[child], &a[root]);
root = child;
child = root * 2 + 1;
}
}
这就可以实现删除堆顶元素了。
其实掌握了这两个算法,就可以实现堆排序了。但是心急吃不了热豆腐,我们放在排序讲解。
二. 链式二叉树
由上面的图片可以看出,二叉树用链式表示会更加形象,在用链来连接二叉树的各个节点之后,我们可以灵活的解一下二叉树的题,以及对于遍历二叉树有着不错的效果。
我们定义,root是头节点,left是左子节点,right是右子节点。接下来我们都会采用这种形式进行讲解。
// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
上面是二叉树的一些基本问题。我们逐一来看。
1.前序,中序,后序
讲到链式二叉树就不得不讲二叉树的前序中序后序遍历了。前序遍历的顺序是,先root再left后right,中序是先left再root后right,后序是先left再right后root。其实可以总结为访问根的顺序不同。
接下来我们有一个二叉树,我们来看看三种遍历的结果。
假设我们的二叉树是“ABD##E#H##CF##G##”,其中“#”代表NULL;
前中后序遍历的结果就是这三行。但是如何实现呢,在二叉树中,由于连接性很强,树与树的结构也相似,所以我们通常用递归实现,代码也不复杂。如下:
// 二叉树前序遍历
void BinaryTreePrevOrder(BTNode* root)
{
if (root == NULL)
return;
printf("%c ", root->data);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
if (root == NULL)
return;
BinaryTreeInOrder(root->left);
printf("%c ", root->data);
BinaryTreeInOrder(root->right);
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
if (root == NULL)
return;
BinaryTreePostOrder(root->left);
BinaryTreePostOrder(root->right);
printf("%c ", root->data);
}
这就是三种遍历,代码看着虽然简短,但是其内部的递归还需大家仔细理解。
2.遍历数组构建二叉树
第一问是让我们通过前序遍历来创造二叉树,我们就用递归来实现遍历数组创造二叉树。
BTNode* BinaryTreeCreate(BTDataType* a, int* pi)
{
if (a[*pi])
{
if (a[*pi] == '#')
{
++(*pi);
return NULL;
}
else
{
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
root->data = a[*pi];
++(*pi);
root->left = BinaryTreeCreate(a, pi);
root->right = BinaryTreeCreate(a, pi);
return root;
}
}
return NULL;
}
这就是遍历过程,如果有些晦涩难懂的话,可以画一画递归图,这样就会很好理解。大家也可以尝试中序,后序遍历的代码,这里我就不做过多讲解。
3.oj题
可以看到,在上面的列表里面,有三个oj题,这里我就对第一个进行分析,有兴趣的小伙伴可以试试第二个第三个,都不难,主要是锻炼我们的递归理解。
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
题目要求我们求二叉树的节点个数,那我们想,是不是可以通过前序遍历,当他遇到了非空节点,我们返回1,遇到空节点,我们就返回0,再通过递归层层返回相加,是不是就可以得到总结点个数了。思路构建好了,我们就可以实战看看代码。
int BinaryTreeSize(BTNode* root)
{
if (root == NULL)
return 0;
return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}
这就是这道题的实现,我们看看结果,还是以上面的树为例:
可以见得并没有错。
4.层序遍历
为什么把层序遍历放到最后来讲呢,其实层序遍历用的是队列来实现的,并不是链式二叉树,这是因为它也是二叉树的一种遍历形式。所以放在了一起。
这个我们简单看看,思路是,每当二叉树的节点出队列实,都会带入两个子节点。这样我们出的数据的排序,就是二叉树的层序遍历。代码如下:
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if(root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* ele= QueueFront(&q);
printf("%c ", ele->data);
if(ele->left)
QueuePush(&q,ele->left);
if(ele->right)
QueuePush(&q,ele->right);
QueuePop(&q);
}
QueueDestroy(&q);
}
可以看到层序遍历与之前三个都不一样,这里没有链式,也就不存在递归一说。其中QueueInit等函数都是队列中的函数,知道是什么作用即可。
当然,二叉树的销毁没有讲,但是大家应该很快可以想到,可以用后序遍历来销毁,因为头节点必须是最后销毁的,不然就找不到子树了。
三:总结
二叉树其实并不难,只要理解了递归结构,那就大差不差了,此外一些选择题的方法,大家可以找找自己学校的卷子,看看解题方法即可。