文章目录
前言
我们知道二叉树有两种存储表示方式:1.数组结构;2. 链式结构。
数组表示法用于完全二叉树的存储非常有效,但表示一般二叉树,特别是形态剧烈变化的,存储空间的利用不是很理想。使用链式结构表示,可以克服这些缺点。
这一节介绍二叉树链式结构的实现以及二叉树常见的一些基础问题。
前置声明:
这篇博客将二叉树的创建和销毁放在了二叉树的遍历后面,因为二叉树的创建需要使用到一些关于二叉树遍历的思想。
所以我们需要先学习二叉树的遍历,这里我们先手动创建一个简单的二叉树(该方法不是真正创建二叉树的方法,只是为了方便学习二叉树的遍历而快速简单地创建的一颗二叉树)
#include<stdio.h>
#include<stdlib.h>
typedef int BTDataType;
typedef struct BTNode {
struct BTNode* _left;
struct BTNode* _right;
BTDataType _data;
}BTNode;
BTNode* BuyBTNode(BTDataType data)
{
BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
if (newnode == NULL)
{
printf("fail : malloc");
exit(-1);
}
newnode->_left = NULL;
newnode->_right = NULL;
newnode->_data = data;
return newnode;
}
BTNode* CreateBinTree()
{
BTNode* node1 = BuyBTNode(1);
BTNode* node2 = BuyBTNode(2);
BTNode* node3 = BuyBTNode(3);
BTNode* node4 = BuyBTNode(4);
BTNode* node5 = BuyBTNode(5);
BTNode* node6 = BuyBTNode(6);
BTNode* node7 = BuyBTNode(7);
node1->_left = node2;
node1->_right = node3;
node2->_left = node4;
node2->_right = node5;
node3->_left = node6;
node3->_right = node7;
return node1;
}
int main()
{
BTNode * tree = CreateBinTree();
return 0;
}
创建出来的结构是这样的:
二叉树的遍历
二叉树是最基本的树形结构,也是我们重点研究的对象,在二叉树中所有可用的操作中,遍历是最常用的操作,所谓二叉树的遍历,就是遵从某种次序,访问二叉树中的所有节点,使得每一个节点被访问一次,而且只访问一次。
学习遍历二叉树之前,我们需要知道树是一种递归结构,每一颗树都是由三部分组成:根、左子树、右子树。
左子树也是一棵树,同样也被分为了三部分:根、左子树、右子树…遍历二叉树时中重要的就是递归思想:递归思想是将一个问题一直分解为子问题,直到分解为不可分割的子问题 ,就结束了。
二叉树有四种遍历结构:前序遍历、中序遍历、后序遍历、层序遍历。
前面三种遍历也被称为深度优先遍历,层序遍历也被称为广度优先遍历。
前序遍历
也叫做先根遍历。即一棵树的访问顺序是:根节点、左子树、右子树。
遍历规则是:先访问一棵树的根节点,然后访问到这棵树的左子树、如果该树的左子树已经遍历完全,那么接下来就遍历右子树。访问到空节点或者访问完右子树后才能返回。
我们利用前面实现好的这棵树来作为例子:
这是一颗完全二叉树,每一颗节点中存储的数据都是一个整形数据。
第一步,我们需要访问这棵树的根节点,即节点1
(在演示过程的时候根节点都使用淡蓝色来注明)。
第二步:访问这棵树的左子树:在访问左子树的时候,我们需要先将整个左子树看作一颗完整的树,然后遍历完这棵树中所有数据。
同样,需要先访问这棵树的根节点:节点2
,然后我们还需要去访问节点2
的左子树:以节点4
为根节点的树。
和每一个节点一样,节点4也有两个指针用来指向它的左右子树,但是因为这个节点是叶子节点,所以这两个指针都是空指针。
思考:我们知道节点4 和节点5 都是叶子节点,在先序遍历节点2为根节点的树的过程中:遍历的顺序是 2、4、5。但是程序是怎样判断出2的左子树已经遍历完全了呢?(换一种问法:遍历函数在什么情况下会返回呢?)
其实,遍历函数在两种情况下会返回:
- 遍历的时候遇到了空指针。例如,当我们遍历节点4的左子树的时候,发现指向做指数的指针是空指针,表示该节点没有左子树,此时就需要返回。
- 遍历完全的时候。当程序遍历完一棵树的左右子树的时候就需要返回。例如,程序遍历以节点2 为根节点的树的时候,如果左子树4 与右子树5 都已经遍历完全的时候,就需要返回上一级遍历。
如图,节点4
的左右指针为空指针,在遍历这一颗树的时候,首先访问了根节点:节点4
,然后访问左子树:空指针。当出现空指针的时候,就说明节点4
已经没有左子树需要遍历。我们接下来需要遍历节点4
的右子树,指向右子树的指针也是空指针,所以节点4 也没有右子树需要遍历。这时,我们就可以返回上一层遍历了。
我们利用函数栈帧的知识来解释(每一次函数调用都会创建一个函数栈帧):
这时就代表一节点4 为根节点的树已经遍历完全了。同时也代表了以节点2 为根节点的树的左子树遍历完全,接下来可以遍历该树的右子树:以节点5为根节点的树。
下面是前序遍历的代码实现:(为了可以更明确的看出整个过程,这段代码将遍历到的空指针也打印出来了)
这里我们采用打印的方法来遍历整棵树:
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%d ", root->_data); //先访问根节点
PrevOrder(root->_left); //再访问左子树
PrevOrder(root->_right); //再访问右子树
}
代码的逻辑:
dayin
打印(遍历)的结果:
可以利用这张图片去看到完整的遍历过程:(n表示空指针)
中序遍历
中序遍历和先序遍历的唯一区别就是访问顺序。中序遍历的访问顺序是:左子树、根、右子树。
访问每一棵树的时候都需要先遍历该树的左子树。左子树遍历完成后访问根节点,然后遍历这棵树的右子树。
void InvOrder(BTNode* root