博客主页:【夜泉_ly】
本文专栏:【数据结构】
欢迎点赞👍收藏⭐关注❤️
数据结构-链式二叉树-四种遍历
- 1.前言
- 2.前、中、后序遍历
- 2.1前序遍历
- 2.1中、后序遍历
- 3.层序遍历
- 3.1递归实现
- 3.2队列实现
- 关于在`Pop`之后为什么还能用`tmp`访问节点?
- 关于都已经把队列`Pop`为空了为什么还要`QueueDestroy`?
- 4.浅谈DFS与BFS
1.前言
在我之前的文章数据结构-堆-详解中,我对堆这种特殊的完全二叉树做了详细介绍。
完全二叉树非常适合用数组存储,但一般的二叉树呢?如下图所示:
可以发现,普通二叉树若使用数组存储,会浪费大量空间,这时,链式存储结构成为更好的选择。
在这种结构中,每个节点应包含自身所存储的数据,以及指向左右子树的指针。
可以通过以下结构体定义二叉树节点:
typedef char BTDataType;
typedef struct TreeNode
{
BTDataType val;
struct TreeNode* left;
struct TreeNode* right;
}TreeNode;
为了便于理解,我手搓了一个简单的二叉树:
代码如下:
TreeNode* BuyTreeNode(BTDataType x)
{
TreeNode* tmp = (TreeNode*)malloc(sizeof(TreeNode));
if (!tmp)
{
perror("BuyTreeNode::malloc");
return NULL;
}
tmp->val = x;
tmp->left = NULL;
tmp->right = NULL;
return tmp;
}
TreeNode* CreatBinaryTree()
{
TreeNode* root = BuyTreeNode('a');
TreeNode* n1 = BuyTreeNode('b');
TreeNode* n2 = BuyTreeNode('c');
TreeNode* n3 = BuyTreeNode('d');
TreeNode* n4 = BuyTreeNode('e');
TreeNode* n5 = BuyTreeNode('f');
TreeNode* n6 = BuyTreeNode('g');
root->left = n1;
root->right = n2;
n1->left = n3;
n1->right = n4;
n2->left = n5;
n2->right = n6;
return root;
}
注意:这并不是二叉树真正的创建方法,等对于二叉树的结构有更深入的了解后,再讲创建。
在对二叉树进行各项操作时,应对其结构有明确的认识:
对任意一个二叉树,都由 根 、左子树 、右子树 组成。
而左右子树,也是二叉树,也有对应的 根 、左子树 、右子树。
因此,二叉树的定义是递归的,在对二叉树进行处理时也常常使用递归。
而对二叉树的递归操作也应以 根 、左子树 、右子树 为基础展开。
2.前、中、后序遍历
2.1前序遍历
二叉树的前序遍历:先访问根,再遍历左子树,最后遍历右子树。
void BinaryTreePrevOrder(TreeNode* root)
{
if (!root)
{
printf("NULL ");
return;
}
printf("%c ", root->val);
BinaryTreePrevOrder(root->left);
BinaryTreePrevOrder(root->right);
}
先判断根,如果为空,就打印NULL
并返回;
若不为空,则先打印根的值,再遍历左子树,最后遍历右子树。
在二叉树这块,可以先画画逻辑结构图:
虽然画的不全,但大概就是这么个意思。
如果难以理解时可以再画画递归展开图:把代码是怎么一行行执行的给画出来。
经过分析,可知打印结果应该是:a b d NULL NULL e NULL NULL c f NULL NULL g NULL NULL
。
也可以选择不打印空,结果是:a b d e c f g
。
在画过图后,应对二叉树的遍历有更清楚的认识:
在打印d
后为什么直接打印e
?
并非是从d
的节点直接到e
的节点,
而是先访问d
的两个空的左右子树,d
结束了左右子树的遍历,返回到b
。
此时b
结束了它的左子树的遍历,于是开始遍历右子树,然后才到了e
处。
2.1中、后序遍历
二叉树的中序遍历:先遍历左子树,再访问根,最后遍历右子树。
void BinaryTreeInOrder(TreeNode* root)
{
if (!root)
{
printf("NULL ");
return;
}
BinaryTreeInOrder(root->left);
printf("%c ", root->val);
BinaryTreeInOrder(root->right);
}
二叉树的后序遍历:先遍历左子树,再遍历右子树,最后访问根。
void BinaryTreePostOrder(TreeNode* root)
{
if (!root)
{
printf("NULL ");
return;
}
BinaryTreePostOrder(root->left);
BinaryTreePostOrder(root->right);
printf("%c ", root->val);
}
此时,可以写个代码测试一下:
int main()
{
TreeNode* root = CreatBinaryTree();
printf("BinaryTreePrevOrder:");
BinaryTreePrevOrder(root);
printf("\nBinaryTreeInOrder:");
BinaryTreeInOrder(root);
printf("\nBinaryTreePostOrder:");
BinaryTreePostOrder(root);
return 0;
}
运行结果:
也可以不打印NULL:
3.层序遍历
就是从上至下从左至右的遍历:
3.1递归实现
此实现方法并不重要,所以略讲。
先求个高度:
int TreeHeight(TreeNode* root)
{
if (!root)
return 0;
int leftheight = TreeHeight(root->left);
int rightheight = TreeHeight(root->right);
return leftheight > rightheight ? 1 + leftheight : 1 + rightheight;
}
再写个打印第k
层元素的函数:
void BinaryTreeLevelPrint(TreeNode* root, int level)
{
if (!root)
return;
if (level == 1)
printf("%c ", root->val);
else
{
BinaryTreeLevelPrint(root->left, level - 1);
BinaryTreeLevelPrint(root->right, level - 1);
}
}
最后组合起来,即一层一层的打印:
void BinaryTreeLevelOrder(TreeNode* root)
{
int level = TreeHeight(root);
for (int i = 1; i < level; i++)
{
BinaryTreeLevelPrint(root, i);
}
}
测试一下:
int main()
{
TreeNode* root = CreatBinaryTree();
printf("BinaryTreeLevelOrder:");
BinaryTreeLevelOrder(root);
return 0;
}
结果:
3.2队列实现
二叉树层序遍历使用递归比较麻烦,且不太直观,因此,通常使用另一种方法,即队列:
- 先将根节点入队列
- 将队首的节点出队列,并带入当前节点的两个非空子节点
- 重复
- 再重复
- 队列为空,停止
其中,有关队列的函数可直接CV --> 数据结构-栈、队列-详解。
需注意的是,存入队列的是指向节点的指针,因此,需改变一下队列存储的数据类型:
//typedef int QDatatype;
typedef TreeNode* QDatatype;
代码实现:
void BinaryTreeLevelOrder(TreeNode* root)
{
Queue q;
QueueInit(&q);
if (!root);
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
TreeNode* tmp = QueueFront(&q);
QueuePop(&q);
printf("%c ", tmp->val);
if (tmp->left)
QueuePush(&q, tmp->left);
if (tmp->right)
QueuePush(&q, tmp->right);
}
printf("\n");
QueueDestroy(&q);
}
常见疑问解答:
关于在Pop
之后为什么还能用tmp
访问节点?
因为,Pop
的是队列的节点,tmp
为局部变量,保存了队首元素的值,作为指针,指向树中的节点。
因此,在Pop
之后还能用tmp
访问节点。
关于都已经把队列Pop
为空了为什么还要QueueDestroy
?
我所写的队列是不带哨兵位头结点的,所以把队列Pop
空了后,用不用QueueDestroy
都无所谓,但如果其他人写的队列带了哨兵位头结点,不Destroy
就会造成内存泄漏。
这里有个词叫耦合,还有个词叫解耦:
耦合是指两个或两个以上的体系或两种运动形式间通过相互作用而彼此影响以至联合起来的现象。
解耦就是用数学方法将两种运动分离开来处理问题,常用解耦方法就是忽略或简化对所研究问题影响较小的一种运动,只分析主要的运动。
在使用队列时,不管是怎样操作的,在最后都加上QueueDestroy
,这也算是一种解耦,因为这样,无论队列是如何实现的,无论实现者用的数组还是链表、单链还是双链、带不带哨兵位的头结点,都可以避免问题的产生。
4.浅谈DFS与BFS
DFS
:即Depth First Search
,深度优先搜索。
二叉树的DFS
就是前序遍历,放宽一点就是前、中、后序遍历。
特点是一条路走到底,再返回并走其他的路。多用递归实现。
BFS
:即Breadth First Search
,广度优先搜索。
二叉树的BFS
就是层序遍历。
特点是一点点扩大搜索范围,类似于地毯式搜索。多用队列实现。
希望本篇文章对你有所帮助!并激发你进一步探索数据结构的兴趣!
本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!