目录
一、前言
🍎为何使用链式二叉树
🍐何为链式二叉树
🍉二叉树的构建
💦创建二叉链结构
💦手动构建一颗树
🍓二叉树的遍历 (重点)
💦前序遍历
💦中序遍历
💦后续遍历
🍌二叉树的经典问题(重点)
💦二叉树节点个数
💦二叉树叶子节点的个数
💦二叉树第K层节点个数
💦二叉树的深度
💦二叉树查找值为x的节点
💦二叉树的销毁
🍊总代码和演示图
二、共勉
一、前言
在之前的文章中,已经详细的讲解了二叉树、堆等问题,所以本次博客将继续延续之前的知识点,讲解链式二叉树的遍历和一些经典问题。
🍎为何使用链式二叉树
在前几篇博文中,我们学习的都是完全二叉树或满二叉树,而这两个都是可以用数组来实现的,但是如果不是完全二叉树呢?回顾下曾经学过的知识点:
由上图得知,普通二叉树也可以使用数组来存储,但是会存在大量的空间浪费,而完全二叉树就不会这样,因为其空间利用率是%100的。既然这样,那普通二叉树该如何进行存储呢?答案是使用链式结构进行存储。
🍐何为链式二叉树
- 链式结构分为两种:二叉链和三叉链
💦先看下代码结构:
💦 画图演示:typedef int BTDataType; // 二叉链 struct BinaryTreeNode { struct BinTreeNode* _pLeft; // 指向当前节点左孩子 struct BinTreeNode* _pRight; // 指向当前节点右孩子 BTDataType _data; // 当前节点值域 }; // 三叉链 struct BinaryTreeNode { struct BinTreeNode* _pParent; // 指向当前节点的双亲 struct BinTreeNode* _pLeft; // 指向当前节点左孩子 struct BinTreeNode* _pRight; // 指向当前节点右孩子 BTDataType _data; // 当前节点值域 };
⚠ 注意:链式二叉树和我们之前的学习略有差别。以前我们学习的数据结构无非就是增删查改这些东西,而链式二叉树不太关注这块的增删查改。因为普通二叉树的增删查改没有意义。如下的二叉树:
链式二叉树是要比之前链表啥的更加复杂的,如果只是单纯的让链式二叉树存储数据的话,价值就不大了,不如使用线性表。接下来,我将通过其遍历方式,结点个数……为大家展开讨论。此节内容是为了后续学习更复杂的搜索二叉树打基础,具体是啥后面再谈。
在具体讲解之前,再回顾下二叉树,二叉树是:
- 空树
- 非空:根节点,根节点的左子树、根节点的右子树组成的。
从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。
🍉二叉树的构建
💦创建二叉链结构
创建二叉链结构其实就很easy了,也就是创建一个结构体罢了,这种在先前已经写过很多,咱就是说直接上代码:
// 二叉树结构体 typedef struct BrinyTree { struct BrinyTree* left; struct BrinyTree* right; int data; }BT;
💦手动构建一颗树
其实构建一棵树的思想还是挺简单的,按照图示创建6个节点,并根据图中的样子将节点顺次链接起来
// 创建一个节点 (有返回值,返回值类型为---结构体指针) BT* BuyNode(int x) { BT* newnode = (BT*)malloc(sizeof(BT)); if (newnode == NULL) { perror("malloc fail!"); exit(-1); } newnode->data = x; newnode->left = NULL; newnode->right = NULL; return newnode; } // 创建一个树 BT* CreatBinaryTree() { // 创建6个节点 BT* node1 = BuyNode(1); BT* node2 = BuyNode(2); BT* node3 = BuyNode(3); BT* node4 = BuyNode(4); BT* node5 = BuyNode(5); BT* node6 = BuyNode(6); //将结点连接起来,构成自己想要的树 node1->left = node2; node1->right = node4; node2->left = node3; node4->left = node5; node4->right = node6; return node1; }
🍓二叉树的遍历 (重点)
以一颗二叉树为例:
后续的遍历均是建立在次二叉树基础上展开。
💦前序遍历
- 遍历规则:
前序遍历,也叫先根遍历
遍历顺序:根 -> 左子树 -> 右子树
- 思路:
既然先从根走,根就是1,接下来访问1的左子树,此时又要先访问其左子树的根为2,接着再访问2的左子树->根:3,接着访问其左子树和右子树,不过均为空,递归返回,此时3作为2的左子树访问完毕,访问2的右子树为NULL,再递归返回此时1的左子树就访问完毕了,访问其右子树,同理访问左树4,再访问左树5,接着左右子树NULL,递归返回访问4的右树,……类似的
- 图示:
- 代码演示:
前序遍历的代码非常简洁,短短几行即可操作,先看代码:
// 前序遍历 根 左 右 void PrevOrder(BT* root) { if (root == NULL) { printf("NULL "); // 如果为空,就打印 return; // 当前函数结束 } printf("%d ", root->data); PrevOrder(root->left); PrevOrder(root->right); }
- 效果如下:
跟我们先前画的图一模一样,让我们通过一副递归图来深刻理解其原理:
- 递归图:
💦中序遍历
- 遍历规则:
中序遍历,也叫中根遍历
遍历顺序:左子树 -> 根结点 -> 右子树
- 思路:
根据遍历顺序,我们得知,如若想访问1,得先访问其左子树2,访问2还得先访问其左子树3,类似的,再访问其左子树为NULL,递归返回访问根结点3,再访问右子树NULL,递归返回访问根结点2,再访问右子树NULL,递归返回访问根结点1,再访问其右子树,1的右子树访问规律同1的左子树,这里不过多赘述。
- 画图演示:
- 代码演示:
中序遍历的代码和前序遍历一样,看起来都非常简洁:
// 中序遍历 左 根 右 void InOrder(BT* root) { if (root == NULL) { printf("NULL "); // 如果为空,就打印 return; // 当前函数结束 } InOrder(root->left); printf("%d ", root->data); InOrder(root->right); }
- 效果如下:
单纯看代码看不出啥头绪,还得是画递归图。
💦后续遍历
- 遍历规则:
后续遍历,也叫后根遍历
遍历顺序:左子树 -> 右子树 -> 根结点
- 思路:
要访问1得先访问1的左子树2,继而得先访问2的左子树3,再先访问3的左子树NULL,右子树NULL,根3,递归返回访问2的右子树NULL,根2,再递归返回访问1的右子树……类似的
- 画图演示:
- 代码演示:
// 后序 左 右 根 void PosOrder(BT* root) { if (root == NULL) { printf("NULL "); // 如果为空,就打印 return; // 当前函数结束 } PosOrder(root->left); PosOrder(root->right); printf("%d ", root->data); }
- 效果如下:
- 画图演示递归过程:
🍌二叉树的经典问题(重点)
💦二叉树节点个数
- 思想:
求结点个数,这里我将提供如下几种方法,但不都是可行的,要对比着看,本质都是递归的思想:
- 法一:遍历
在前文中,我们已经学习了如何遍历链式二叉树,现在想求结点的个数,那么只需要随便采用一种遍历方式,并把打印换成计数++来求个数即可,听起来非常容易,先看代码:
//节点个数 void BTreeSize(BTNode* root) { int count = 0; //局部变量 if (root == NULL) //如果为空 return; ++count; BTreeSize(root->left); BTreeSize(root->right); }
难道上述代码能够准确求出结点个数吗?其实不然,根本求不出来。
具体解释起来需要借用栈帧的思想,因为这里用的是递归,而递归是每递归一次在栈帧里头都会开辟一块空间,每一块栈帧都会有一个count,而我希望的是只需要有一个count,然后所有的count均加在一起,可是现在每递归一次,重新开辟一个count,count即局部变量。递归完就销毁,同形参的改变不会影响实参一样,一个道理。所有此法根本就行不通,得换。
- 法二:定义局部静态变量count
在法一中,我们定义的是局部变量count,会导致每递归一次就开辟栈帧,并创建count,每次递归结束返回就销毁栈帧。那如果可以把count放在静态区里头,不久可以保留住count吗
//节点个数 int BTreeSize(BTNode* root) { static int count = 0; //局部静态变量 if (root == NULL) //如果为空 return count; ++count; BTreeSize(root->left); BTreeSize(root->right); return count; }
- 效果如下:
看似好像是成功了,确实结点个数为6,但真的就是成功了吗?当然不是,如果我们现在想多打印几次呢?
什么鬼?怎么size还呈现等差数列递增呢?就是因为这里运用了static关键字,将count扣在静态区,导致多次调用没办法初始化为0,使其每次递归调用累计加加,但是当你再重新调用自己时,count不会重新置为0,会依旧保留为曾经++的结果。局部的静态变量有一个好处,它的生命周期在全局,但是只能在局部去访问。它的初始化为0只有第一次调用会访问,其余均不会。由此可见,局部的静态也是不行的,还得再优化。
- 法三:定义全局变量count
法二的局部静态变量行不通,那就把count设定为全局变量。要知道全局变量是存在静态区的。虽然也在静态区,但是其初始化为0是可以重复访问的。
//节点个数 int count = 0; void BTreeSize(BTNode* root) { if (root == NULL) //如果为空 return; ++count; BTreeSize(root->left); BTreeSize(root->right); }
- 效果如下:
确实可以求出结点个数,并且也不会出现像法二一样的问题。但是其实定义全局变量也会存在一个小问题:线程安全的问题,这个等以后学到Linux再来讨论,我们这边考虑再换一种更优解。
- 法四:最优解
我们这里可以考虑多套一层,可以考虑把变量的地址传过去。这样操作不会存在任何问题,上代码:
//节点个数 void BTreeSize(BTNode* root, int* pCount) { if (root == NULL) //如果为空 return; ++(*pCount); BTreeSize(root->left, pCount); BTreeSize(root->right, pCount); }
确实可以求出结点个数,并且也不会出现像法二一样的问题。但是其实定义全局变量也会存在一个小问题:线程安全的问题,这个等以后学到Linux再来讨论,我们这边考虑再换一种更优解。
- 法五:新思路
直接利用子问题的思想来写,返回当root为空为0,不是就递归左树+右树+1。
- 空树,最小规模子问题,结点个数返回0
- 非空,左子树结点个数+右子树结点个数 + 1(自己)
int BTreeSize(BTNode* root) { return root == NULL ? 0 : BTreeSize(root->left) + BTreeSize(root->right) + 1; }
此法非常巧妙,很灵活的运用了递归的思想,我们通过递归图来深刻理解下:
💦二叉树叶子节点的个数
- 思路1:遍历+计数
在遍历的基础上如果结点的左右子树均为空则count++。但是此题我们依旧采用分治思想。
// 计算叶子节点个数 // 遍历 + 计数 void TreeLeveSize1(BT* root, int* count) { if(root==NULL) { return ; } if(root->left==NULL&&root->right==NULL) { (*count)++; } TreeLeveSize1(root->left,count); TreeLeveSize1(root->right,count); }
- 思路2:遍历+递归
// 叶子节点个数 int TreeLeveSize2(BT*root) { if(root==NULL) { return 0; } int left = TreeLeveSize2(root->left); int right = TreeLeveSize2(root->right); if(left==0&&right==0) { return 1; } return left+right; }
- 思路3:分治思想
首先,如果为空,直接返回0,如若结点的左子树和右子树均为空,则为叶节点,此时返回1,其它的继续分治递归。
- 代码演示:
/ 计算叶子节点个数 int TreeLeveSize(BT* root) { if(root==NULL) { return 0; } if(root->left==NULL&&root->right==NULL) { return 1; } return TreeLeveSize(root->left)+TreeLeveSize(root->right); }
- 递归图:
💦二叉树第K层节点个数
- 思路:
假设K=3
- 空树,返回0
- 非空,且K == 1,返回1
- 非空,且K>1,转换成左子树K-1层节点个数 + 右子树K-1层节点个数
- 代码演示:
// 树的第K层节点个数 // 1. 当前树的第K层 = 左子树的第K-1层 + 右子树的第K-1层 int TreeKSize(BT* root, int k) { assert(k > 0); // 给出 所问 问题 能成成立的条件(所问,问题的的结束条件) if (root == NULL) { return 0; } if (k == 1) { return 1; } return TreeKSize(root->left, k - 1) + TreeKSize(root->right, k - 1); }
- 递归图:
💦二叉树的深度
- 思路:
此题同样是运用分治的思想来解决,要比较左子树的高度和右子树的高度,大的那个就+1,因为还有根结点也算1个高度。
- 代码演示:
// 树的深度 int TreeDeep(BT* root) { if(root==NULL) { return 0; } int left = TreeDeep(root->left); int right = TreeDeep(root->right); if(left>right) { return left+1; } else { return right+1; } }
- 递归图:
💦二叉树查找值为x的节点
- 思路:
还是利用分治的思想,将其递归化成子问题去解决
- 先去根结点寻找,是就返回此节点
- 此时去左子树查找有无节点x
- 最后再去右子数去查找有无节点x
- 若左右子树均找不到节点x,直接返回空
- 代码演示:
// 查询 树中的某一个节点是否存在 BT* TreeNode(BT* root,TreeDatatype x) { if(root==NULL) { return NULL; } if(root->data==x) { return root; } BT* left = TreeNode(root->left,x); BT* right = TreeNode(root->right,x); if(left!=NULL) { return left; } if(right!=NULL) { return right; } return NULL; }
- 递归图:
假设我们寻找的是数字5
💦二叉树的销毁
- 思路:
销毁的思想和遍历类似,如若我挨个遍历的同时,没遍历一次就销毁一次,岂不能达到效果,但是又会存在一个问题,那就是你要采用什么样的遍历方式?倘若你采用前序遍历,刚开始就把根销毁了,那么后面的子树还怎么销毁呢?因为此时根没了,子树找不到了就,所以要采用倒着销毁的规则,也就是后续的思想
- 代码演示:
// 二叉树的销毁 // 从后往前销毁-------后续思想 // 进行遍历然后在销毁 void DestoryTree(BT*root) { if(root==NULL) { return; } DestoryTree(root->left); DestoryTree(root->right); free(root); }
思想和后续遍历类似,不做递归图演示。
🍊总代码和演示图
💦代码
#include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <assert.h> typedef int TreeDatatype; typedef struct BinaryTree { struct BinaryTree* left; struct BinaryTree* right; TreeDatatype data; }BT; BT* BuyTreenode(TreeDatatype x) { BT* tree = (BT*)malloc(sizeof(BT)); if(tree==NULL) { perror(" fail"); exit(-1); } tree->data = x; tree->left = tree->right = NULL; return tree; } BT* CreatTree() { BT* node1 = BuyTreenode(1); BT* node2 = BuyTreenode(2); BT* node3 = BuyTreenode(3); BT* node4 = BuyTreenode(4); BT* node5 = BuyTreenode(5); BT* node6 = BuyTreenode(6); //BT* node7 = BuyTreenode(7); node1->left = node2; node1->right = node4; node2->left = node3; //node2->right = node7; node4->left = node5; node4->right = node6; return node1; } // 前序遍历:根 左 右 void PreOrder(BT* root) { if(root==NULL) { printf("NULL "); return; } printf("%d ",root->data); PreOrder(root->left); PreOrder(root->right); } // 中序遍历 void InOrder(BT*root) { if(root==NULL) { printf("NULL"); return; } InOrder(root->left); printf("%d ",root->data); InOrder(root->right); } // 后序遍历 void PastOrder(BT* root) { if(root==NULL) { printf("NULL"); return; } PastOrder(root->left); PastOrder(root->right); printf("%d ",root->data); } // 计算一颗树的节点个数 int TreeSize(BT* root) { if(root==NULL) { return 0; } return TreeSize(root->left)+TreeSize(root->right)+1; } // 计算叶子节点个数 int TreeLeveSize(BT* root) { if(root==NULL) { return 0; } if(root->left==NULL&&root->right==NULL) { return 1; } return TreeLeveSize(root->left)+TreeLeveSize(root->right); } // 计算叶子节点个数 // 遍历 + 计数 void TreeLeveSize1(BT* root, int* count) { if(root==NULL) { return ; } if(root->left==NULL&&root->right==NULL) { (*count)++; } TreeLeveSize1(root->left,count); TreeLeveSize1(root->right,count); } // 叶子节点个数 int TreeLeveSize2(BT*root) { if(root==NULL) { return 0; } int left = TreeLeveSize2(root->left); int right = TreeLeveSize2(root->right); if(left==0&&right==0) { return 1; } return left+right; } // 树的第 K 层的节点 int TreeKLeveSize(BT* root,int k) { if(root==NULL) { return 0; } if(k==1) { return 1; } return TreeKLeveSize(root->left,k-1)+TreeKLeveSize(root->right,k-1); } // 树的深度 int TreeDeep(BT* root) { if(root==NULL) { return 0; } int left = TreeDeep(root->left); int right = TreeDeep(root->right); if(left>right) { return left+1; } else { return right+1; } } // 查询 树中的某一个节点是否存在 BT* TreeNode(BT* root,TreeDatatype x) { if(root==NULL) { return NULL; } if(root->data==x) { return root; } BT* left = TreeNode(root->left,x); BT* right = TreeNode(root->right,x); if(left!=NULL) { return left; } if(right!=NULL) { return right; } return NULL; } // 二叉树的销毁 // 从后往前销毁-------后续思想 // 进行遍历然后在销毁 void DestoryTree(BT*root) { if(root==NULL) { return; } DestoryTree(root->left); DestoryTree(root->right); free(root); } int main() { BT* tree = CreatTree(); printf("树的前序遍历 >\n"); PreOrder(tree); printf("\n"); printf("\n"); printf("树的中序遍历 >\n"); InOrder(tree); printf("\n"); printf("\n"); printf("树的后序遍历 >\n"); PastOrder(tree); printf("\n"); printf("\n"); int size = 0; printf("树的节点个数\n"); size = TreeSize(tree); printf("%d\n",size); printf("\n"); int sizeLeve = 0; printf("树的叶子节点个数\n"); int i = 0; TreeLeveSize1(tree,&i); printf("%d\n",i); printf("\n"); int SizeLeve = 0; printf("树的叶子节点个数1\n"); SizeLeve = TreeLeveSize2(tree); printf("%d\n",SizeLeve); printf("\n"); int ksize = 0; printf("树的第K层节点个数\n"); ksize = TreeKLeveSize(tree,3); printf("%d \n",ksize); printf("\n"); int deep = 0; printf("树的深度\n"); deep = TreeDeep(tree); printf("%d \n",deep); printf("\n"); printf("\n"); BT* tre; int x; printf("请输入你要在树中查询的数值:>\n"); scanf("%d",&x); tre = TreeNode(tree,x); if(tre==NULL) { printf("没有你要查询的数据"); } else { printf("查询到了:%d\n",tre->data); } // 销毁这颗树 DestoryTree(tree); tree = NULL; return 0; }
💦视图
二、共勉
以下就是我对数据结构---二叉树遍历的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对数据结构-------二叉树的面试题型,请持续关注我哦!!!!!