【数据结构】树形结构--二叉树
一.知识补充
1.什么是树
如图是一个现实生活中的树,观察可以发现,一棵树只有一个主干,而主干又会分出许多枝干,这些枝干可能会再分出更多枝干,最后以叶子结束。
树型结构在现实世界广泛存在,如人类社会的族谱和各种社会组织机构都可以用树来形象表示。
数据结构中的树与现实的树类似,下图中的三种都是数据结构中的树。
树也是由结点构成的有限集合。我们将树定义为:
①.有且仅有一个结点被称为根结点(root)
②.剩余结点又可成为互不相交的集合,每个集合本身是一个树,也是根结点的子树。
2.树的常见概念
根据此图,为大家介绍一下树的常见概念。
结点的度:每个结点拥有的子树个数。
图中树的A结点的度为3,B结点的度为2。
叶子结点(终端结点):度为0的结点,即没有子树的结点。
图中树的叶子结点有K、L、F、G、M、I、J。
子结点(孩子结点):a结点是b结点的子树的根,则a结点是b结点的孩子结点。
图中树的B、C、D结点都是A结点的子结点。
双亲结点(父结点):a结点是b结点的子结点,那么b结点就是a结点的双亲结点。
图中树的A结点是B、C、D结点的双亲结点。
兄弟结点:具有相同父节点的结点被称为兄弟节点。
图中树的B、C、D结点即为兄弟结点。
树的度:一棵树中所有结点的度中最大的度。
图中树的度为3,因为最大的度是A结点和D结点。
结点的层次:从根开始定义,根结点为第一层,根的孩子为第二层,孩子的孩子为第三层,以此类推。(也有的是将根结点定义为第0层,依次相加。)
树的深度(高度):结点层次中最大的那个。
图中树的深度即为4。
祖先:从根结点到某个结点路径上的所有结点都是该结点的祖先。
图中M结点的祖先有H、D、A结点。
子孙:以某结点为根的树中的所有结点都是它的子孙。
图中E、F、K、L结点都是B结点的祖孙。
有序树、无序树:树的各个子树从左至右是有顺序的,不能改变,即称为有序树,反之为无序树。
森林:由多棵互不相交的树构成的集合。
二.二叉树(Binary Tree)
1.二叉树的定义
二叉树是一种特殊的树,它的特点是每个结点最多只有两棵子树,并且子树的左右顺序不能改变。
图中五种都属于二叉树。
2.二叉树的分类
①.一般二叉树:
每个结点最多有两个子结点,无其他要求,结构灵活。
如图就是一棵普通二叉树。
②.满二叉树:
除了叶子结点,其余所有结点度均为2,不存在度为1的结点。
如图就是一棵满二叉树。
如果一棵满二叉树层数为k,那么总结点个数就是2k - 1(等比数列求和)。
③.完全二叉树:
对一棵具有n个结点的二叉树按层序编号,每一个编号为i(1≤i≤n)的结点与同样深度的满二叉树中编号为i 的结点的位置完全相同。
如图右侧就是一棵完全二叉树。
我们会发现完全二叉树就像满二叉树从最后一个结点开始向左任意删除n个结点。完全二叉树最后一层可能不满,但从左往右结点一定是连续的,因此完全二叉树只存在一个度为1的结点。
如图就不是一棵完全二叉树,因为最后一层叶子结点并非连续的。
满二叉树是一种特殊的完全二叉树。
④.二叉排序树(二叉查找树):
左子树结点的值均小于根结点,右子树的值均大于根结点。左右子树又各是一棵二叉排序树。
如图是一棵二叉排序树。
二叉排序树常用于元素的搜索和排序。
⑤.平衡二叉树:
平衡二叉树上任意一个结点的左右子树的高度差不会超过1。
如图就是一棵平衡二叉树。
平衡二叉树与二叉查找树相结合可以提高搜索效率。
⑥.其他二叉树:
线索二叉树(Threaded Binary Tree):通过空指针指向先驱或后继节点,优化遍历。
哈夫曼树(Huffman Tree):用于数据压缩,按频率构建的带权二叉树。等等
3.二叉树的性质
①.一棵二叉树的第i层上,最多有2i-1个结点。
②.一棵二叉树如果总共有k层,那么它最多有2k-1个结点。
③.一棵二叉树如果其度为0的结点个数为n0,度为2的结点个数为n2,那么满足n0=n2+1。
三.二叉树的实现
在逻辑上,树是用递归定义的,而二叉树的各种操作也是使用递归实现的,因此对递归还不熟悉的朋友建议先学习一下递归,否则可能较难上手。
1.二叉树的存储
我们在实现数据元素的存储时,应当明确的是,对于树来说,它的存储应当着重关注数据元素以及数据元素之间的逻辑关系在存储器中的表示。说人话就是,如何表示树的结点之间的逻辑关系,即如何表示结点的双亲和孩子。
二叉树的存储也有顺序存储和链式存储两种。(树的存储结构常用链表结构)
顺序存储:
一棵二叉树的顺序存储就是用一组地址连续的存储单元依次自上而下、自左至右存储树上的结点。
如图是一棵完全二叉树
如上所示,这棵树的结点的编号就是按照从上到下、从左到右来编号。在数组中存储时,就是按照下面这样的方式来去顺序存储:(数组中的内容是相应编号的结点数据)。
借着完全二叉树,我们可以理解一般二叉树的顺序存储。
如图是一棵普通二叉树
我们在进行存储时先将它补全:
那么在数组中的就是这样存储的:
这种存储方式有较明显的缺点,如图是一棵很不平衡的二叉树:
它仅有四个结点,但在数组中存储时却浪费了较大空间。
链式存储:
因为二叉树最多有两个孩子结点,只有一个双亲结点,因此在定义链式存储时有两种表示:二叉链、三叉链。
二叉链:有两个指针域,一个指向左孩子,另一个指向右孩子。
其结构表示为:
cpp
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* leftchild;
struct BinaryTreeNode* rightchild;
}BTNode;
三叉链:有三个指针域,一个指向双亲结点,另外两个指向左、右孩子结点。
其结构表示为;
cpp
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* leftchild;
struct BinaryTreeNode* rightchild;
struct BinaryTreeNode* parent;
}BTNode;
普通二叉树一般用二叉链结构表示,特殊二叉树(AVL树、红黑树、B树等)会采用三叉链结构来表示。
本文使用二叉链式结构来实现二叉树。由于二叉树是由根结点,左子树,右子树构成,其中左子树也包含它的根结点,左子树和右子树。
2.二叉树的遍历
二叉树的遍历是指从根结点出发按照某种次序依次访
问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次。
对于线性结构,遍历是很简单的事。一个数组我们可以从头到尾依次遍历,一个链表我们可以根据结点的指向来遍历,那么对于一个有多分支的树形结构,我们如何遍历一棵二叉树呢?可以通过以下四种方法。
①.先序遍历
先序遍历也叫前序遍历,就是根结点在前,按照根->左->右的顺序递归遍历一棵树。
若二叉树为空,则返回;否则:①访问根结点;②前序遍历根结点的左子树;③前序遍历根结点的右子树。
由于每个结点又包含左子树和右子树,因此遍历完该结点,遍历它的左子树时,同样要进入左子树的左子树,按照根->左->右的顺序遍历,直到最底层的结点的左右子树都遍历完,再向上返回遍历其他结点。
以图中这棵树为例,详细讲解一下先序遍历的过程。
先序遍历这棵树,先访问根结点,得到A,然后遍历A的左子树:
对于该左子树,同样先访问根结点,得到A->B,再遍历B的左子树
同样先访问根结点,得到A->B->D,再遍历D的左子树,左子树为空,然后遍历D的右子树,得到A->B->D->G。
B这棵左子树递归遍历完之后依次向上返回,进入A的右子树。
同样先访问根结点,得到A->B->D->G->C,进入C的左子树
同样先访问根结点,得到A->B->D->G->C->E,然后遍历左子树,为空,再遍历右子树,为空,返回。
C的左子树遍历完,进入C的右子树
同样先访问根结点,得到A->B->D->G->C->E->F,然后遍历左子树,为空,再遍历右子树,为空,返回。
至此,整棵树已经完全被遍历完,顺序如图
得到先序遍历结果是:A->B->D->G->C->E->F。
代码实现为:
cpp
//先序遍历
void PreOrder(BTNode* root)
{
//根结点为空,即树为空时,直接返回
if (root == NULL)
return;
printf("%c ", root->data); //遍历根结点
PreOrder(root->leftchild); //遍历左子树
PreOrder(root->rightchild); //遍历右子树
}
②.中序遍历
中序遍历即根结点在中间,按照左->根->右的顺序递归遍历一棵树,即:①中序遍历根结点的左子树;②访问根结点;③中序遍历根结点的右子树。
每个结点都包含左子树和右子树。在遍历时从根结点进入它的左子树,再进入左子树的左子树,直到进入最后一个左子树,访问该左子树的左孩子,然后访问根结点,然后访问右孩子,再依次向上返回遍历剩下结点。
依旧按照这棵树为例,详细讲述中序遍历的过程。
中序遍历这棵树,首先根据根结点进入A的左子树:
B的左子树不为空,再进入B的左子树:
发现D的左子树为空,返回,然后访问根结点得到D,然后进入D的右子树:
发现G的左子树为空,返回,访问根结点得到D->G,G的右子树也为空,返回。
此时B的左子树遍历完,访问B这个结点,得到D->G->B,
然后进入B的右子树,为空,返回。
A的左子树已经全部遍历完,访问A这个结点,得到D->G->B->A,然后进入A的右子树:
C的左子树不为空,进入C的左子树:
发现E的左子树为空,返回,然后访问B结点,得到D->G->B->A->E,E的右子树也为空,返回。
C的左子树遍历完,访问C结点,得到D->G->B->A->E->C,然后进入C的右子树:
F的左子树为空,返回,访问F结点得到D->G->B->A->E->C->F,F的右子树为空,返回。
此时C这棵树已经全部遍历完,向上返回,A这棵树也全部遍历完,中序遍历结束,顺序为:
遍历结果为D->G->B->A->E->C->F。
代码实现为:
cpp
//中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
return;
InOrder(root->leftchild);//先遍历左子树
printf("%c ", root->data);//然后是根
InOrder(root->rightchild);//最后是右子树
}
③.后序遍历
后序遍历即按照左->右->根的顺序,最后访问根结点的操作,即:①后序遍历根结点左子树;②后序遍历根结点的右子树;③最后访问根结点。
依旧以该树为例进行后序遍历,这次画图演示,不做详细说明。
得到的后序遍历结果为:G->D->B->E->F->C->A。
代码实现为:
cpp
//后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
return;
PostOrder(root->leftchild); //先遍历左子树
PostOrder(root->rightchild);//再遍历右子树
printf("%c ", root->data); //最后遍历根结点
}
④.层序遍历
层序遍历即一层一层地依次访问每个结点。
如图即为层序遍历的顺序:
层序遍历是借助队列这个数据结构来实现的(对队列不清楚的可以先看看我这篇讲述队列的博客链接: 【数据结构】--队列。)
首先将根结点加入队列,当队列不为空时,取出队头结点,访问该结点,然后将队头结点的左孩子,右孩子加入队列。循环执行该操作,直到队列所有元素都取出,队列为空时停止。
代码实现为:
cpp
//层序遍历
void LevelOrder(BTNode* root)
{
//借助队列完成
Queue q;
QueueInit(&q);
if (root == NULL)
return;
QueuePush(&q, root);
while (QueueSize(&q))
{
BTNode* tmp = QueueFront(&q);
QueuePop(&q);
if (tmp->leftchild)
QueuePush(&q, tmp->leftchild);
if (tmp->rightchild)
QueuePush(&q, tmp->rightchild);
printf("%c ", tmp->data);
}
}
OK,这篇文章先讲到这里,二叉树内容有点多且相对较难,剩下的操作下篇文章再详细介绍。
感谢阅读!^ _ ^