数据结构第五章——树和二叉树
树的定义
树(Tree)是n(n>=0)个结点的有限集
若n = 0,称为空树
若n>0,则它满足以下两个条件:
- 有且仅有一个特定的称为根
- 其余节点可分为m(m>0)个互不相交的有限集T1,T2,T3,……,Tm,其中每一个集合本身又是一棵树,并称为根的子树(SubTree)
根结点:非空树中无前驱结点的结点
结点的度:结点有用的子树数
树的度:树内各结点度的最大值
树的深度:树的结点的最大层次
有序树
树中结点的各子树从左只有有次序(最左表单为第一个还在)
无序树
树中结点的各子树无次序
森林
是m(m>=0)棵互不相交的树的集合
一棵树可以看作一个特殊的森林
树一定是森林,森林不一定是树
二叉树的定义
二叉树是n(n>=0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两棵互不相交的分别称做这个根的左子树和右子树的二叉树组成
特点
- 每个结点最多有两个还是(二叉树中不存在度大于2 的结点)
- 子树有左右之分,其次序不能颠倒
- 二叉树可以是空集合,根可以有空的左子树或空的右子树‘
二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要进行区分,说明它是左子树,还是右子树
树当结点只有一个孩子时,就无须区分它是左还是右的次序
二叉树的性质和存储结构
-
在二叉树的第i层至多有2^(i-1)个结点(i>=1)
第i层上至少有1个结点 -
深度为k的二叉树至多有2^k - 1个结点(k>=1)
深度为k时至少有k
-
对任何一棵二叉树T,如果其叶子数为n0,度为2的节点数为n2,则n0 = n2+1
满二叉树
一棵深度为k且有2^k -1个结点的二叉树称为满二叉树
特点:
- 每层上的结点数都是最大结点数(即每层都满)
- 叶子结点全部在最底层
完全二叉树
深度为k的具有n个结点的二叉树,当且仅当每一个结点都与深度为k的满二叉树的编号1~n意义对应时,称之为完全二叉树
注:在满二叉树中,从最后一个结点开始,连续去掉任意个结点,即是一棵完全二叉树。(一定要是连续去掉)
特点:
- 叶子只可能分布在层次最大的两层上
- 对任一结点,如果其右子树的最大层次为i
完全二叉树的性质
- 具有n个结点的完全二叉树的深度为log2n + 1
- 如果对一棵有n个结点的完全二叉树(深度为log2n + 1)的结点按层序编号,则对任一结点i,有
- 如果i = 1,则i是二叉树的根,无双亲;如果i>1,则其双亲结点i/2
- 如果2i>n,则结点i为叶子结点,无左孩子,否则左孩子是结点2i
- 如果2i+1>n,则结点i无右孩子,否则,其右孩子是结点2i+1
二叉树的顺序存储
实现;按满二叉树的结点层次编号,依次存放二叉树中的数据元素
#define MAXSIZE 100
typedef int SqBiTree[MAXSIZE]
SqBiTree bt;
二叉树顺序存储的缺点:
最坏情况:深度为k的且只有k个结点的单支树需要长度为2^k - 1的一维数组
结点间关系蕴含在其存储位置中浪费空间,适合存满二叉树和完全二叉树
二叉树的链式存储结构
typedef struct BiNode{
int data;
struct BiNode *lchild, *rchild;//左右孩子指针
}BiNode,*BiTree;
三叉链表
typedef struct TriTNode{
int data;
struct TriTNode *lchild, *parent, *rchild;
}TriTNode, *TriTree;
遍历二叉树
顺着某一条搜索路径寻访二叉树中的结点,使得每个结点被访问一次,而且仅被访问一次
先序遍历:根左右
中序遍历:左根右
后序遍历:左右根
由先序和中序或中序和后序可以确立一个二叉树
二叉先序遍历算法
Status PreOrderTraverse(BiTree T){
if(T==NULL) return OK;//空二叉树
else
{
vist(T);//访问根结点;
PreOrderTraverse(T->lchild)
PreOrderTraverse(T->rchild)
}
}
void Pre(BiTree *T){
if(T!= NULL)
{
cout << T->data;
pre(T->lchild);
pre(T->rchild)
}
}
//中序遍历算法
Status InOrderTraverse(BiTree T){
if(T==NULL)return OK;
else
{
InOrderTraverse(T->lchild);
visit(T);
InOrderTraverse(T->rchild)
}
}
//后序遍历算法
Status PostOrderTraverse(BiTree T){
if(T==NULL) return OK;
else
{
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
visit(T)
}
}
遍历二叉树的非递归算法
二叉树中序遍历的非递归算法的关键:在中序遍历过某个结点的整个左子树后,如何找到该节点的根以及右子树
基本思想
- 建立一个栈
- 根结点进栈,遍历左子树
- 根结点出栈,遍历根结点,遍历右子树
Status InOrderTraverse(BiTree T){
BiTree p;
InitStack(S);
p = T;
while(p || !StackEmpty(S)){
if(p){
Push(S,p);
p=p->lchild;
}
else
{
Pop(S,q);
cout << q->data;
p = q->left
}
return OK;
}
}
二叉树的层次遍历
对于一棵二叉树,从根结点开始,按从上到下,从左到右的顺序访问每一个结点。
每一个结点仅仅访问一次。
算法思路
- 从根结点进队
- 队不空时循环:从队列中出列一个结点*p,访问它;
- 若它有左孩子结点,将左孩子结点进队
- 若它有右孩子结点,将右孩子结点进队。
//使用队列类型定义如下:
typedef struct{
BTNode data[Maxsize];//存放队中元素
int front, rear;//队头和队尾指针
}SqQueue;//顺序循环队列类型
void LevelOrder(BTNode *b){
BTNode *p;
SqQueue *qu;
InitQueue(qu);//初始化队列
enQueue(qu, b);//根结点指针进入队列
while(!QueueEmpty(qu)){//队不空,则循环
deQueue(qu, p);//对队结点p
cout << p->data << endl;
if(p->lchild != NULL)
enQueue(qu, p -> lchild);//有左孩子时将其进队
if(p->rchild != NULL)
enQueue(qu, p->rchild);//有右孩子时将其进队
}
}
Status CreateBiTree(BiTree &T){
cin >> ch;
if(ch == '#') T = NULL;
else{
if(!T=(BiTNode *) malloc(sizeof(BiTNode)))
exit(OVERFLOW);
T->data = ch;
CreateBiTree(T->lchild);//构造左子树
CreateBiTree(T->rchild);//构造右子树
}
return OK;
}
二叉树遍历算法的应用——复制二叉树
int Copy(BiTree T, BiTree &NewT){
if(T==NULL){
NewT = NULL;
return 0;
}
else
{
NewT = new BiTNode;
NewT -> data = T-> data;
Copy(T->lchild, NewT->lchild);
Copy(T->rchild, NewT->rchild);
}
}
二叉树遍历算法的应用——计算二叉树深度
如果是空树,则深度为0。
否则,递归计算左子树的深度记为m,递归计算右子树的深度记为n,二叉树的深度则为m与n 的较大者+1。
int Depth(BiTree T){
if(T==NULL) return 0;//如果是空树返回0
else
{
m = Depth(T->lchild);
n = Depth(T->rchild);
if(m > n) return m + 1;
else return n + 1;
}
}
二叉树遍历算法的应用——计算二叉树结点总数
如果是空树,则结点个数为0。
否则,结点个数为左子树的结点个数+右子树的结点个数再+1
int NodeCount(BiTree T){
if(T==NULL)
return 0;
return NodeCount(T->lchild) + NodeCount(T->rchild) + 1;
}
二叉树遍历算法的应用——计算叶子结点的个数
如果是空时,则叶子结点个数为0;
否则,为左子树的叶子节点数 + 右子树的叶子结点个数
int leadCount(BiTree T){
if(T==NULL)
return 0;
if(T->lchild == NULL && T->rchild == NULL)
return 1;
else
return LeafCount(T->lchild) + leaveCount(T->rchild);
}
线索二叉树
利用二叉链表中的空指针域:
如果某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱;如果某结点的右孩子为空,则将空的右孩子阵雨改为指向其后继。——这种改变指向的指针称为“线索”。
为区分lchild和rchild指针到底是指向孩子的指针还是指向前驱或者后继的指针,对二叉链表中每个结点增设两个标志域ltag和rtag,并约定
ltag = 0;lchild指向该结点的左孩子
ltag = 1;lchild指向该节点的前驱
rtag =0;rtag指向该结点的右孩子
rtag = 1rtag指向该结点的后继
增设一个头结点
Itag = 0, lchild指向根结点
rtag = 1,rchild指向遍历序列的最后一个结点
遍历序列中第一个结点的lc域和最后一个结点的rc域都指向头结点
树的存储结构
双亲表示法
特点:找双亲容易,找孩子难
typedef struct PTNode{
TEelemType data;
int parent;//双亲位置域
}PTNode;
#define MAX_TREE_SIZE 100
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int r, n;//根结点的位置和结点个数
}PTree;
孩子链表
吧个结点的孩子结点排列起来,看成是一个线性表,用单链表存储。则n个结点有n个孩子链表(叶子的孩子链表为空表)。而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储
typedef struct CTNode{
int child;
struct CTNode *next;
}*ChildPtr;
typedef struct{
TElemType data;
ChildPtr firstchild;
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n, r;//结点数和根结点的位置
}CTree;
特点:找孩子容易,找双亲难
孩子兄弟表示法(二叉树表示法,二叉链表表示法)
实现:用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子和下一个兄弟结点
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild, *nextsibling;
}CSNode, *CSTree;
树与二叉树的转换
将树转化为二叉树进行处理,利用二叉树的算法来实现对树的操作
由于树和二叉树都可以用二叉链表作存储结构,则二叉链表作媒介可以导出树与二叉树之间的一个对应关系
将树转化为二叉树:(兄弟相连留长子)
- 加线:在兄弟之间加一连线
- 去线:对每个结点,除了左孩子外,去除与其余孩子之间的关系
- 旋转:以树的根结点为轴心,将整树顺时针旋转45°
将二叉树转化为树(左孩右右连双亲,去掉原来右孩线)
- 加线:若p结点的双亲结点的左孩子,则将p的右孩子,右孩子的右孩子……沿分支找到的所有右孩子,都与p的双亲用线连起来
- 抹线:抹掉原二叉树中双亲与右孩子之间的连线
- 调整,将结点按层次排列,形成树的结构
森林转换为二叉树(二叉树与多棵树之间的关系)
- 将各棵树转换成二叉树
- 将每棵树的根结点用线相连
- 以每一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树型结构。
二叉树转换成森林
- 抹线:将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树
- 还原:将孤立的二叉树还原成树
树和森林的遍历
树的遍历(三种方式)
- 先根(次序)遍历:
若树不空,则先访问根结点,然后依次先根遍历各棵子树 - 后根(次序)遍历:
若树不空,则先依次后根遍历各棵子树,然后访问根结点 - 按层次遍历:
若树不空,则自上而下自左至右的访问树中每个结点。
森林的遍历
将森林看作由三部分构成
- 森林中第一棵树 的根结点
- 森林中第一棵树的子树森林
- 森林中其它树构成的森林
先序遍历:
若森林不空,则:
- 访问森林中第一棵树的根结点
- 先序遍历森林中第一棵树的子树森林
- 先序遍历森林中(除第一棵树之外)其余树构成的森林
即:一次从左至右对森林中的每一棵树进行先根遍历。
中序遍历
若森林不空,则
- 中序遍历森林中第一棵树的子树森林
- 访问森林中第一棵树的根结点
- 中序遍历森林中(除第一棵树之外)其余树构成的森林。
即:依次从左至右对森林中每一棵树进行后根遍历。
哈夫曼树
哈夫曼树的基本概念
路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径。
结点的路径长度:两结点间路径上的分支数。
树的路径长度:从树根到每一个结点的路径长度之和。
权(weight):将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。
树的带权路径长度:树中所有叶子结点的带权路径长度之和。
哈夫曼树:带权路径长度最短的树
哈夫曼树中权越大的叶子离根越近
哈夫曼算法
- 根据n个给定的权值构成n棵二叉树的森林,其中Ti只有一个带权为wi的根结点
- 在F中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
- 在F中删除这两棵树,同时将新得到的二叉树加入森林中。
- 重复2 和 3 ,直到森林中只有一棵树位置,这棵树记为哈夫曼树。
哈夫曼算法口诀:
- 构造森林全是根
- 选用两小造新树
- 删除两小添新人
- 重复23省单根。
哈夫曼树构造算法的实现
采用顺序存储结构——以为结构数组
typedef struct{
int weight;
int parent, lch, rch;
}HTNode, *HuffmanTree;
1、初始化HT[1……2n-1]:lch = rch = parent = 0
2、输入初始n个叶子结点:置HT[1……n]的weight值
3、进行一下n-1次合并,依次产生n-1个结点HT[i],i = n + 1
void CreateHuffmanTree(HuffmanTree HT, int n){
if(n<=1) return;
int m = 2 * n - 1;//数组共2n-1个元素
HT = new HTNode[m+1];//0号单元未用,HT[m]表示根结点
for(int i = 1; i <= m; i++){
HT[i].lch = 0;
HT[i].rch = 0;
HT[i].parent = 0;
}
for(int i = 1; i <= n; i++)
cin >> H[i].weight;//输入前n个元素的weight值
for(int i = n + 1; i <= m; i++){//合并产生n-1个结点
Select(HT, i -1, s1, s2);//在HT[k](1<=k<=i-1)中选择两个其双亲域为0,且权值最小的结点,并返回它们在HT中的序号s1和s2
HT[s1].parent = i;
HT[s2].parent = i;
HT[i].lch = s1;
HT[i].rch = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;//i的权值为左右孩子权值之和
}
}
//初始化结束,下面开始建立哈夫曼树
为什么哈夫曼编码能够保证是前缀编码?
因为没有一片输液是另一片树叶的主线,所以每个叶结点的编码就不可能是其他叶结点编码的前缀
为什么哈夫曼编码能保证字符编码总长度最短
因为哈夫曼树的带权路径长度最短,故字符编码的总长最短。
哈夫曼编码是前缀码,哈夫曼编码是最优前缀码
void CreateHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n){//从叶子到根你想求每个字符的哈夫曼编码,存储在编码表C中
HC = new char*[n+1];//分配n个字符编码的头指针矢量
cd = new char[n];//分配临时存放编码的动态数组空间
cd[n-1] = '\0';//编码结束符
for(int i = 1; i <= n; i++){//逐个字符求哈夫曼编码
start = n - 1;
c = i;
f = HT[i].parent;
while(f!=0){//从叶子结点开始向上回溯,知道根结点
--start;//回溯一次start向前指一个文职
if(HT[f].lchild == c) cd[start] = '0'//结点c时f的左孩子,则生成代码0
else cd[start] = '1';//结点c是f的右孩子,则生成代码1
c = f;//继续向上回溯
f = HT[f].parentl
}//求第i个字符的编码
HC[i] = new char[n-start];//为第i个字符串编码分配空间
strcpy(HC[i], &cd[start]);//将求得的编码从临时空间cd复制到HC的当前行中
}
delete cd;//释放临时空间
}