二叉树的各种操作补充
- 求二叉树的结点数
- 求二叉树的叶结点数
- 求二叉树的高度
- 求二叉树的第k层结点数
- 查找指定结点
- 层序遍历
- 判断二叉树是否是完全二叉树
我们任然沿用二叉树的基本信息:
typedef char BTDataType;
typedef struct BinaryTreeNode {
BTDataType _data;
struct BinaryTreeNode* _left;
struct BinaryTreeNode* _right;
} BTNode;
求二叉树的结点数
我们可以在遍历二叉树的同时顺便统计结点个数。
- 遍历+全局变量记录
所有递归生成的函数都能调用通用的计数变量来记录。
// 遍历计数
int size = 0;
void BTreeSize(BTNode* root)
{
if (root == NULL)
return;
++size;
BTreeSize(root->_left);
BTreeSize(root->_right);
}
这么做有它的局限性:
- 全局变量所有函数都能调用,因此只能在约定上约束全局变量的使用。
- 在现实中有时会将函数定义和主函数(或其他调用定义的函数的函数)分离,此时需要通过关键字extern对外部全局变量进行声明。
我们除了局部变量,还可以通过上传局部变量的地址来记录。但除了这些我们还有更好的办法。
- 遍历+搜索
我们直接让函数带一个返回值,这样可以免去全局变量的局限性。这里用到分治的思想。
二叉树的很多操作都用到了分治的思想。
分治即分而治之。举个例子:校长统计人数,会问离自己最近的单位也就是下一级的单位,下一级的单位再问自己下一级的单位,如此往下划分,直到最低一级的单位报到等着被统计即可。
所以分治也可以理解为求助”别人“。但分治要注意:若只根据结果进行判断但不记录结果,则算法的时间复杂度会进一步增加。例如后面的求树高。
// 二叉树节点个数
//搜索
int BinaryTreeSize(BTNode* root) {
if (root == NULL) {
return 0;
}
//每个子树负责上传自己的子结点数但不负责保存
return BinaryTreeSize(root->_left) + BinaryTreeSize(root->_right) + 1;
}
这个函数还能进一步简化:
int BinaryTreeSize(BTNode* root) {
return root == NULL ? 0 : BTreeSizeRecursion(root->_left)
+ BTreeSizeRecursion(root->_right) + 1;
}
求二叉树的叶结点数
叶结点没有子树,因此在递归遍历到叶结点时没必要继续递归下去,直接将这个叶结点上传统计。
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root) {
if (root == NULL) {
return 0;
}
if (root->_left == NULL && root->_right == NULL)//无度结点即为叶结点
return 1;
//每个子树分别记录自己的左、右子树的叶结点数
return BinaryTreeLeafSize(root->_left) + BinaryTreeLeafSize(root->_right);
}
因为二叉树在统计结点数时几乎不会二次遍历曾经遍历过的结点,所以记录子树结点数就没有很大的必要。
求二叉树的高度
还是前序遍历。但是对当前结点的处理是在子树的基础上加上自己的一份。
// 求二叉树的高度简化(但不知道哪边的树高)
int BTreeHeight(BTNode* root) {
if (root == NULL)
return 0;
return BTreeHeight(root->_left) > BTreeHeight(root->_right)
? BTreeHeight(root->_left) + 1 :
BTreeHeight(root->_right) + 1;
}
这个代码就有一个很严重的问题:重复遍历。例如表达式中存在两次BTreeHeight(root->left)
即存在两次遍历左子树的问题。
比如这个OJ题:104. 二叉树的最大深度 - 力扣(LeetCode) ,用这个代码上去提交铁定超时。
解决方法是剔除不必要的重复遍历。此时我们可以记录每个子树的子树高度,然后将自己的子树高度中较大者返回给父结点。最后根结点返回的树高即为整个树的最大树高。
// 求二叉树的高度(规定一个结点的二叉树树高为1)
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;
}
这种处理方式还有另外的称呼:记忆化搜索。以后有机会再细谈。
求二叉树的第k层结点数
没错,还是前序遍历。当判断出当前结点即为第k层结点时,即可直接返回并统计。
这和统计叶结点一样都是统计具有某种特征的结点数。
// 二叉树第k层结点个数
int BTreeLevelKSize(BTNode* root, int k) {
if (root == NULL)
return 0;
if (k <= 1)//当确认当前结点为第k层的结点时直接返回并计数。
return 1;
return BTreeLevelKSize(root->_left, k - 1)
+ BTreeLevelKSize(root->_right, k - 1);
}
查找指定结点
在前序遍历树root的过程中,若发现结点的数据是我们要找的x时将结点返回。
BTNode* BinaryTreeFind(BTNode* root, BTDataType x) {
if (root == NULL)
return NULL;
if (root->_data == x)
return root;
BTNode* ret1 = BinaryTreeFind(root->_left, x);
if (ret1)
return ret1;//需要思考递归时如何将结果带回主函数
BTNode* ret2 = BinaryTreeFind(root->_right, x);
if (ret2)
return ret2;
return NULL;
}
关于查找二叉树中的指定结点,有很大的学问。比如二叉搜索树(一种左子树根结点的数据小于父结点,右子树根结点的数据大于父结点的二叉树),以后有机会的话再整理。
层序遍历
比如对于这个树:
我们想得到这样的输出结果:1 2 4 3 5 6
。
想要得到这种遍历结果,我们可以通过队列实现:
- 根结点1入队。
- 处理队首结点。
- 队首结点的子结点入队。如果结点存在的话。
- 弹出队首。
- 重复2到4,直到队列为空。
队列用到了这里的。
参考程序:
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root) {
Queue q;
QueueInit(&q);
QueuePush(&q, root);//根结点入队
while (QueueSize(&q)) {
BTNode* t = QueueFront(&q);
printf("%c ", t->_data);
//子结点入队
if (t->_left)
QueuePush(&q, t->_left);
if (t->_right)
QueuePush(&q, t->_right);
QueuePop(&q);
}
}
这种层序遍历有一种固定的称呼:广度优先搜索(Breadth First Search,简称BFS )。这是个很重要的算法,在以后会总结这个算法的应用。
判断二叉树是否是完全二叉树
完全二叉树和满二叉树上对应的结点一一对应。所以可以在层序遍历的基础上进行改造:空结点也入队。层序遍历枚举到空结点时停止遍历,最后检查队列内还剩的结点是否全是空结点,不是的话则该二叉树不是完全二叉树。
参考程序:
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root) {
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
// 遇到空就跳出
if (front == NULL)
break;
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;
}