无论你觉得自己多么的了不起,也永远有人比你更强。💓💓💓
目录
✨说在前面
亲爱的读者们大家好!💖💖💖,我们又见面了,经过上一篇章中“堆”的学习,大家已经了解了树的基本结构。但是,堆只是树中特殊的一种数据结构,并且是基于树的一种数据结构。
今天我们将要学习它更加抽象的一面,即二叉树,那什么是二叉树,他们又是用什么来实现,又有什么作用呢?我们今天就详细剖析二叉树这一的数据结构吧~
在上一篇文章中,为了引入堆,也很清晰地介绍了树和二叉树的概念和性质,忘记的小伙伴可以去看看。
👇👇👇
💘💘💘知识连线时刻(直接点击即可)🎉🎉🎉复习回顾🎉🎉🎉
【数据结构】详解二叉树之堆博主主页传送门:愿天垂怜的博客
🍋知识点一:二叉树的遍历
• 🌰1.创建一棵二叉树
在学习二叉树的基本操作之前,我们首先需要创建一棵二叉树,然后才能学习其相关的基本操作。但是由于现在我们知识有限,先手动创建一个二叉树一遍快速进入二叉树操作的学习,等以后我们再回过头来学习它真正的创建方式。
比如我们要创建如下的一棵二叉树:
代码如下:
#include <stdio.h> #include <stdlib.h> #include <assert.h> //创建二叉树的数据结构 typedef int BTDatatype; typedef struct BinaryTreeNode { BTDatatype data; struct BinaryTreeNode* left; struct BinaryTreeNode* right; }BTNode; //创建二叉树的节点 static BTNode* BuyNode(BTDatatype x) { BTNode* node = (BTNode*)malloc(sizeof(BTNode)); if (node == NULL) { perror("malloc operation failed"); exit(1); } node->data = x; node->left = node->right = NULL; return node; } //手动创建二叉树 BTNode* CreateBinaryTree() { 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; }
注意:上面的代码不是创建二叉树的真正的方式,真正的二叉树是递归定义的,因此后序基本操作
• 🌰2.二叉树的遍历
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历1(Traversal)是按照某种特定的规则,依次堆二叉树中的节点进行相应的操作,使得每个节点只操作依次。访问节点所做的操作依赖与具体的应用问题。
遍历是二叉树上最重要的运算之一,也是二叉树上进行其他运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历;
🔥前序遍历(Prevorder Traversal)——访问根节点的操作发生在遍历其左右子树之前。
🔥中序遍历(Inorder Traversal)——访问根节点的操作发生在遍历其左右子树之中(间)。
🔥后序遍历(Postorder Traversal)——访问根节点的操作发生在遍历其左右子树之后。
由于被访问的节点必然是某子树的根,所以N(Node)、L(Left subtree)、R(Right subtree)又可以解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先跟遍历、中根遍历和后根遍历。
•🔥前序遍历
对于以下这一棵二叉树:
我们如果按照访问根节点的操作发生在遍历其左右子树之前,那么所能得到这样的遍历顺序:1,2,3,4,5,6,这样的遍历称之为前序遍历。对于这样一棵树,我们先访问左子树,再访问根,最后访问右子树,而左子树和右子树中又是按照根——左子树——右子树的顺序遍历,以此往复,直到遍历完成。
代码如下:
void PrevOrder(BTNode* root) { if (root == NULL) { printf("N "); return; } printf("%d ", root->data); PrevOrder(root->left); PrevOrder(root->right); }
上面代码完成了二叉树以前序遍历的方式打印每个节点中的值。.
•🔥中序遍历
对于以下这一棵二叉树:
我们如果按照访问根节点的操作发生在遍历其左右子树之中(间),那么所能得到这样的遍历顺序:3,2,1,5,4,6,这样的遍历称之为中序遍历。对于这样一棵树,我们先访问根,再访问左子树,最后访问右子树,而左子树和右子树中又是按照左子树——根——右子树的顺序遍历,以此往复,直到遍历完成。
代码如下:
void InOrder(BTNode* root) { if (root == NULL) { printf("N "); return; } InOrder(root->left); printf("%d ", root->data); InOrder(root->right); }
•🔥后序遍历
对于以下这一棵二叉树:
我们如果按照访问根节点的操作发生在遍历其左右子树之后,那么所能得到这样的遍历顺序:3,2,5,6,4,1,这样的遍历称之为后序遍历。对于这样一棵树,我们先访问左子树,再访问右子树,最后访问根,而左子树和右子树中又是按照左子树——右子树——根的顺序遍历,以此往复,直到遍历完成。
代码如下:
void PostOrder(BTNode* root) { if (root == NULL) { printf("N "); return; } PostOrder(root->left); PostOrder(root->right); printf("%d ", root->data); }
•🔥层序遍历
除了先序遍历,中序遍历、后序遍历外,还可以对二叉树进行层序遍历,如下面这棵二叉树:
设二叉树的根节点 所在的层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第二层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的节点的过程就是层序遍历。
实现二叉树的层序遍历,我们需要借助另一数据结构——队列。我们需要将根节点入队列,再每删除一个节点时,进行需要的操作,并将它的左右子节点入队列即可。
代码如下:
void TreeLevelOrder(BTNode* root) { Queue q; QueueInit(&q); if (root) 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); } QueueDestroy(&q); }
•🔥给出两种遍历如何求二叉树?
我们经常会碰到以下这种题型:
这些题通常都是给出其中两种遍历序列(或给出层序遍历序列),求节解这棵二叉树或者它的其他序列 。我们以上面第2题为例,假设我们要求整棵二叉树:
前序遍历:E F H I G J K 中序遍历:H F I E J K G
1.前序遍历确定根
即前序遍历的第一个E即为整棵二叉树的根节点。
2.中序遍历分割左右子树
即在中序遍历中找到E,则E左边的H F I为左子树,右边的J K G为右子树。
由此我们得到一下分析:
分析到这个地方,我们可以得到左子树不为空,那么前序遍历的第二个F就是左子树的根,进而可以从中序遍历中找到F,它的左边H就是左子树的左子树,右边I就是左子树的右子树。同理,左子树走完后的第一个为G,则G是右子树的根节点,在中序遍历中找到G,它的左边J K就是右子树的左子树,而右子树的右子树为空。那么J K同理可以分析出J为它的左子树,K是J的右子树,即整棵树为:
🍋知识点二:二叉树基本方法
• 🌰1.二叉树节点个数
我们利用递归的思想,将大问题拆分成许多类似的小问题。二叉树的节点个数等于它左子树的节点个数加上它右子树的节点个数再加1(它自己),而左子树和右子树的节点数同样等于它的左、右子树节点数和它自己,递归到最后节点如果为空,数量为0即可。
int TreeSize(BTNode* root) { return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1; }
• 🌰2.二叉树叶子节点个数
我们的思想还是不变,都是递归的思想。叶子节点指的是度为0的节点,所以我们要检查一个节点它的左右孩子节点是否为空,如果都为空,那么就是叶子节点,返回1;如果不都为空,就递归下去,等于它的左子树和右子树的叶子节点之和;如果本身就是空,那就是0。
int TreeLeafSize(BTNode* root) { if (root == NULL) return 0; return (!root->left && !root->right) ? 1 : TreeLeafSize(root->left) + TreeLeafSize(root->right); }
• 🌰3.二叉树第k层节点个数
第k层的节点个数等于左右子树第k-1层的节点个数之和,同样递归下去,直到k=1,也就是第1层,那就直接返回1。特殊地,如果节点为空,返回0。
int TreeLevelKSize(BTNode* root, int k) { if (root == NULL) return 0; if (k == 1) return 1; return TreeLeafSize(root->left, k - 1) + TreeLeafSize(root->right, k - 1); }
• 🌰4.二叉树的高度
二叉树的高度等于它左右子树高的那一棵子树的高度+1,我们可以记录左右子树的值再进行判断,也可以用fmax函数,如果数为空,返回0。fmax函数在头文件math.h中声明。
int TreeHeight(BTNode* root) { if (root == NULL) return 0; /*int left = TreeHeight(root->left); int right = TreeHeight(root->right); return left > right ? right + 1 : left + 1;*/ return fmax(TreeHeight(root->left), TreeHeight(root->right)) + 1; }
注意:这里使用三目运算符会使性能大大降低,因为每次return都要更多次地计算子树的高度。
• 🌰5.查找值为x的节点
用递归的方式遍历整棵树,如果为空就直接return NULL,不为空判断值是否为x,不为x就继续往下递归,为x就返回。也就是看自己是不是x,不是就返回左子树的查找,左子树也没有就返回右子树的查找。
BTNode* TreeFind(BTNode* root, int x) { if (root == NULL) return NULL; if (root->data == x) return root; BTNode* ret = TreeFind(root->left, x); if (ret) return ret; return TreeFind(root->right, x); }
• 🌰6.判断是否为完全二叉树
这时我们依然需要借助队列来进行判断。我们的思路是:将数中的每一个节点按照层序遍历的方式入队列(空节点也入队列),如果进入队列的节点为空,那么就停止,去判断后面所剩下的节点是否都为空。如果都为空,那就是完全二叉树,如果后面有不为空的节点,就不是完全二叉树。
代码如下:
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; } 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; }
• 🌰7.二叉树的销毁
销毁儿茶俗话就比较简单了,依次释放左右节点再释放根1节点就可以了。
void TreeDestroy(BTNode* root) { if (root == NULL) return; free(root->left); free(root->right); free(root); }
• ✨SumUp结语
到这里本篇文章的内容就结束了,二叉树比我们以往的数据结构更加抽象,相信大家看完本篇文章已经发现了,涉及到的实现方法不断用到了递归的思想。希望大家可以好好复习今天的内容,自己尝试写代码~