上期我们学习了二叉树的堆,这期让我们来看看二叉树的结构,以及如何实现。
在这之前先补充上期没有说到的二叉树的链式存储:
链式存储二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是 链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所 在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面 学到高阶数据结构如红黑树等会用到三叉链。
二叉树链式结构的实现:
在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在大家对二 叉树结构掌握还不够深入,此处手动快速创建一棵简单的二叉树,快速进入二叉树 操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。
下面用代码的方式展示:假设我们要创建的是如图二叉树
我们可以用以下代码实现:
//手动创造出一颗二叉树 BTNode* BinaryTreeCreate() { BTNode* n1 = (BTNode*)malloc(sizeof(BTNode)); assert(n1); BTNode* n2 = (BTNode*)malloc(sizeof(BTNode)); assert(n2); BTNode* n3 = (BTNode*)malloc(sizeof(BTNode)); assert(n3); BTNode* n4 = (BTNode*)malloc(sizeof(BTNode)); assert(n4); BTNode* n5 = (BTNode*)malloc(sizeof(BTNode)); assert(n5); BTNode* n6 = (BTNode*)malloc(sizeof(BTNode)); assert(n6); //给节点赋值 n1->data = 1; n2->data = 2; n3->data = 3; n4->data = 4; n5->data = 5; n6->data = 6; //建立链接关系 n1->left = n2; n1->right = n4; n2->left = n3; n2->right = NULL; n3->left = NULL; n3->right = NULL; n4->left = n5; n4->right = n6; n5->left = NULL; n5->right = NULL; n6->left = NULL; n6->right = NULL; return n1; }
二叉树的遍历:(前序、中序以及后序遍历)
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉 树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历 是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为 根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
我们先看看这三种遍历的结果:
上图中一个矩形代表一棵树,通过其打印的前后顺序就可以看出来递归的特点。
二叉树的遍历我们要使用递归的方式去遍历下面是递归的代码:
// 二叉树前序遍历 void BinaryTreePrevOrder(BTNode* root)///根 左子树 右子树 { if (root == NULL) { printf("NULL "); return; } printf("%d ", root->data); BinaryTreePrevOrder(root->left); BinaryTreePrevOrder(root->right); } // 二叉树中序遍历 void BinaryTreeInOrder(BTNode* root)//左子树 根 右子树 { if (root == NULL) { printf("NULL "); return; } BinaryTreeInOrder(root->left); printf("%d ", root->data); BinaryTreeInOrder(root->right); } // 二叉树后序遍历 void BinaryTreePostOrder(BTNode* root)//左子树 右子树 根 { if (root == NULL) { printf("NULL "); return; } BinaryTreePostOrder(root->left); BinaryTreePostOrder(root->right); printf("%d ", root->data); }
下面我就通过画函数栈帧的图来说明这是如何遍历的:(这里以前序遍历为例)
二叉树的其他接口:
对于这些接口,我们的整体的思路是分而治之,就是大事化小,小事化了。
一般我们都可以画图解决,先想好每一步该怎么走,怎么分配到子树。对于这种问题,我个人认为,从最后一层开始思考是比较好找到解决方案的,因为最后一层最接近底层,就可以不断往上走,根据所需去解决。
// 二叉树节点个数 int BinaryTreeSize(BTNode* root) { if (root == NULL) { return 0; } //返回左边的和右边的还有自己 return BinaryTreeSize(root->left) + BinaryTreeSize(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); } // 二叉树第k层节点个数 int BinaryTreeLevelKSize(BTNode* root, int k) { assert(k > 0); if (root == NULL) { return 0; } if (k == 1) { return 1; } return BinaryTreeLevelKSize(root->left, k-1) + BinaryTreeLevelKSize(root->right, k-1); } // 二叉树查找值为x的节点 BTNode* BinaryTreeFind(BTNode* root, BTDataType x) { if (root == NULL) { return NULL; } if (root->data == x) { return root; } //看根的左子树有没有 BTNode* left = BinaryTreeFind(root->left, x); if (left) return left; //看根的右子树有没有 BTNode* right = BinaryTreeFind(root->right, x); if (right) return right; //都没有 return NULL; } //二叉树的高度 int TreeHeight(BTNode* root) { //左子树和右子树的和加根自己也就是加1就是结果 if (root == NULL) { return 0; } int leftHeight = TreeHeight(root->left); int rightHeight = TreeHeight(root->right); return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1; } // 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树 BTNode* BinaryTreeCreateByPrev(BTDataType* a, int* pi) { //遵守根左子树右子树的规则,从最后一层开始链接起来 if (a[*pi] == '#') { (*pi)++; return NULL; } BTNode* root = (BTNode*)malloc(sizeof(BTNode)); if (root == NULL) { perror("malloc fail"); return NULL; } root->data = a[(*pi)++]; //链接根的左右 root->left = BinaryTreeCreateByPrev(a, pi); root->right = BinaryTreeCreateByPrev(a, pi); return root; } //二叉树的销毁 void BinaryTreeDestory(BTNode* root) { //递归到最后一个再不断向上销毁 if (root == NULL) { return; } BinaryTreeDestory(root->left); BinaryTreeDestory(root->right); free(root); }
不用递归的两个接口:
一个是层序遍历,另一个是判断一棵树是否为满二叉树。
首先先介绍什么是层序遍历:
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在 层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层 上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。通过以上方式打印出来的就是层序遍历,那我们该如何实现呢?
很容易看出来这里用递归的方式不合适,因为递归是要递归到最后的,而这里的打印就不能递归到最后解决,这里可以利用队列的性质,根入队列后就记录打印一下,出队列的时候如该根的左右子树,这样最后的顺序就是正确的,让我们看看代码的实现方式:
// 层序遍历 void BinaryTreeLevelOrder(BTNode* root) { //利用队列的性质,先将根的左右子树进队,然后每次出队的时候都进出队的那个元素的两个子树 Qe q; QueueInit(&q); //先讲根进队列 QueuePush(&q,root); while (!QueueEmpty(&q)) { BTNode* front = QueueFront(&q); printf("%d ", front->data); QueuePop(&q); //出一个根进根的左右子树 if (front->left) { QueuePush(&q, front->left); } if (front->right) { QueuePush(&q, front->right); } } }
接下来就是和层序遍历的思想很像的判断一棵树是否为完全二叉树:主要思路也是利用队列来实现,和层序遍历相似,把全部元素入队列,如果遇到空指针,并且空指针的后面还有其他元素则说明这不是完全二叉树,否则就是完全二叉树,下面我们用代码来实现:
// 判断二叉树是否是完全二叉树 int BinaryTreeComplete(BTNode* root) { //利用层序遍历的思想,如果最后出到空之后的元素中还存在非空元素,就不是完全二叉树 Qe q; QueueInit(&q); //先讲根进队列 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 != NULL) { QueueDestroy(&q); return false; } } QueueDestroy(&q); return true; }
最后在出函数前不要忘记销毁队列,防止内存泄漏。
到这里二叉树的学习就暂时到一段落了,在学习二叉树的时候,尤其是递归,如果不会一定要画图分析,因为不可能有正确的代码给你画函数栈帧。