本文已收录至《数据结构(C/C++语言)》专栏!
作者:ARMCSKGT
你的阅读和理解将是我极大的动力!
目录
前言
我们前面了解过二叉树的顺序结构那就是堆,但是二叉树的链式结构更重要,在以后的高级数据结构中AVL树,二叉搜索树等都是基于链式二叉树构建的,而且现在各大公司对于二叉树知识的考察也很频繁,所以掌握链式二叉树是必不可少的,学好了二叉树也能极大的锻炼我们对递归的理解!
正文
二叉树操作的实现
二叉树的前,中,后序遍历(深度优先遍历)
- 二叉树的前序遍历
口诀:根左右(先走根节点 再走左节点 最后走右节点)。
前序遍历是先访问根节点,再访问左子树和右子树。
// 二叉树前序遍历 void BinaryTreePrevOrder(BTNode* root) { if (root == NULL)//root为空则输出#并停止递归 { printf("# "); return; } printf("%c ", root->data);//先输出根节点 BinaryTreePrevOrder(root->left);//递归左子树 BinaryTreePrevOrder(root->right);//递归右子树 }
- 二叉树的中序遍历
口诀:左根右(先走左节点 再走根节点 最后走右节点)。
中序遍历是先访问左子树,再访问根节点和右子树。
// 二叉树中序遍历 void BinaryTreeInOrder(BTNode* root) { if (root == NULL)//节点为空停止递归并输出# { printf("# "); return; } BinaryTreeInOrder(root->left);//先访问左子树 printf("%c ", root->data);//再输出根节点 BinaryTreeInOrder(root->right);//最后访问右子树 }
- 二叉树的后序遍历
口诀:左右根(先走左节点 再走右节点 最后走根节点)。
后序遍历是先访问左子树和右子树,再访问根节点。
// 二叉树后序遍历 void BinaryTreePostOrder(BTNode* root) { if (root == NULL)//节点为空输出#并停止递归 { printf("# "); return; } BinaryTreePostOrder(root->left);//先访问子树 BinaryTreePostOrder(root->right);//再访问右子树 printf("%c ", root->data);//最后访问根节点 }
通过中序序列与前序序列或者中序序列与后序序列就可以确定一棵唯一的二叉树,而通过前序序列和后序序列则不一定!对于二叉树的遍历,我们采用递归去解决,一直到空节点则停止递进开始回归到主调函数!
求二叉树的节点个数
求二叉树的节点个数,我们仍然是使用递归,将每个节点看作一棵树,那么根节点在计算时就是左子树的节点个数加上右子树的节点个数然后加1(根节点自己),如果是空节点则返回0。
// 二叉树节点个数 int BinaryTreeSize(BTNode* root) { if (root == NULL)//如果为空返回0 { return 0; } return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1; //返回左子树和右子树的节点个数加上自己 }
求叶子节点个数
求叶子节点的个数,利用递归,在递归时当碰到节点的左右子树都为空时则返回1且不再递归,空节点返回0。最后根节点返回左右子树的叶子节点之和!
// 二叉树叶子节点个数 int BinaryTreeLeafSize(BTNode* root) { if (root == NULL)//如果左孩子存在右孩子不存在,右子树返回0 { return 0; } //如果左右子树都为空则为叶子节点,返回1 if (root->left == NULL && root->right == NULL) { return 1; } //返回根节点的左右子树叶子节点之和 return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right); }
求二叉树的深度
求二叉树的深度主要是比较左右子树那一棵树最高,返回最高的那一棵树的深度加1(根节点自己),通过递归比较每一棵树,如果相同则随意返回左右子树的高度即可。
这是一个典型的大事化小的过程,函数先递归到叶子节点,然后逐一返回开始比较左右子树的高度,最后返回最大的高度即是深度!
//求二叉树的深度 int BinaryTreeDepth(BTNode* root) { if (root == NULL)//如果遍历到底则返回0 { return 0; } int leftBT = BinaryTreeDepth(root->left);//获取左子树的深度 int rightBT = BinaryTreeDepth(root->right);//获取右子树的深度 //判断左右子树谁大返回最大值加上自己 return leftBT > rightBT ? leftBT + 1 : rightBT + 1; }
二叉树的层序遍历(广度优先遍历)
二叉树的层序遍历借助队列来完成,所谓层序遍历就是广度优先遍历,逻辑思想中是一层一层的进行遍历,队列的先进先出性质非常适合层序遍历,因此层序遍历不需要递归。
层序遍历的代码逻辑是根节点先入队,然后节点出队时让子节点入队,如果节点为空则不入队,循环往复一直到队列为空时层序遍历结束。核心思想是父节点出队时将自己的子节点带入队列!
// 层序遍历-借助队列 void BinaryTreeLevelOrder(BTNode* root) { Queue q; QueueInit(&q);// 初始化队列 if (root)//判断树是否为空 { QueuePush(&q, root);//根节点入队 while (!QueueEmpty(&q))//队列不为空则继续循环 { QDataType BT = QueueFront(&q);//取队头元素 printf("%c ", BT->data);//打印这个节点值 QueuePop(&q);//队头出队 if (BT->left)//当前节点的左孩子如果存在则入队 { QueuePush(&q, BT->left); } if (BT->right)//当前节点的右孩子如果存在则入队 { QueuePush(&q, BT->right); } } } QueueDestroy(&q);//销毁队列 }
二叉树的构建函数
在二叉树的操作中,树的构建是必不可少的,二叉树的构建对一个数组序列进行解读,通过每一次的解读决定节点是否为空,而为空的标准为当前数组元素是 # ,如果当前数组元素是 # 则该节点为空,每创建完成一个节点就会递归去创建该节点的左右孩子节点,无论节点是否为空都会递归,每创建完一个节点后数组的地址也会加1走到下一个元素然后传递给递归函数,所以在递归时需要传递数组的指针。这里递归的思想类似于前序遍历的思想,所以数组中存入的是这棵二叉树的前序序列!
具体过程:
1. 判断数组当前的元素是否为 # 如果为 # 则函数返回NULL表示该节点为空。
2. 数组的地址自加1走到下一个元素,方便后面递归。
3. 申请一个新节点,节点值为当前的数组元素。
4. 传入当前树的父节点和数组当前元素进度的地址,递归创建左孩子和右孩子。
5. 最后当所有节点创建完成后返回根节点的地址。
//创建二叉树 BTNode* BinaryTreeCreate(BTDataType* a, int* pi) { assert(a); if (a[*pi] == '#')//如果是#返回空 { (*pi)++;//数组地址+1走到下一个元素 return NULL;//返回空 } //创建新节点 BTNode* TreeNode = (BTNode*)malloc(sizeof(BTNode)); if (!TreeNode) { perror("malloc Tree fail\n"); exit(EOF); } TreeNode->data = a[(*pi)++];//给节点赋值且数组地址+1走到下一个元素 TreeNode->left = BinaryTreeCreate(a, pi);//递归创建左右子树 TreeNode->right = BinaryTreeCreate(a, pi); return TreeNode;//返回根节点的地址 }
二叉树的相关OJ题
判断完全二叉树
题目链接:958. 二叉树的完全性检验 - 力扣(LeetCode)
对于完全二叉树的判断,我们需要了解完全二叉树的特点:完全二叉树可以是满二叉树也可以是不满的二叉树,但不满的二叉树的叶子节点必须是从左到右排列的中间不能间断!
我们通过图片可以发现,完全二叉树的叶子节点是从左到右连续的,而非完全二叉树的叶子节点中间则缺失了一个,这就是我们判断完全二叉树的重要条件!基于完全二叉树的这种特性,我们使用层序遍历,一层一层的检验,如果二叉树是完全二叉树那么他的每一层节点都是连续的(非空的),一旦有空节点则表示所有节点已经都遍历过了,后面都是空节点,所以当有节点时我们进行层序遍历,一旦碰到空就退出开始进行检验,检验是通过循环进行排查,如果后面的节点都为空则是完全二叉树,如果在空节点之后排查到非空的节点,那么就不是完全二叉树!
这里的核心思想就是层序遍历和完全二叉树的特点!
判断完全二叉树的代码逻辑:
1. 根节点入队,然后出队带入子节点入队。
2. 出队的节点判断是否为空,如果不为空则将左右孩子入队(无论孩子是否为空节点)。
3. 一旦碰到队列中的空节点则入队结束,开始判断。
4. 利用迭代判断队列中剩余的节点是否都是空节点,如果中间有一个非空节点则返回false。
5. 如果检验完成都是空节点,最后销毁队列返回true。
//完全二叉树的判断---代码接口来自力扣第958题 bool isCompleteTree(struct TreeNode* root ) { Queue q; QueueInit(&q);// 初始化队列 if (root)//判断树是否为空 { QueuePush(&q, root);//根节点入队 while (!QueueEmpty(&q))//队列不为空则继续循环 { QDataType BT = QueueFront(&q);//取节点 QueuePop(&q); if (BT)//如果节点不为空则入队--NULL也入队 { QueuePush(&q, BT->left); QueuePush(&q, BT->right); } else//如果为空则开始判断是否全部为空节点(叶子节点的孩子) { break; } } while(!QueueEmpty(&q)) { QDataType BT = QueueFront(&q);//取节点 QueuePop(&q); if(BT!=NULL)//如果后面的所有节点中有一个不为空则表示不是完全二叉树 { QueueDestroy(&q); return false; } } } QueueDestroy(&q); return true; }
判断平衡二叉树
题目链接:110. 平衡二叉树 - 力扣(LeetCode)
平衡二叉树是一个二叉树每个节点的左右两个子树的高度差的绝对值不超过 1 。
对于平衡二叉树的判断,我们采用求深度的办法去解决,根据题目意思,那么该树的每一层都应该只相差1层,如果相差大于1层则不是平衡二叉树,所以这里需要借助求二叉树深度的函数来解决。
判断平衡二叉树代码思想:
1. 判断节点是否为空,为空则返回true停止递归!
2. 求出当前父节点的左右子树的深度,如果两数的深度只差大于1则不是平衡二叉树。
3. 递归求每一个节点的左右子树是否都满足这个要求!
//代码接口来自力扣题目第110题 //求二叉树的深度 int BinaryTreeDepth(struct TreeNode* root) { if (root == NULL)//如果遍历到底则返回0 { return 0; } int leftBT = BinaryTreeDepth(root->left);//获取左子树的深度 int rightBT = BinaryTreeDepth(root->right);//获取右子树的深度 return leftBT > rightBT ? leftBT + 1 : rightBT + 1;//判断左右子树谁大返回最大值加上自己 } //一层一层的判断是否满足平衡二叉树 bool isBalanced(struct TreeNode* root) { if(!root) return true; int left = BinaryTreeDepth(root->left); int right = BinaryTreeDepth(root->right); if(abs(left-right)>1)//如果该层的高度差不超过1就进入下一层 return false; return isBalanced(root->left) && isBalanced(root->right);//返回所有层的结果 }
翻转二叉树
题目链接:226. 翻转二叉树 - 力扣(LeetCode)
如图为翻转二叉树:
对于二叉树的翻转,我们采用的思想是先递归到叶子节点开始从最下面向上翻转,这是一种后序遍历的思想,当递归到叶子节点时交换左右两棵子树的节点(无论是否为空),然后逐一向上一层一层的递归交换。
翻转二叉树的代码思想:
1. 判断节点是否为空或是否为叶子节点,如果为叶子节点则停止递归,返回当前的节点地址!
2. 后序遍历递归左右子树。
3. 递归到叶子节点时开始交换左右子树,交换完成后回到父节点的递归函数再次交换父节点的左右子树,依次递归直到回到根节点时进行最后的交换完成后翻转结束!
//代码接口来自力扣226题 struct TreeNode* invertTree(struct TreeNode* root) { if(!root||(root->left==NULL&&root->right==NULL))//如果树为空或者树只有一个节点则返回空 { return root; } //后序遍历思想 root->left = invertTree(root->left);//左子树继续遍历 root->right = invertTree(root->right);//右子树继续遍历 //开始调换节点--无论是否为空都换 struct TreeNode* leftnode=root->left; root->left=root->right; root->right=leftnode; return root; }
最后
本篇我们介绍了二叉树的链式实现的相关操作,相对于前面的数据结构,二叉树的实现和理解明显要难一点,但是其应用也更多是我们必须掌握的,这是二叉树的基础知识,关于二叉树的学习还不止于此,还有更多高阶的二叉树我们后期会进行介绍!
本次二叉树的基本知识就介绍到这里啦,希望能够尽可能帮助到大家。
如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!
博客中的所有代码合集:二叉树博客代码
🌟其他文章阅读推荐🌟
🌹欢迎读者多多浏览多多支持!🌹