树的相关概念
-
树是 n 个结点的有限集,它为空树或非空树,对于非空树 T
①有且仅有一个称之为根的结点
②除根结点之外的其余结点可分为 m 个互不相交的有限集 T1, T2…Tm,其中每个集合本身又是一棵树,并且称之为根的字数(SubTree) -
树的基本术语
①结点的度:结点拥有的子树个数成为结点的度
②树的度:树内各结点度的最大值
③层次:结点的层次从根开始定义起,根为第一层,根的孩子为第二层,任一结点的层次为其双亲结点的层次加 1
④树的深度:树中结点的最大层次称为树的深度或高度
⑤森林:是 m 根互不相交的树的集合 -
就逻辑结构而言
任一一颗树都是一个二元组 Tree = ( root , F ) ,其中 root 是树的根结点,F 是 m 棵树的森林,F = ( T1, T2…Tm ),其中 Ti = ( ri,Fi ) 为根 root 的第 i 棵子树 -
树的存储结构
孩子兄弟法,链表中包含三个域,数据域和两个链域,两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点,分别命名为 firstchild 域和 nextsibling 域。这样,要访问结点 x 的第 i 个孩子,则只要先从 firstchild 域找到第 1 个孩子结点,然后沿着孩子结点的 nextsibling 域连续走 i-1 步,便可找到 x 的第 i 个孩子
/* 树的二叉链表(孩子-兄弟)表示法 */
typedef struct CSNode
{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
二叉树
-
二叉树(Binary Tree)是 n 个结点的有限集,它为空树或非空树,对于非空树 T
①有且仅有一个称之为根的结点
②除根结点之外的其余结点可分为两个互不相交的有限集 T1, T2,分别称为 T 的左子树和右子树,而 T1,T2 本身又都是二叉树 -
二叉树的性质
①第 i 层上最多有 2i-1 个结点
②深度为 k 二叉树至多有 2k - 1 个结点
③对于任一一棵而二叉树,如果其终端结点数(叶子结点)为 n0,度为 2 的结点数为 n 2,则 n0 = n2 + 1
④满二叉树/完美二叉树:深度为 k 且含有 2k - 1 个结点的树,即每一层 i 的结点数都是最大值 2i-1
⑤完全二叉树:深度为 k 的,有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 k 的满二叉树中编号从 1 至 n 的结点一一对应时,称之为完全二叉树 -
完全二叉树的特点
①叶子结点只可能在层次最大的两层上出现,且对任一结点,若其右分支下的子孙的最大层次为 l ,则其左分支下的子孙的最大层次必为 l 或 l + 1
②具有 n 个结点的完全二叉树的深度为 [log2n] + 1
③如果对任一一棵有 n 个结点的完全二叉树的结点按层序编号,则对于任一结点 i ,有:(1)若 i = 1,则结点 i 为二叉树的根,无双亲,否则其双亲结点为 i / 2 (2) 若 2i > n,则结点 i 无左孩子,否则其左孩子是 2i (3) 若 2i+1>n,则结点 i 无右孩子,否则其右孩子是 2i+1 -
二叉树的存储结构
①顺序存储结
使用一组连续的存储单元来存储数据元素。
对于完全二叉树,只要从根结点起按层序存储即可,将编号为 i 的结点存储在如上定义的一维数组中下标为 i-1 的分量中(双亲和孩子结点可由完全二叉树的特点得)
对于一般二叉树,则应将其上每一个结点与完全二叉树上的结点相对照,存储在一维数组的相应分量中(但会造成存储空间的极大浪费,例如深度为 k 的且只有 k 个结点的单支树却需要长度为 2k-1 的一维数组)
②链式存储结构
结点由一个数据元素和指向其左、右子树的两个分支构成,因此至少包含 3 个域,数据域和左、右指针域
/* 顺序存储结构 */
#define MAXSIZE 100
typedef TElemType SqBiTree[MAXSIZE];
SqBiTree bt;
/* 链式存储结构 */
typedef struct BiTNode
{
TElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
二叉树的遍历及应用
- 二叉树的遍历
①前、中、后序遍历的递归算法
②前、中、后序遍历的非递归算法
③层 次 遍 历
/* 中序遍历的递归算法 */
void InOrderTraverse(BiTree T)
{
if(T)
{
InOrderTraverse(T->lchild); /* 中序遍历左子树 */
printf("%d",T->data);
InOrderTraverse(T->rchild); /* 中序遍历右子树 */
}
}
/* 中序遍历的非递归算法 */
void InOrderTraverse(BiTree T)
{
InitStack(S); /* 初始化一个空栈 S */
BiTree p=T; /* 指针 p 指向根结点 */
BiTNode q=(*BiTNode)malloc(sizeof(BiTNode)); /* 用于存放栈顶弹出的元素 */
while( p || !StackEmpty(S) )
{
if(p)
{
Push(S,p);
p=p->lchild; /* 遍历访问左子树 */
}
else
{
q=Pop(S);
printf("%d",q->data);
p=*斜体样式*q->rchild; /* 遍历右子树 */
}
}
}
/* 层序遍历 */
void LevelInOrderTraverse (BiTree BT)
{
Queue Q;
BinTree T;
if (BT == NULL) return; /* 若是空树则直接返回 */
Q = CreatQueue(); /* 创建空队列Q */
AddQ( Q, BT );
while ( !IsEmpty(Q) )
{
T = DeleteQ( Q );
printf("%d ", T->Data); /* 访问取出队列头的结点 */
if ( T->Left ) AddQ( Q, T->Left );
if ( T->Right ) AddQ( Q, T->Right );
}
}
- 根据遍历序列确定二叉树
①由二叉树的先序和中序序列
②由二叉树的后序和中序序列
- 二叉树遍历的应用
①建立二叉链表
[1]扫描字符序列,读入字符 ch
[2]如果字符是一个 “#” 字符,则表明该二叉树为空树,T 为 NULL,否则:
(1)申请一个结点空间 T,将 ch 赋给 T->data
(2)递归创建 T 的左子树
(3)递归创建 T 的右子树
/* 先序遍历建立二叉链表 */
void CreateBiTree(BiTree &T)
{
scanf("%c",&ch);
if(ch=='#') T=NULL; /* 递归结束,创建空树 */
else
{
T=(BiTree)malloc(sizeof(BiTNode));
T->data=ch;
CreateBiTree(T->lchild); /* 递归创建左子树 */
CreateCitree(T->rchild); /* 递归创建右子树 */
}
}
②复制二叉链表
[1]如果是空树,递归结束
[2]否则:
(1)申请一个结点空间,复制根结点
(2)递归复制左子树
(3)递归复制右子树
/* 先序复制二叉树 */
void Copy(BiTree T,BiTree &NewT)
{
if(T == NULL) NewT = NULL; /* 空树则递归结束 */
else
{
NewT=(BiTree)malloc(sizeof(BiTNode));
NewT->data=T->data;
Copy(T->lchild,NewT->lchild); /* 递归复制左子树 */
Copy(T->rchild,NewT->rchild); /* 递归复制右子树 */
}
}
③计算二叉树的深度
[1]如果是空树,递归结束,深度为 0
[2]否则:
(1)递归计算左子树、右子树的深度,记为 m、n
(2)如果 m>n,二叉树的深度记为 m+1,否则记为 n+1
/* 计算二叉树 *T* 的深度 */
int Depth(BiTree T)
{
if(T == NULL) return 0; /* 空树则递归结束 */
else
{
m=Depth(T->lchild); n=Depth(T->rchild); /* 递归计算左、右子树的深度 */
return (m>n)?m+1:n+1;
}
}
④统计二叉树的结点个数
[1]如果要统计叶结点、度为 1、度为 2 的结点个数,则需要添加 if 条件判断语句
/* 统计二叉树 *T* 的结点个数 */
int NodeCount(BiTree T)
{
if(T == NULLm) return 0; /* 空树则结点数为 *0* */
else
return NodeCount(T->lchild)+NodeCount(T->rchild)+1; /* 总数为左、右子树结点个数+1*/
}
⑤求解表达式的值
构建表达式树:表达式树中的叶子结点均为操作数,分支结点均为运算符
[1]先初始化 OPTR 和 EXPT 栈,将表达式起始符 “#” 压入 OPTR 栈
[2]扫描表达式,读入第一个字符 ch ,如果表达式没有扫描完毕或 OPTR 的栈顶元素不为 “#” 时,则:
(1)若 ch 为运算数,则以 ch斜体样式 为根创建一棵只有根结点的二叉树,且将该根结点压入 EXPT 栈,读入下一字符 ch
(2)若 ch 是运算符,则根据 OPTR 栈顶元素和 ch 的优先级比较结果,作不同处理: 若是 小于 ,则 ch 压入 OPTR 栈,继续读入下一字符;若是 大于 ,弹出 OPTR 栈顶的运算符,再从 EXPT 栈弹出两个两个表达式子树的根结点,以运算符为根结点,弹出的第二个子树为左子树,第一个子树为右子树,创建一个新的二叉树,并将该树根结点插入 EXPT 栈; 若是 等于,则栈顶元素怒为 “(” 且 ch 为")",这时弹出 OPTR 栈顶元素,相当于括号匹配成功,然后读入下一字符 ch
void InitExpTree()
{
InitStack(OPTR); InitStack(EXPT);
Push(OPTR,'#');
scanf("%c",&ch);
while(ch!='#'||GetTop(OPTR)!='#')
{
if(!Is(ch)) /* ch不是运算符 */
{
CreateExpTree(T,NULL,NULL,ch);
Push(EXPT,T);
scanf("%c",&ch); /* 读入下一字符 */
}
else
{
switch(Precede(GetTop(OPTR),ch) /* 比较 ch 和栈顶元素优先级 */
{
case '<':
Push(OPTR,ch); scanf("%c",&ch);
break;
case '>':
Pop(OPTR,theta);
Pop(EXPT,b); Pop(EXPT,a);
CreateExpTree(T,a,b,theta);
Push(EXPT,T);
break;
case '=':
Pop(OPTR,x); scanf("%c",&ch);
break;
}
}
}
}
[1]设变量 lvalue 和 rvalue 分别用以记录表达式树左子树和右子树的值,初始为 0
[2]如果当前为叶子结点,则返回该结点的数值,否则:
(1)递归计算左子树的值记为 lvalue
(2)递归计算右子树的值记为 rvalue
(3)根据当前结点运算符的类型,将 lvalue 和 rvalue 进行相应运算并返回
int EvaluateExpTree(BiTree T)
{
lvalue=rvalue=0;
if(T->lchild==NULL && T->rchild==NULL)
return T->data-'0'; /* 结点为操作数返回其数值 */
else
{
lvalue=EvalueteExpTree(T->lchild);
rvalue=EvalueteExpTree(T->rchild);
return GetValue(T->data,lvalue,rvalue); /* 根据当前运算符进行相应类型的计算 */
}
}
哈夫曼树及哈夫曼编码
- 哈夫曼树(Huffman)
哈夫曼树又称为最优树,是一类带权路径长度最短的树
①路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径
②路径长度:路径上的分支数目称作路径长度
③树的路径长度:从树根到每一结点的路径长度之和
④结点的带权路径长度:从该结点到树根之间的路径长度与结点上的权的乘积
⑤树的带权路径长度:树中所有叶子结点的带权路径长度之和,记为 WPL = ∑wklk
⑥哈夫曼树:假设有 m 个权值 { w1,w2,…,wm },可以构造一棵含 n 个叶子结点的二叉树,每个叶子结点的权值为 wk,则其中带权路径长度 WPL 最小的二叉树称作最优二叉树或哈夫曼树
- 哈夫曼树的构造算法
①根据给定的 n 个权值{ w1,w2,…,wn },构造 n 棵只有根结点的二叉树,这 n 棵二叉树构成一个森林 F
②在森林 F 中选取两棵根结点权值最小的树作为左、右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为左、右子树上根结点的权值之和
③从森林 F 中删除这两棵树,同时将新得到的二叉树加入 F 中
④重复②、③步骤,直到 F 只含一棵树为止,这棵树便是哈夫曼树
- 哈夫曼树算法的实现
哈夫曼树是一种二叉树,当然可以用前面介绍的通用存储方法,而由于哈夫曼树没有度为 1 的结点,则一棵有 n 个叶子结点的哈夫曼树一共有 2n-1 个结点,可以存储在大小为 2n-1 的一维数组中。树中的每个结点还要包含其双亲信息和孩子结点的信息
哈夫曼树的各结点存储在由 HuffmanTree 定义的动态分配数组中,为了实现方便,数组的 0 号单元不使用,从 1 号单元开始使用,所以数组的大小定义为 2n
将叶子结点集中存储在前面 1 - n 的部分,而后面 n-1 个位置存储其余非叶子结点
/* 哈夫曼树的存储表示 */
typedef struct{
int weight; /* 结点权值 */
int parent,lchild,rchild; /* 结点的双亲、左、右孩子的下标 */
}HTNode,*HuffmanTree;
①初始化:首先动态申请 2n 个单元,然后循环 2n-1 次,从 1 号单元起,依次将 1 到 2n-1 所有单元的双亲和孩子结点下标都初始化为 0,最后再循环 n 次,输入前 n 个单元中叶子结点的权值
②创建树:循环 n-1 次,通过 n-1 次的选择、删除与合并来创建哈夫曼树
[1]选择:从当前树中选择双亲为 0 且权值最小的两个树根结点 s1 和 s2
[2]删除:将结点 s1 和 s2 的双亲改为非 0
[3]合并:将 s1 和 s2 的权值的和作为一个新结点的权值依次存入到数组的第 n+1 之后的单元中,同时记录这个新结点左孩子的下标 s1,右孩子的下标 s2
/* 哈夫曼树的构造 */
void CreateHuffmanTree( HuffmanTree &HT,int n )
{
if(n<=1) return ;
m = 2*n-1;
HuffmanTree HT = (HuffmanTree)malloc((m+1)*sizeof(HTNode));
/* 初始化 */
for(i=1;i<=m;i++)
HT[i].parent = HT[i].lchild = HT[i].rchild = 0;
for(i=1;i<=n;i++)
scanf("%d",&HT.weight); /* 输入前 n 个单元,即叶子结点的权值 */
/* 创建树 */
for(i=n+1;i<=m;i++)
{
Select(HT,i-1,s1,s2); /* 选择 */
HT[s1].parent = HT[s2].parent = i; /* 删除 */
HT[i].weight = HT[s1].weight + HT[s2].weight; /* 合并 */
HT[i].lchild = s1; HT[i].rchild = s2;
}
}
- 哈夫曼编码
在进行数据压缩时,为了使压缩过后的数据文件尽可能的短,可采用不定长编码,其基本思想是:为出现次数较多的字符编以较短的编码
为确保对数据文件进行有效的压缩和对压缩文件进行正确的解码,可利用哈夫曼树来设计二进制编码,约定树中左分支标记为 0 ,右分支标记为 1 ,则根结点到每一个叶子结点路径上的 0、1 序列即为相应字符的编码(哈夫曼编码)
为防止解码时的二义性,采用前缀编码对文件进行压缩
哈夫曼编码是前缀编码且是最优前缀编码:对于包括 n 个字符的数据文件,分别以它们的出现次数为权值构造哈夫曼树,则利用该树对应的哈夫曼编码对文件进行编码,能使该文件压缩后对应的二进制文件的长度最短
- 哈夫曼编码的算法实现
在构造哈夫曼树之后,求哈夫曼编码的主要思想是:依次以叶子为出发点,向上回溯至根结点为止,回溯时走左分支则生成代码 0 ,走右分支则生成代码 1
由于每个哈夫曼编码都是变长编码,因此使用一个指针数组来存放每个字符编码串的首地址,各字符的哈夫曼编码存储在由 HuffmanCode 定义的动态分配的数组 HC 中,为了实现方便,数组的 0 号单元不使用,从 1 号单元开始使用,所以数组的大小为 n+1 ,即哈夫曼编码表包括 n+1 行
但由于每个字符编码的长度事先不能确定,所以不能预先为每个字符分配大小合适的存储空间。 为了避免浪费存储空间,动态分配一个长度为 n (字符编码长度一定小于 n)的一维数组 cd,用来临时存储当前正在求解的第 i 个字符的编码,该字符求解完毕后,根据数组 cd 的字符串长度分配 HC[i] 的空间,然后将数组 cd 中的编码复制到 HC[i] 中
因为求解编码时是从哈夫曼树的叶子结点出发,向上回溯至根结点,所以对于每一个字符,得到的编码顺序都是从右向左的,故将编码向数组 cd 中存放的顺序也是从后向前的,即每个字符的第一个编码存放在 cd[n-2] 中(cd[n-1]存放字符串结束标志 ‘\0’),第二个字符存放在 cd[n-3] 中,依此类推,直到该字符编码全部存放完毕
/* 哈夫曼编码的存储表示 */
typedef char **HuffmanCode; /* 动态分配数组存储哈夫曼编码 */
算法步骤
①分配存储 n 个字符编码的编码表空间 HC,长度为 n+1,分配临时存储每个字符编码的动态数组空间 cd,cd[n-1] 置为 ‘\0’
②逐个求解 n 个字符的编码,循环 n 次,执行以下操作:
·设置变量 start 用于记录编码在 cd 中存放的位置,start 初始指向最后,即编码结束符的位置 n-1
·设置变量 c 用于记录从叶子结点向上回溯至根结点所经过的结点下标, c 初始时为当前待编码字符的下标 i, f 用于记录记录 i 的双亲结点的下标
·从叶子结点向上回溯至根结点,求得字符 i 的编码,当 f 没有达到根结点时,循环执行以下操作
>回溯一次 start 向前指一个位置,即 start- -
>若结点 c 是 f 的左孩子,则生成代码 0 ,否则生成代码 1,生成的代码保存在 cd[start] 中
>继续向上回溯,改变 c 和 f 的值
·根据数组 cd 的字符串长度为第 i 个字符分配空间 HC[i],然后将数组 cd 中的编码复制到 HC[i] 中
③释放临时空间 cd
/* 求哈夫曼编码 */
void CreateHuffmanCode( HuffmanTree HT, HuffmanCode &HC, int n )
{
/* 从叶子到根逆向求解每个字符的哈夫曼编码,存储在编码表 HC 中 */
HC = (HuffmanCode)malloc((n+1)*sizeof(char*));
/* 分配临时存放每个字符编码的动态数组空间 */
char* cd = (char*)malloc(n*sizeof(char)); cd[n-1] = '\0';
/*逐个字符求解哈夫曼编码 */
for(int i=1;i<=n;i++)
{
start = n-1;
c = i; f = HT[i].parent; /* f指向 c的双亲结点 */
/* 从叶子结点向上回溯,直到根结点 */
while( f != 0 )
{
start - -; /* 每回溯一次, start 前移一个位置 */
if(HT[f].lchild == c) cd[start] = '0'; /* 左孩子,生成代码 0 */
else cd[start] = '1'; /* 右孩子,生成代码 1 */
c = f; f = HT[c].parent; /* 继续向上回溯 */
}
HC[i] = (char*)malloc((n-start)*sizeof(char));
strcpy(HC[i],&cd[start]);
}
free(cd); /* 释放临时空间 */
}