树(Tree)
树的定义:
树(英语:tree)是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限结点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
树的应用:操作系统的文件管理、操作系统目录管理、数据库系统的索引等。
树一般具有以下的特点:
- 每个结点都只有有限个子结点或无子结点;
- 没有父结点的结点称为根结点;
- 每一个非根结点有且只有一个父结点;
- 除了根结点外,每个子结点可以分为多个不相交的子树;
- 树里面不存在环路(cycle)
基本术语
- 结点: 数据结构中的基础,是构成复杂数据结构的基本组成单位。
- 结点的层: 从根结点算,根结点所在的位置为第一层,根的子结点为第二层,以此类推,结点的层为,根结点到该结点的路长加一;
- 森林: 由m(m>=0)棵互不相交的树的集合称为森林;
- 结点的度: 一个结点含有的子树的个数称为该结点的度;
- 树的深度: 对于任意结点n,n的深度为从根到n的唯一路径长,根的深度为0;
- 兄弟结点: 具有相同父结点的结点互称为兄弟结点;
- 叶结点或终端结点: 度为0的结点称为叶结点;
树的种类
- 无序树:
树中任意结点的子结点之间没有顺序关系,这种树称为无序树,也称为自由树; - 有序树:
树中任意结点的子结点之间有顺序关系,这种树称为有序树; - 二叉树:
树中每个结点最多含有两个子树的树称为二叉树;- 平衡二叉树(AVL):
它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 - 完全二叉树:
对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的结点数目均已达最大值,且第d层所有结点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树;- 满二叉树:
所有叶结点都在最底层的完全二叉树;
- 满二叉树:
- 平衡二叉树(AVL):
- B树:
一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多于两个子树。 - 霍夫曼树:
给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
树的存储结构
1. 双亲表示法
用一段连续的空间存储树的结点,其中每个结点除了有数据域,还附加一个指针,指针指向其双亲结点在链表中的位置。也就是说,每个结点除了知道自已是谁以外,还知道它的双亲在哪里。
双亲表示法的结点结构如下图 1-2所示:
上图 1-2 中 data 是数据域,存储结点的数据信息。而 parent 是指针域,存储该结点的双亲在数组中的下标。
图 1-1 中的树采用双亲表示法
优点: parent指针域指向数组下标,所以找双亲结点的时间复杂度为O(1),向上一直找到根结点也快。
缺点: 由上向下找就十分慢,若要找结点的孩子或者兄弟,要遍历整个树。
双亲表示法结构定义
/*树的双亲表示法结点结构定义*/
typedef int ElemType;
/*树的最大长度*/
#define TREE_MAX_SIZE 50
/*树的结点结构*/
typedef struct TNode
{
ElemType data;/*数据域*/
int parent;/*指向双亲结点*/
}TNode,* pTNode;
/*树结构*/
typedef struct Tree
{
TNode nodes[TREE_MAX_SIZE];
int r;/*根结点位置*/
int n;/*结点数*/
}Tree,* pTree;
2. 孩子表示法
一棵树中存在很多个结点,而且每个结点都包含多颗子树,可以使用多重链表完成,即每个结点含有一个数据域和多个指针域,指针用来指向包含的每个子树的根结点。由于每个结点所包含的子树的个数不相同(结点的度不同),则会出现以下两种表示方法:
- 每个结点的度等于树的度d
这种结构中每个结点的度都等于树的度d,当一棵树中结点的度相差很大时,会造成空间的大量浪费,出现许多不必要的空指针。
- 每个结点的指针域个数等于各个结点的度
这种结构中多了一个结点用来存放各个结点的度,后面的指针域的个数等于前面保存的各个结点的度,这个方法有效的解决了第一种方法带来的空间上的浪费,但是由于树中每个结点的度的大小各不相同,维护各个结点的度时,会存在时间上的浪费。
以上两种方法或多或少都存在空间或时间上的浪费,当然也会有更优的解决方法解决这一问题。
为了遍历整课树,把每个结点放到一个顺序存储结构的数组中是合理的,但是每个结点的孩子有多少是不确定的,所以可以建立一个单链表体现它们的关系-------- 孩子表示法。
把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中,这样便可以有效的节省空间和时间。
图 1-1中的树用孩子表示如下:
这样的结构对于我们要查找某个结点的某个孩子,或者找某个结点的兄弟,只需要查找这个结点的孩子单链表即可。对于遍历整棵树也是很方便的,对头结点的数组循环即可。
这种表示方法会产生两种结构,分别为孩子结点结构和表头结构。
孩子结点结构:
孩子结点结构中child用来存储某个结点在表头数组的下标,next为指向某结点的下一个孩子结点的指针。
表头结构:
表头结构中data存储某结点的数据信息,firstchild表示头指针,用来指向孩子链表的第一个元素。
孩子表示法结构定义
//待处理数据类型
typedef char ElemType;
//最大存储长度
#define MAX_TREE_SIZE 50
//孩子结点结构
typedef struct TNode
{
int child; //孩子结点下标
struct TNode *next; //指向下一个孩子结点的指针
}*PTNode;
//表头结点结构
typedef struct
{
ElemType data; //存放的数据
PTNode firstchild; //指向第一个孩子结点的指针
}Header;
//树结构
typedef struct
{
Header nodes[MAX_TREE_SIZE]; //结点数组
int r,n;//r表示根结点位置,n表示结点个数
}Tree,* PTree;
这种结构也会存在一些问题:无法得知某结点的双亲结点,所以可以在表头结构中添加一项双亲结点即可。
图 1-1中的树用双亲孩子表示如下:
3. 二叉链表表示法(孩子兄弟表示法)
任意一棵树的任意结点的第一个孩子如果存在,那么就是唯一的,这个孩子的右兄弟如果存在也是唯一的,那么,可以设置两个指针分别指向第一个孩子和这个结点的右兄弟。
其中 data 是数据域,用来存放结点的数据,firstchild 为指针域,指向此结点的第一个孩子,rightsib 是指针域指向此结点的右兄弟,如果有兄弟不存在则为空。
图 1-1采用孩子兄弟表示如下图:
孩子表示法结构定义
//待处理数据类型
typedef char ElemType;
//最大存储长度
#define MAX_TREE_SIZE 50
typedef struct SNode
{
ElemType data; //存放的数据
struct SNode * firstchild; //结点的第一个孩子
struct SNode * rightsib; //结点的右兄弟
}SNode,* PSNode;
这种表示法,给查找某个结点的某个孩子带来了方便,只需要通过fistchild 找到此结点的长子,然后再通过长子结点的rightsib 找到它的二弟,接着一直下去,直到找到具体的孩子。当然,如果想找某个结点的双亲,这个表示法也是有缺陷的。
二叉树(Binary Tree)
二叉树(英语:Binary tree)是每个结点最多只有两个分支(即不存在分支度大于2的结点)的树结构。通常分支被称作“左子树”或“右子树”。二叉树的分支具有左右次序,不能随意颠倒。二叉树的第i层至多拥有 2i-1个结点;深度为 k 的二叉树至多总共有2k-1个结点(定义根结点所在深度 k0 = 0 ),而总计拥有结点数符合的,称为“满二叉树”;深度为 k 有 n 个结点的二叉树,当且仅当其中的每一结点,都可以和同样深度 k 的满二叉树,序号为 1 到 n 的结点一 一对应时,称为完全二叉树。
与普通树不同,普通树的结点个数至少为1,而二叉树的结点个数可以为 0;普通树结点的最大分支度没有限制,而二叉树结点的最大分支度为2;普通树的结点无左、右次序之分,而二叉树的结点有左、右次序之分。
二叉树的特点:
- 二叉树的每个结点最多有两颗子树,所以二叉树中不存在度大于 2 的结点;
- 左子树和右子树是有顺序的,次序不能任意颠倒;
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
二叉树的性质:
- 二叉树的第 i 层最多会有 2i-1 个结点(i ≥ 1);
- 对任何一棵非空的二叉树 T,如果其叶片(终端结点)数为 n0 ,分支度为 2 的结点数为 n2,则n0 = n2+1;
- 若完全二叉树具有 n 个结点,则这棵二叉树的深度为⌊log2n⌋ +1 1;
- 深度为 k 的二叉树至多总共有 2k-1 个结点(定义根结点所在深度 k0 = 0 )。
斜树:
所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。
特殊的二叉树:
- 完全二叉树
在一颗二叉树中,若除最后一层外的其余层都是满的,并且最后一层要么是满的,要么在右边缺少连续若干结点,则此二叉树为完全二叉树(Complete Binary Tree)。具有 n 个结点的完全二叉树的深度为 log2n +1。深度为 k 的完全二叉树,至少有 2k-1 个结点,至多有 2k-1个结点。
完全二叉树的特点:
- 叶子结点只能出现在最下层和次下层;
- 最下层的叶子结点集中在树的左部;
- 倒数第二层若存在叶子结点,一定在右部连续位置;
- 如果结点度为1,则该结点只有左孩子,即没有右子树;
- 同样结点数目的二叉树,完全二叉树深度最小;
- 满二叉树
一棵深度为 k,且有 2k-1 个结点的二叉树,称为满二叉树(Full Binary Tree)。这种树的特点是每一层上的结点数都是最大结点数。
满二叉树的特点:
- 所有叶子结点都集中在最底层;
- 非叶子结点的度一定为2;
- 在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
注意: 满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。
二叉树的存储结构
二叉树的存储方式有两种:顺序存储 和 链式存储。
- 顺序存储
顺序存储结构是指用一维数据存储二叉树中的结点,其中,数组的下标要能体现结点之间的逻辑关系,对于上图中的二叉树,其顺序存储结构为:
在上图的顺序存储结构中,“∧”表示的是该结点不存在,从顺序存储可以看出,若出现大量“∧”,则会对存储空间造成大量的浪费。
- 链式存储
由二叉树的定义可知,一个结点最多存在两个孩子结点,所以采用链式存储时一个结点存在两个指针域和一个数据域。
上述结构中,data 为数据域,Lchild 和 Rchild 为指针域,分别指向该结点的左孩子和右孩子。
链式存储结构定义
//待处理数据类型
ElemType ElemType char;
//链式存储结构定义
typedef struct BiTNode{
TElemType data;//结点数据
struct BiTNode *lchild;//指向左孩子
struct BiTNode *rchild;//指向右孩子
} BiTNode, *BiTree;
二叉树的遍历(traversing binary tree)
二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点使得每个结点被访问一次且仅被访问一次。
遍历方式的命名,源于其访问节点的顺序。最简单的划分:是考虑在遍历时采用 深度优先(先访问子节点,再访问父节点,最后是第二个子节点)?还是 广度优先(先访问第一个子节点,再访问第二个子节点,最后访问父节点)? 深度优先可进一步按照根节点相对于左右子节点的访问先后来划分。如果把左节点和右节点的位置固定不动,那么根节点放在左节点的左边,称为前序(pre-order)、根节点放在左节点和右节点的中间,称为中序(in-order)、根节点放在右节点的右边,称为后序(post-order)。对广度优先而言,遍历没有前序中序后序之分:给定一组已排序的子结点,其“广度优先”的遍历只有一种唯一的结果。
对于二叉树的遍历操作,主要分为:
- 前序遍历:根结点 —> 左子树 —> 右子树
- 中序遍历:左子树 —> 根结点 —> 右子树
- 后序遍历:左子树 —> 右子树 —> 根结点
- 层次遍历:从上到下、从左到右
前序遍历
若二叉树为空,则空操作返回,否则先访问根结点然后前序遍历左子树,再前序遍历右子树。
上图1-2中的二叉树前序遍历后输出为:A B D H I E C F G J
中序遍历
若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。
上图1-2中的二叉树中序遍历后输出为:H D I B E A F C J G
后序遍历
若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根结点。
上图1-2中的二叉树中序遍历后输出为:H I D E B F J G C A
层序遍历
若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。
上图1-2中的二叉树中序遍历后输出为:A B C D E F G H I J
二叉树遍历实现
宏定义及结构体
/******************选择哪一种遍历方式输出***************/
#define PREORDER_TRAVERSAL 1 //前序遍历二叉树
#define IN_ORDER_TRAVERSAL 0 //中序遍历二叉树
#define POST_ORDER_TRAVERSAL 0 //后序遍历二叉树
//待处理数据类型
typedef char ElemType;
//结点结构
typedef struct BiTNode
{
ElemType data;
struct BiTNode *lchild, *rchild;
}BiTNode,* BiTree;
函数实现
/**************************************
* 函数名称:CreatBiTree(BiTTree *T)
* 功能描述:递归的方式创建一棵二叉树,采用前序遍历的方式进行输入
* 传入参数:BiTree T 传入树
* 返回值:无
***************************************/
void CreatBiTree(BiTree *T)
{
ElemType c;
scanf("%c",&c);
if(' ' == c)
{
*T = NULL;
}
else
{
(*T) = (BiTNode *)malloc(sizeof(BiTNode));
(*T) ->data = c;
CreatBiTree(&(*T) -> lchild);
CreatBiTree(&(*T) -> rchild);
}
}
/**************************************
* 函数名称:PreorderTraverse(BiTree T, int level)
* 功能描述:遍历二叉树
* 传入参数:BiTree T 传入树
int level 传入层数
* 返回值:无
***************************************/
void PreorderTraverse(BiTree T, int level)
{
if(T)
{
#if PREORDER_TRAVERSAL//前序遍历二叉树
Visit(T ->data,level);
PreorderTraverse(T ->lchild, level+1);
PreorderTraverse(T ->rchild, level+1);
#elif IN_ORDER_TRAVERSAL//中序遍历二叉树
PreorderTraverse(T ->lchild, level+1);
Visit(T ->data,level);
PreorderTraverse(T ->rchild, level+1);
#elif POST_ORDER_TRAVERSAL//后序遍历二叉树
PreorderTraverse(T ->lchild, level+1);
PreorderTraverse(T ->rchild, level+1);
Visit(T ->data,level);
#endif
}
}
/**************************************
* 函数名称:Visit(ElemType T,int level)
* 功能描述:访问到数据后具体操作
* 传入参数:ElemType data 传入数据
int level 传入层数
* 返回值:无
***************************************/
void Visit(ElemType data,int level)
{
printf("%c ",data);
}
/**************************************
* 函数名称:main()
* 功能描述:主函数
* 传入参数:无
* 返回值:0
***************************************/
int main()
{
int level = 1;
BiTree T =NULL;
CreatBiTree(&T);
PreorderTraverse(T,level);
return 0;
}
🍟
🤒
🕒
⌊log2n⌋表示取下限,例如:3.7取下限为3。 ↩︎