文章目录
二叉树
二叉树即为树的度不大于2的树。换言之,每个结点最多有两个子结点。
还可以发现,二叉树叶子结点的个数=度为2结点的个数+1.
原因是设二叉树度为0,1,2的结点个数为N0、N1、N2,总结点个数为N,则有N-1条边。
则N0+N1+N2-1=N1+2*N2,即N0=N2+1.
- 由此我们得出,二叉树是递归定义的,这是因为二叉树的每个结点都包含最多两个子结点,而每个子结点都可以看成一颗二叉树也就是子树。这样我们可以通过不断地递归定义每个子结点,进而构建出整个二叉树。
- 那么递归的好处就是在逻辑清晰的前提下,能够将一个复杂的问题分解成多个相同或相似的子问题,而且每个子问题的解决方法也可以采用相同的递归定义。在后面提到的链式二叉树各种实现中也是由递归解决的。
根据前言,我们可以写出二叉树结点的结构体:
typedef char BTDataType;
typedef struct BinaryTreeNode
{
BTDataType _data;
struct BinaryTreeNode* _left;
struct BinaryTreeNode* _right;
}BTNode;
二叉树的链式结构
二叉树的遍历
二叉树前序遍历、中序遍历和后序遍历
由上述可知,我们将每个二叉树都看成,根结点、左子树和右子树。
那么二叉树的前序遍历就是指,访问根结点的顺序在左右子树前面,也就是按照根结点、左子树和右子树的顺序读取二叉树。
如下:
void BinaryTreePrevOrder(BTNode* root)
{
if (!root)
{
printf("N ");
return;
}
printf("%c ", root->_data);
BinaryTreePrevOrder(root->_left);
BinaryTreePrevOrder(root->_right);
}
可以看出,上述代码中我们利用了二叉树是递归定义的这个特性来实现前序遍历。
具体流程是将前序遍历拆成,遍历根结点再遍历左子树和右子树,如果遇到NULL就返回。
举一反三,我们不难实现二叉树的中序遍历和后序遍历:
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)
{
if (!root)
{
printf("N ");
return;
}
BinaryTreeInOrder(root->_left);
printf("%c ", root->_data);
BinaryTreeInOrder(root->_right);
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)
{
if (!root)
{
printf("N ");
return;
}
BinaryTreePostOrder(root->_left);
BinaryTreePostOrder(root->_right);
printf("%c ", root->_data);
}
可以看到,二叉树的中序遍历是指根结点在左右子树中间访问,而后序遍历则是指根结点在左右子树之后访问,
层序遍历
二叉树的前中后序遍历都是能通过较为简单的递归实现的,但层序遍历的话略显复杂。如果还是按照上述代码的思路那么就时间复杂度较高。
根据二叉树的下标性质,我们不妨创建一个数组来存储二叉树的结点,在打印出数组的元素。
void CopyTree(BTNode* root, char* Tree,int i,int size)
{
if (!root&&i<size)
{
Tree[i] = 'N';
return;
}
if (i<size)
{
Tree[i] = root->_data;
CopyTree(root->_left, Tree, 2 * i + 1, size);
CopyTree(root->_right, Tree, 2 * i + 2, size);
}
}
void BinaryTreeLevelOrder(BTNode* root)
{
int depth = maxDepth(root);
int size = 1;
while (depth)
{
size *= 2;
depth--;
}
size--;
char* Tree = (char*)malloc(size * sizeof(char));
CopyTree(root, Tree, 0,size);
for (int i = 0; i < size; i++)
{
printf("%c ", Tree[i]);
}
free(Tree);
Tree = NULL;
}
首先第一个函数,递归地将二叉树的节点数据复制到一个字符数组中,按照完全二叉树的顺序存储。如果二叉树为空或者已经超出数组大小,将字符数组对应位置标记为’N’。否则,将当前节点的数据存储在数组中,然后分别递归处理左子树和右子树。
然后在第二个函数中传入root、size等数据,最后顺序打印数组中的数据并释放内存。
整体流程是根据二叉树的层序遍历顺序将数据存储在字符数组中,并按顺序打印出来。
那么分析上述流程我们可以知道这个代码的时间复杂度和空间复杂度都为O(N)。
- 那么除了这种方式外还有无别的方式实现二叉树的层序遍历呢?答案是有的。
观察层序遍历的打印顺序可知,我们想要二叉树的左结点打印完后就立刻打印右结点。这种打印方式让我们不禁联想到队列。
- 我们可以创建一个队列,用于存储待访问的节点。
- 将根节点入队列。
- 当队列不为空时,将队头结点的左右子树入队列,再队头出队列。
这样是不是就巧妙地实现了二叉树的层序遍历了呢?
void BinaryTreeLevelOrder(BTNode* root)
{
Queue qu;
BTNode * cur;
QueueInit(&qu);
QueuePush(&qu, root);
while (!QueueIsEmpty(&qu))
{
cur = QueueTop(&qu);
putchar(cur->_data);
if (cur->_left)
{
QueuePush(&qu, cur->_left);
}
if (cur->_right)
{
QueuePush(&qu, cur->_right);
}
QueuePop(&qu);
}
QueueDestory(&qu);
}
二叉树的构建
我们这里提到的二叉树构建是指根据前中后序遍历的序列来构建二叉树。
由上述代码可知三序遍历大同小异,那么我们这里只提根据二叉树的前序遍历序列来构建二叉树。
如下:
BTNode* BuyNode(BTDataType x)
{
BTNode* tmp = (BTNode*)malloc(sizeof(BTNode));
if (!tmp)
{
perror("malloc fail!");
exit(-1);
}
tmp->_data = x;
tmp->_left = tmp->_right = NULL;
return tmp;
}
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
if (n == 0)
return NULL;
if (a[*pi] == '#')
{
(*pi)++;
return NULL;
}
BTNode* p = BuyNode(a[*pi]);
int x = *pi;
(*pi)++;
p->_left = BinaryTreeCreate(a, n+x-*pi, pi);
p->_right = BinaryTreeCreate(a, n+x-*pi, pi);
return p;
}
由此,我们就可以根据二叉树的前序遍历序列来构建二叉树。
数值计算
二叉树节点个数
根据递归的思想,我们可以将二叉树的结点个数分为求根结点个数和左右子树结点个数。
- 当根结点为NULL时,二叉树结点个数为0.
- 当根结点不为NULL时,二叉树结点个数为1+左右子树结点个数。
综上:
int BinaryTreeSize(BTNode* root)
{
if (!root)
return 0;
return 1 + BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right);
}
二叉树叶子结点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (!root)
return 0;
if (root->_left == NULL && root->_right == NULL)
return 1;
return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);
}
二叉树第k层结点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
assert(k > 0);
if (!root)
return 0;
if (k == 1)
return 1;
return BinaryTreeLevelKSize(root->_left, k - 1) + BinaryTreeLevelKSize(root->_right, k - 1);
}
二叉树的高度
判断二叉树的高度时,我们也是用上述思想来分治子问题:
- 当根结点为NULL时,高度为0.
- 当根结点不为NULL时,高度为1+max{左右子树高度}。
那么代码是否为这样呢
int BinaryTreeDepth(BTNode* root)
{
if(!root)
return 0;
return BinaryTreeDepth(root->_left)>BinaryTreeDepth(root->_right)?
BinaryTreeDepth(root->_left)+1:BinaryTreeDepth(root->_right)+1;
}
咋一看上述代码思路非常清晰正确,实际上也是没什么错误的地方。
但是呢这个函数的递归调用次数太多了,每次得到的左右子树高度不加以记录而是再度递归得到。长期以往该时间复杂度将是以指数爆炸增长。
那么正确的做法是记录好每一次得到的左右子树高度:
int BinaryTreeDepth(BTNode* root)
{
if(!root)
return 0;
int left=BinaryTreeDepth(root->_left)+1;
int right=BinaryTreeDepth(root->_right)+1;
return left>right?left:right;
}
查找指定的值
与上述记录数据类似的思想,查找指定的值我们也需要记录下来,才能跳出重重递归:
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (!root)
return NULL;
if (root->_data == x)
return root;
BTNode* p = BinaryTreeFind(root->_left, x);
if (p)
return p;
p = BinaryTreeFind(root->_right, x);
if (p)
return p;
return NULL;
}
判断二叉树是否为满二叉树
int BinaryTreeFull(BTNode* root)
{
if (!root)
return 0;
int leftnum = BinaryTreeSize(root->_left);
int rightnum = BinaryTreeSize(root->_right);
if (leftnum == rightnum && ((leftnum + rightnum + 2) ^ (leftnum + rightnum + 1) == 0))
return 1;
return 0;
}
销毁二叉树
void BinaryTreeDestory(BTNode** root)
{
if (!*root)
{
return;
}
BinaryTreeDestory(&(*root)->_left);
BinaryTreeDestory(&(*root)->_right);
if((*root)->_right==NULL&&(*root)->_left==NULL)
{
free(*root);
}
}
那么以上代码就是实现链式二叉树各种基本功能的代码,希望对你的学习有所帮助。