1.树的存储结构有多种,既可以采用顺序存储结构,也可以采用链式存储结构,都要求能唯一地反映出树中各结点之间的逻辑关系,三种常用的存储结构
1)双亲表示法
采用一组连续空间来存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲节点在数组中的位置,根节点下标为0,其伪指针域为-1。
#define MAX_TREE_SIZE 100 //树中最多结点数
typedef struct{ //树的结点定义
ElemType data; //数据元素
int parent; //双亲位置域
}PTNode;
typedef struct{ //树的类型定义
PTNode nodes[MAX_TREE_SIZE]; //双亲表示
int n; //结点数
}PTree;
缺点是求结点的孩子时需要遍历整个结构
2)孩子表示法
是将每个结点的孩子都用单链表链接起来形成的一个线性结构,则N个结点就有N个孩子链表(叶子结点的孩子链表为空表),对于这种存储方式寻找子女的操作非常直接,而寻找双亲的操作需要遍历N个结点中孩子链表指针域所指向的N个孩子链表。
3)孩子兄弟表示法又称为二叉树表示法,即以二叉链表作为树的存储结构。孩子兄弟表示法是使每个结点包括三部分内容:结点值、指向结点第一个孩子结点的指针和指向结点下一个兄弟结点的指针(沿此域可以找到结点的所有兄弟结点):
存储结构:
typpedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
这种存储方式比较灵活,其最大的优点是可以方便地实现树转换为二叉树的操作,易于查找结点的孩子等,但缺点是从当前结点查找其双亲结点比较麻烦。如果为每个结点增设一个parent域指向其父结点,则查找结点的父结点也很方便。
2.树、森林与二叉树的转换
二叉树和树都可以用二叉链表作为存储结构,则以二叉链表作为媒介可以导出树与二叉树的一个对应关系,即给定一棵树,可以找到唯一的一棵二叉树与之对应。从物理结构上看,树的孩子兄弟表示法与二叉树的二叉链表表示法相同,即每个结点有两个指针,因此可以使用同一存储结构的不同解释将一棵树转换为二叉树。
树转换为二叉树的规则:每个结点的左指针指向它的第一个孩子结点,右指针指向它在树中的相邻兄弟结点,可表示为左孩子右兄弟,由于根结点没有兄弟,所以由树转换而得的二叉树没有右子树。森林转换先将每个子树转换,然后第一子树根结点作为转换树的根结点,其余子树的根结点在转换树中与第一棵子树的根结点为兄弟,二叉树转换为树或森林是唯一的
3.树和森林的遍历
树的先根遍历访问顺序与对应二叉树的先序遍历相同
树的后根遍历访问顺序与这棵树的中序遍历顺序相同
森林的先序遍历和中序遍历与二叉树的遍历相同
4.树的应用-----并查集
通常用树的双亲表示作为并查集的存储结构,每个子集合以一棵树表示,所有表示子集合的树,构成表示全集合的森林,存放在双亲表示数组内。通常用数组元素的下标代表元素名,根结点的下标代表子集合名,根结点的双亲结点为负数。初始化时,大集合S中每个结点都是根结点,他们的双亲域为-1.
为了得到两个子集的并,只要将其中一个子集合根结点的双亲指针指向另一个集合的根结点即可。
//并查集的结构定义如下:
#define SIZE 100
int UFSets[SIZE]; //集合元素数组,双亲指针数组
//并查集的初始化操作(S即为并查集)
void Init(int S[]){
for(int i = 0;i < SIZE;i++){
S[i] = -1;
}
}
//Find操作(函数在并查集中查找并返回包含元素x的树的根)
int Find(int S[], int x){
while(S[x] >= 0)
x=S[x];
return x;
}
//Union操作(求两个不相交子集合的名字)
void Union(int S[], int Root1, int Root2){
S[Root2] = Root1; //将根Root2连接到另一根Root1下面
}
5.树与二叉树的应用
1)二叉排序树(简称BST),也称为二叉查找树,要么为空,要么具有以下特性:
a)若左子树非空,则左子树上所有结点关键字值均小于根结点的关键字值
b)若右子树非空,则右子树上所有结点关键字值均大于根结点的关键字值
c)左右子树本身也分别是一棵二叉排序树
对二叉排序树进行中序遍历就可以得到一个递增的有序序列
2)二叉排序树的查找
从根结点开始,沿某一个分支逐层向下查找的过程
3)插入:二叉排序树是一种动态集合,树的结构不是一次性生成的,而是在查找过程中,当树中不存在关键字等于给定值的结点时再进行插入。
插入的新结点一定是一个叶结点
4)二叉树排序树的删除
5)二叉排序树的查找效率分析
对于高度为H的二叉排序树,其插入和删除的运行时间都是O(H).在最坏的情况下,构造二叉排序树的输入序列是有序的,则会形成一个倾斜的单支树,此时二叉排序树的性能显著变坏,树的高度也增加为元素个数N
等概率情况下,高度为10的单支树查找成功的平均查找长度为
ASL = (1+2+3+4+5+6+7+8+9+10)/10 = 5.5
如果二叉排序树的左右子树的高度之差得绝对值不超过1,这样的树称之为平衡二叉树。它的平均查找长度达到O(log2n);
查找过程看,二叉排序树和二分查找相似,但二分查找判定树唯一 , 二叉排序树不唯一,相同的关键字其插入顺序不同可能生成不同的二叉排序树。
如果有序表是静态查找表,宜用顺序表作为其存储结构,采用二分查找实现查找操作,如果是动态查找表(包含插入和删除),则应该选择二叉排序树作为其逻辑结构
6.平衡二叉树(AVL树)
平衡因子:定义左子树和右子树的高度差为该结点的平衡因子,则平衡二叉树的平衡因子的值只能为-1,0,1
保证平衡:每当在二叉排序树中插入(删除)一个结点时,首先要检查其插入路径上的结点是否因为此次操作而导致了不平衡, 如果导致了不平衡,则先找到插入路径上离插入结点最近的平衡因子绝对值大于1的结点A,再对以A为根的子树,在保持二叉排序树特性的前提下,调整各结点的位置关系,使之重新达到平衡。
注意:每次调整的对象都是最小不平衡子树,即在插入路径上离插入结点最近的平衡因子绝对值大于1的结点作为根的子树
7.哈夫曼树和哈夫曼编码
在许多实际应用中,树中结点常常被赋予一个表示某种意义的数值,成为该结点的权。从树根结点到任意结点的路径长度(经过的边数)与该结点上权值的乘积称为该结点的带权路径长度。树中所有叶结点的带权路径长度之和称为该树的带权路径长度WPL
在N个带权叶子结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称为最优二叉树。
构造哈夫曼过程中共新建了N-1个结点,因此哈夫曼树中结点总数为2N-1.且不存在度为1的结点
哈夫曼编码:频率高的字符赋予短编码,频率较低字符赋予长编码,起到压缩数据的效果。
如果没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。如0、101和100,
typedef int ElemType;
typedef struct BitNode {
ElemType data; //数据域
struct BitNode *lchild, *rchild; //左右孩子指针
}BitNode,*BitTree;
void visit(BitNode *b) {
printf("%d ", b->data);
}
//无论采用哪种遍历方法,时间复杂度都是O(n),因为每个结点都访问一次且仅访问一次,递归工作栈的栈深恰好为树的深度,空间复杂都为O(n)
//先序遍历根左右
void PreOrder(BitTree T) {
if (T != NULL) {
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
//中序遍历左根右
void InOrder(BitTree T) {
if (T != NULL) {
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
//后序遍历左根右
void PostOrder(BitTree T) {
if (T != NULL) {
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
//递归算法转换为非递归,
void Inorder2(BitTree T) {
stack<BitTree> s; BitTree p = T;
while (p != NULL || !s.empty()) { //栈不空时或p不空时循环
if (p) {
s.push(p);
p = p->lchild;
}
else {
p = s.top; s.pop(); //弹出时,p没有左指针
visit(p);
p = p->rchild;
}
}
}
//二叉树层次遍历,借助队列
void LevelOrder(BitTree T) {
queue<BitTree> q; BitTree p = T;
q.push(p);
while (!q.empty) {
p = q.front; q.pop();
visit(p);
if (p->lchild)
q.push(p->lchild); //左子树不空,则左子树入队
if (p->rchild)
q.push(p->rchild);
}
}
注意:由遍历序列构造二叉树,由二叉树的先序序列和中序序列,后续序列和中序序列,层序序列和中序序列可以唯一地确定一棵二叉树,如果只知道二叉树的先序序列和后序序列则无法唯一确定一棵二叉树
线索二叉树:
#include <cstdlib>
#include <cstdio>
#include <iostream>
#include <stack>
#include <queue>
using namespace std;
typedef int ElemType;
typedef struct ThreadNode {
ElemType data; //数据域
struct ThreadNode *lchild, *rchild; //左右孩子指针
int ltag, rtag; //左右线索标志,0,孩子结点,1为指向前驱或者后继结点
}ThreadNode, *ThreadTree;
//线索二叉树的构造,遍历过程中检查当前结点左右指针域是否为空,若为空,将它们改为指向前驱结点或者后驱结点的线索
//中序遍历二叉树线索化
void InThread(ThreadTree &p, ThreadTree &pre) {
if (p != NULL) {
InThread(p->lchild, pre); //递归,线索化左子树
if (p->lchild == NULL) { //左子树为空,建立前驱线索
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL&&pre->rchild == NULL) {
pre->rchild = p; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = p; //标记当前结点为刚刚访问过的结点
InThread(p->rchild, pre); //递归线索化右子树
}
}
void CreateInthread(ThreadTree T) {
ThreadTree pre = NULL;
if (T != NULL) {
InThread(T, pre); //非空二叉树,线索化
pre->rchild = NULL; //处理遍历后的最后一个结点
pre->rtag = 1;
}
}
//线索二叉树的遍历,这种遍历不再借助栈
//求中序线索二叉树中序序列下的第一个结点,不一定是叶子结点
ThreadNode *FirstNode(ThreadNode *p) {
while (p->ltag == 0) p = p->lchild;
return p;
}
//中序线索二叉树中结点p在中序序列下的后继结点
ThreadNode *NextNode(ThreadNode *p) {
if (p->rtag == 0) return FirstNode(p->rchild);
else return p->rchild; //rtag==1直接返回后继线索
}
//最后一个结点
ThreadNode *GetLast(ThreadNode *p) {
while(p->rtag == 0) p = p->rchild;
return p;
}
//不含头结点的中序遍历二叉树算法
void InOrder(ThreadNode *T) {
for (ThreadNode *p = FirstNode(T); p != NULL; p = NextNode(p))
visit(p);
}
void visit(ThreadNode *b) {
printf("%d ", b->data);
}