在# 08--树的基础知识是已经介绍了树的基础知识,下面来实现一下二叉树的顺序存储。
一、二叉树的特征
- 1.二叉树的第i层上,最多有
2的(i-1)次方个结点;- 2.深度为k的树最多有
(2的k+1次方)-1个结点;- 3.有n个结点的完全二叉树的深度为
(log以2为底n的对数)+1;- 4.对有n个结点的完全二叉树,如果按照从上到下,从左到右的顺序的顺序从1开始对所有的结点编号,则对任意序号为i的结点有:
- (1).如果i=1,则序号为i的结点为
根结点,根结点无双亲结点;- (2).如果i>1,则序号为i的结点的
双亲结点的序号为i/2;- (3).如果2*i <= n,则序号为i的结点的
左孩子的序号为2*i;- (4).如果2*i > n,则序号为i的结点无左孩子;
- (5).如果2*i + 1 <= n,则序号为i的结点的
右孩子的序号为2*i+1;- (6).如果2*i + 1 > n,则序号为i的结点无右孩子。
二、二叉树顺序实现的设计
结合顺序存储内存连接的特征,需要设计一个数组来保存二叉树。以下面这棵二叉树为例:
1.准备工作
定义一些状态和数据类型,用户二叉树的顺序实现过程的使用,设计一个数组类型SqBiTree来保存二叉树。
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 100 //存储空间初始分配量
#define MAX_TREE_SIZE 100 //二叉树的最大结点数
typedef int Status; //Status是函数的类型,其值是函数结果状态代码,如OK等
typedef int CElemType; //树结点的数据类型,目前暂定为整型
typedef CElemType SqBiTree[MAX_TREE_SIZE]; //0号单元存储根结点
CElemType Nil = 0; //设整型以0为空 或者以 INT_MAX(65535)
设计一个结构体,用来表示二叉树中某个结点的位置
typedef struct {
int level; //结点所在的层
int order; //本层的序号(按照满二叉树给定序号规则)
}Position;
2.初始化二叉树
Status InitBiTree(SqBiTree T){
for (int i = 0; i < MAX_TREE_SIZE; i++) {
//将二叉树初始化值置空
T[i] = Nil;
}
return OK;
}
二叉树T是一个
SqBiTree类型的数组,初始状态下,二叉树为空,所以所有位置都赋为Nil。
3.创建二叉树
Status CreateBiTree(SqBiTree T){
int i = 0;
while (i < 10) {
T[i] = i+1;
printf("%d ",T[i]);
//结点不为空,且无双亲结点
if (i != 0 && T[(i+1)/2-1] == Nil && T[i] != Nil) {
printf("出现无双亲的非根结点%d\n",T[i]);
exit(ERROR);
}
i++;
}
//将空赋值给T的后面的结点
while (i < MAX_TREE_SIZE) {
T[i] = Nil;
i++;
}
return OK;
}
- 1.创建二叉树时,一般按照
从上到下,从右到右的层序的次序方式将树的结点存入数组T;- 2.在向树中插入结点时,需要判断当前插入的位置的
双亲结点是否存在,如果连双向都没有,又怎么会有孩子结点呢?- 3.将未插入数据的位置设置为Nil,表示该位置暂无数据。
如上图的二叉树中,只有结点1 2 3 5 7 10的位置有结点,其他位置上没有结点,所以在数组T中,需要把4 6 8 9 这些没有结点的位置在数组T中空出来,表示这些位置上没有结点,即结点B和C没有左孩子。
4.清空二叉树
#define ClearBiTree InitBiTree
清空后的二叉树,数组T的所有位置的数据都为Nil,所以清空和初始化的代码实现是等同的,所以可以使用
#define的方式对初始化函数重命名为清空函数。
5.二叉树判空
Status BiTreeEmpty(SqBiTree T){
//根结点为空,则二叉树为空
if (T[0] == Nil)
return TRUE;
return FALSE;
}
根结点存在数组T的0的位置,当根结点都不存在时,说明当前的树为空。
6.获取二叉树的深度
int BiTreeDepth(SqBiTree T){
int j = -1;
int i;
//找到最后一个结点
for (i = MAX_TREE_SIZE-1 ; i>=0; i--) {
if (T[i] != Nil)
break;
}
do {
j++;
} while ( powl(2, j) <= i); //计算2的j次幂
return j;
}
- 1.从后面开始遍历可以快速找到最后一个结点,因为最后一个结点之后的结点都是
Nil;- 2.第i个结点所在的
深度为(log以2为底i的对数),如:
- (1)第0元素A的深度为(log以2为底0的对数) = 0;
- (2)第1元素B的深度为(log以2为底2的对数) = 1;
- (3)第6元素E的深度为(log以2为底6的对数) = 2;
- (4)以此类推,第i个结点的深度为(log以2为底i的对数)。
- 3.所以上面的i = (log以2为底j的对数), 从而推导出do while循环结束的条件上是(2的j次方) <= i。
7.返回指定位置的数据
CElemType Value(SqBiTree T,Position e){
/*
Position.level -> 结点层.表示第几层;
Position.order -> 本层的序号(按照满二叉树给定序号规则)
*/
//二叉树不为空
if (BiTreeEmpty(T)) {
return ERROR;
}
//pow(2,e.level-1) 找到层序
printf("%d\n",(int)pow(2,e.level-1));
//e.order
printf("%d\n",e.order);
//4+2-2;
return T[(int)pow(2,e.level-1)-1 + e.order-1];//-1是因为数据是从0开始的
}
- 1.根结点在第1层,结点所在的
层的计算公式为:2的(i-1)次方;- 2.位置和层都是从1开始算的,但是数组存储数据是从0开始存的,所以第i层第j个位置的结点在数组中的位置为:
(2的(i-1)次方)-1 + (j-1);
- (1)
(2的(i-1)次方)的位置为每一层的最左边的结点,但这个结点在数组中的位置为(2的(i-1)次方)-1,如上图中的B为第2层第1个结点,它在数组中的位置为1 = 2-1 = (2的(2-1)次方)-1;- (2)第i层的第j个结点的位置为
第i层第一个元素的位置加上j-1,所以就有了上面的公式。
8.获取根结点的数据
Status Root(SqBiTree T,CElemType *e){
if (BiTreeEmpty(T)) {
return ERROR;
}
*e = T[0];
return OK;
}
默认数组T的第0个元素保存的就是根结点。
9.修改或设置二叉树指向位置的数据
Status Assign(SqBiTree T,Position e,CElemType value){
//找到当前e在数组中的具体位置索引
int i = (int)powl(2, e.level-1)-1+e.order -1;
//叶子结点的双亲为空
if (value != Nil && T[(i+1)/2-1] == Nil) {
return ERROR;
}
//给双亲赋空值但是有叶子结点
if (value == Nil && (T[i*2+1] != Nil || T[i*2+2] != Nil)) {
return ERROR;
}
T[i] = value;
return OK;
}
- 1.第i层第j个位置的结点在数组中的位置为:
(2的(i-1)次方)-1 + (j-1);- 2.修改或设置某个位置数据时,需要判断这个位置结点的
双亲结点是否存在,如果不存在,则这个位置是无效的,不能执行插入操作;- 3.如果修改或设置的值为空,且当前结点
有叶子结点,则对这个位置的操作是无效的;- 4.数组中,第i个结点的
双亲结点的下标为:(i+1)/2 -1;- 5.数组中,第i个结点的
左孩子的下标为:i*2 +1;- 6.数组中,第i个结点的
右孩子的下标为:i*2 +2。
10.获取指定数据所在结点的双亲结点
CElemType Parent(SqBiTree T, CElemType e){
//空树
if (T[0] == Nil) {
return Nil;
}
for (int i = 1 ; i < MAX_TREE_SIZE; i++) {
//找到e
if (T[i] == e) {
return T[(i+1)/2 - 1];
}
}
//没有找到
return Nil;
}
- 1.当二叉树为空时,不存在指定数据的双亲结点;
- 2.从第1个位置开始循环数组T,找到对应的数据的结点,不从0开始是因为
根结点没有双亲结点;- 3.数组中,
第i个结点的双亲结点的下标为:(i+1)/2 -1。
11.获取指定数据所在结点的左孩子
CElemType LeftChild(SqBiTree T,CElemType e){
//空树
if (T[0] == Nil) {
return Nil;
}
for (int i = 0 ; i < MAX_TREE_SIZE-1; i++) {
//找到e
if (T[i] == e) {
return T[i*2+1];
}
}
//没有找到
return Nil;
}
- 1.当二叉树为空时,任何位置结点的左孩子为Nil;
- 2.从第0个位置开始循环数组T,找到对应的数据的结点;
- 3.数组中,
第i个结点的左孩子的下标为:i*2 +1;- 4.如果当前结点为叶子结点,则它的左孩子不存在,返回Nil。
12.获取指定数据所在结点的右孩子
CElemType RightChild(SqBiTree T,CElemType e){
//空树
if (T[0] == Nil) {
return Nil;
}
for (int i = 0 ; i < MAX_TREE_SIZE-1; i++) {
//找到e
if (T[i] == e) {
return T[i*2+2];
}
}
//没有找到
return Nil;
}
- 1.当二叉树为空时,任何位置结点的右孩子为Nil;
- 2.从第0个位置开始循环数组T,找到对应的数据的结点;
- 3.数组中,
第i个结点的右孩子的下标为:i*2 +2;- 4.如果当前结点为叶子结点,则它的右孩子不存在,返回Nil。
13.获取指定数据所在结点的左兄弟
CElemType LeftSibling(SqBiTree T,CElemType e)
{
/* 空树 */
if(T[0]==Nil)
return Nil;
for(int i=1;i<=MAX_TREE_SIZE-1;i++)
{
//找到e且其序号为偶数(是右孩子)
if(T[i]==e && i%2==0){
return T[i-1];
}
}
return Nil; //没找到e
}
- 1.当二叉树为空时,任何位置结点的左兄弟为Nil;
- 2.从第1个位置开始循环数组T,找到对应的数据的结点,根结点是没有左兄弟的;
- 3.二叉树中,
右孩子在数组中的下标为偶数,左孩子在数组中的下标为奇数;- 4.要找左兄弟,那目标结点就是右孩子,所以才有代码中i为偶数的判断。
14.获取指定数据所在结点的右兄弟
CElemType RightSibling(SqBiTree T,CElemType e)
{
/* 空树 */
if(T[0]==Nil)
return Nil;
for(int i=1;i<=MAX_TREE_SIZE-1;i++)
{
//找到e且其序号为奇数(是左孩子)
if(T[i]==e && i%2==1){
return T[i+1];
}
}
return Nil; //没找到e
}
- 1.当二叉树为空时,任何位置结点的右兄弟为Nil;
- 2.从第1个位置开始循环数组T,找到对应的数据的结点,根结点是没有右兄弟的;
- 3.二叉树中,
右孩子在数组中的下标为偶数,左孩子在数组中的下标为奇数;- 4.要找右兄弟,那目标结点就是左孩子,所以才有代码中i为奇数的判断。
三、二叉树的遍历
1.层序遍历
1.解释说明
像创建二叉树的那样,一般按照
从上到下,从左到右的层序的次序方式将二叉树的结点数据依次打遍历,即数组T从头到尾依次遍历。
2.代码实现
void LevelOrderTraverse(SqBiTree T){
int i = MAX_TREE_SIZE-1;
//找到最后一个非空结点的序号
while (T[i] == Nil) i--;
//从根结点起,按层序遍历二叉树
for (int j = 0; j <= i; j++)
//只遍历非空结点
if (T[j] != Nil)
visit(T[j]);
printf("\n");
}
- 1.先找到
最后一个结点;- 2.从第
0个依次遍历到最后一个结点;- 3.如果当前结点是有效结点,则打调用打印函数visit。
Status visit(CElemType c){
printf("%d ",c);
return OK;
}
2.前序遍历
1.解释说明
- 1.如上图,按照1~9的顺序遍历二叉树中的数据;
- 2.规则:
先打印双亲节点,再打印左孩子,最后打印右孩子。
2.代码实现
void PreTraverse(SqBiTree T,int e){
//打印结点数据
visit(T[e]);
//先序遍历左子树
if (T[2 * e + 1] != Nil) {
PreTraverse(T, 2*e+1);
}
//最后先序遍历右子树
if (T[2 * e + 2] != Nil) {
PreTraverse(T, 2*e+2);
}
}
- 1.二叉树具有明显的可以把
大问题分解成小问题,并且小问题和大问题具有相同的特征,所以我们可以使用递归的方法来实现二叉树的遍历;- 2.
先打印双亲结点,再递归左子树,最后递归右子树。
Status PreOrderTraverse(SqBiTree T){
//树不为空
if (!BiTreeEmpty(T)) {
PreTraverse(T, 0);
}
printf("\n");
return OK;
}
- 1.二叉树
不能为空遍历才有意义;- 2.从
根结点的位置开始遍历。
3.中序遍历
1.解释说明
- 1.如上图,按照1~9的顺序遍历二叉树中的数据;
- 2.规则:
先打印左孩子,再打印双亲结点,最后打印右孩子。
2.代码实现
void InTraverse(SqBiTree T, int e){
/* 左子树不空 */
if (T[2*e+1] != Nil)
InTraverse(T, 2*e+1);
//打印
visit(T[e]);
/* 右子树不空 */
if (T[2*e+2] != Nil)
InTraverse(T, 2*e+2);
}
- 1.同样采用递归的方式遍历;
- 2.
先递归左子树,再打印,最后递归右子树。
Status InOrderTraverse(SqBiTree T){
/* 树不空 */
if (!BiTreeEmpty(T)) {
InTraverse(T, 0);
}
printf("\n");
return OK;
}
- 1.二叉树不能为空遍历才有意义;
- 2.从根结点的位置开始遍历。
4.后序遍历
1.解释说明
- 1.如上图,按照1~9的顺序遍历二叉树中的数据;
- 2.规则:
先打印左孩子,再打印右孩子,最后打印双亲结点。
2.代码实现
void PostTraverse(SqBiTree T,int e)
{ /* 左子树不空 */
if(T[2*e+1]!=Nil)
PostTraverse(T,2*e+1);
/* 右子树不空 */
if(T[2*e+2]!=Nil)
PostTraverse(T,2*e+2);
//打印
visit(T[e]);
}
- 1.同样采用递归的方式遍历;
- 2.
先递归左子树,再递归右子树,最后打印。
Status PostOrderTraverse(SqBiTree T)
{
if(!BiTreeEmpty(T)) /* 树不空 */
PostTraverse(T,0);
printf("\n");
return OK;
}
- 1.二叉树不能为空遍历才有意义;
- 2.从根结点的位置开始遍历。
四、总结
二叉树的前序遍历、中序遍历和后序遍历描述的就是在使用递归遍历时什么时机打印双亲结点。规则如下:
前序遍历:先打印,再递归左子树,最后递归右子树;中序遍历:先递归左子树,再打印,最后递归右子树;后序遍历:先递归左子树,再递归右子树,最后打印。