数据结构-树(哈夫曼树)
树的基本概念
定义
树(Tree)是n(n>=0)个结点的有限集。n=0时成为空树。
在任意一个非空树中:有且仅有一个特定的称为根(Root)的结点;n>1时,其余结点可分为m(m>0)个互不相交的有限集,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。
ps:
- 根结点是唯一的。
- 子树的个数没有限制,但它们一定互不相交。
结点的分类
- 结点拥有的子树数称为结点的度(Degree)。
- 度为0的结点称为叶结点(Leaf)或终端结点;度不为0的结点称为非终端结点或分支结点。除根结点外,分支结点也称为内部结点。
- 数的度是树内各结点的度的最大值。
结点间的关系
- 结点的子树的根称为该节点的孩子,相应的,该结点称为孩子的双亲。
- 同一个双亲的孩子之间互称兄弟。
- 结点的祖先是从根到该结点所经分支上的所有结点。反之,以某结点为根的子树中的任一结点都称为该结点的子孙。
其他相关概念
- 节点的层次(Level)从根开始定义,根为第一层,根的孩子为第二层。若某结点在第l层,则其子树就在第l+1层。
- 其双亲在同一层的结点互为堂兄弟。
- 树中结点的最大层次称为树的深度(Depth)或高度。
- 如果将树中结点的各子树看成从左至右的有序次的、不能互换的,则称该树为有序树,否则称为无序树。
- 森林(Forest)是m(m>=0)棵互不相交的树的集合。对于树中的每个结点而言,其子树的集合即为森林。
树的储存结构
双亲表示法
以一组连续空间储存树的结点,同时在每个结点中,附设一个指示器指示其双亲结点在数组的位置。
#define MAX_TREE_SIZE 100typedef int ElemType;typedef struct PTNode//结点结构
{ElemType data;int parent;//双亲位置
}PTNode;typedef struct//树结构
{PTNode nodes[MAX_TREE_SIZE];int r, n;//根的位置和结点数
}PTree;
孩子表示法
每个结点有多个指针域,其中每个指针指向向一棵的根结点,通常把这种方法叫做多重链表表示法,这种方法可以提高对空间的利用效率,但是由于要维护结点的度,会带来时间上的浪费。
而孩子表示法就是把每个结点的孩子结点排列起来,以单链表作储存结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序储存结构,存放进以为数组中:
#define MAX_TREE_SIZE 100typedef int TElemType;typedef struct CTNode//孩子结点
{int child;struct CTNode *next;
}*childPtr;
typedef struct//表头结点
{TElemType data;childPtr firstChild;
}CTBox;
typedef struct//树结构
{CTBox nodes[MAX_TREE_SIZE];//结点数组int r, n;//根的位置和结点数
}CTree;
孩子兄弟表示法
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
typedef struct CSNode
{TElemType data;struct CSNode *firstChild, *rightSib;
}CSNode, *CSTree;
这种表示法最大的好处就是可以表示成二叉树的形式。
二叉树
二叉树(Binary Tree)是n(n >=0)个结点的有限集合,该集合或则为空集(空二叉树),或者有一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
特点
- 每个结点最多有两颗子树,所以二叉树中不存在大于2的结点。
- 左子树和右子树是有顺序的,次序不可以任意颠倒。
- 即使树中某结点只有一颗子树,也要区分它是右子树还是左子树。
特殊二叉树
- 斜树:所有结点都只有左子树的二叉树叫作左斜树,所有结点都只有右子树的二叉树叫作右斜树,这两者统称为斜树。
- 满二叉树:在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
- 完全二叉树:对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。
完全二叉树的一些特点:
1、 叶子结点只能出现在最下两层
2、最下层叶子一定集中在左部连续的位置
3、倒数二层,若有叶子结点,一定都在右部连续位置
4、如果结点的度为1,则该结点只有左孩子,即不存在只有右子树的情况
5、同样结点数的二叉树,完全二叉树的深度最小。 - 二叉搜索树
二叉搜索树是一个有序树:
1、若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
2、若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
3、它的左、右子树也分别为二叉搜索树。
二叉树的性质
- 在二叉树的第i层至多有2i-1 个结点(i>=1)
- 深度为k的二叉树至多有2k - 1个结点(k>=1)
- 对于任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1 。
ps:想要让终端结点数+1,那么度为2的结点一定也会+1(只增加左子树或者右子树是不会导致终端结点增加的) - 具有n个结点的完全二叉树的深度为[log2 n]+1([x]表示不大于x的最大整数)
- 如果对一棵有n个结点的完全二叉树(其深度为[log2 n]+1)的结点按层序编号(从第1层到第[log2 n] 层,每层从左到右),对任一结点j(1<=i<=n)有:
(1)如果i = 1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2]。
(2)如果2i>n,则结点i无左孩子(结点i为叶子的左结点);否则其左孩子是结点2i。
(3)如果2+i>n,则结点i无右孩子;否则其右孩子是结点2*i+1.
储存结构
顺序储存结构
顺序储存即直接按照其在完全二叉树中的序号在对应的数组位置中存放相应的数据。
顺序储存结构通常只用于完全二叉树。
二叉链表
将二叉树结点空间设置为一个数据域和两个指针域。
typedef struct BiTNode
{ElemType data;struct BiTNode *lchild, *rchild;
}BiTNode, *BiTNode;
遍历二叉树
二叉树的遍历(traversing binary tree)是指从根节点出发,按照某种次序依次访问二叉树中的所有结点,使得每一个结点被访问一次且仅被访问一次。
如果需要遍历整棵树,递归函数就不能有返回值。如果需要遍历某一条固定路线,递归函数就一定要有返回值!
前序遍历
void PreOrderTraverse(BiTree T)
{if(T == NULL)return 0;printf("%d", T->data);//这里是对结点的操作,可以更换所需的函数PreOrderTraverse(T->lchild);PreOrderTraverse(T->rChild);
}
中序遍历
void PreOrderTraverse(BiTree T)
{if(T == NULL)return 0;PreOrderTraverse(T->lchild);printf("%d", T->data);//可替换为其他对二叉树的操作PreOrderTraverse(T->rchild);
}
后序遍历
void PreOrderTraverse(BiTree T)
{if(T == NULL)return 0;PreOrderTraverse(T->lchild);PreOrderTraverse(T->rchild);printf("%d", T->data);
}
前序和中序可以唯一确定一棵二叉树。后序和中序可以唯一确定一棵二叉树。
前序和后序不能唯一确定一棵二叉树!,因为没有中序遍历无法确定左右部分,也就是无法分割。
二叉树的建立
//#表示空树
void CreateBiTree(BiTree *T, char *str)
{ElemType ch;scanf("%c", &ch);ch = str[index++];if(ch == '#')*T = NULL;else{*T = (BiTree)malloc(sizeof(BiTNode));if(*T)exit(OVERFLOW);(*T)->data = ch;//生成根结点CreateBiTree(T->lchild);CreateBiTree(T->rchild);}
}
线索二叉树
我们将对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化;把这种指向前驱和后继的指针成为线索,加上线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树。
如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么就可以考虑采用线索二叉树。
线索二叉树的指向:
(1)当前左孩子为空时,指向当前遍历顺序序列的直接前驱
(2)当前右孩子为空时,指向当前遍历顺序序列的直接后驱
线索二叉树的实现(中序)
为了知道某一结点的lchild时指向它的左孩子还是前驱,rchild是指向右孩子还是后继,因此设置两个标志域ltag和rtag,其存放数字0或1数字型布尔型变量(是0则指向孩子,是1则指向前驱/后继)
储存结构:
typedef char TElemType;
typedef enum {Link, Thread} PointerTag;
//Link==0表示指向左右孩子指针,Thread==1表示指向前驱或后继的线索typedef struct BiThrNode{TElemType data;struct BiTheNode *lchild, *rchild;PointerTag LTag;//左右标志PointerTag RTag;
}BiThrNode, *BiThrTree;
中序遍历线索化的递归函数代码:
BiThrTree pre;//始终指向刚刚访问过的结点void InThreading(BiThrTree p)
{if(p){InThreading(p->lchild);//递归左子树线索化if(!p->lchild)//没有左孩子{p->LTag = Thread;//前驱线索p->lchild = pre;//左孩子指针指向前驱}if(!p->rchild)//没有右孩子{pre->RTag = Thread;pre->rchild = p;//前驱右孩子指针指向后继(当前结点p)}pre = p;InThreading(p->rchild);}
}
判断后继时:因为此时p结点的后继还没有访问到,因此只能对它的前驱结点pre的右指针rchild作判断,if(!pre->rchild)表示如果为空,则p就是pre的后继,于是pre->rchild=p并且设置pre->RTag= Thread,完成后继结点的线索化。
线索化二叉树时,令二叉树中序序列中的第一个结点的lchlid指向头结点、最后一个结点的rchild指向头结点,使得我们既可以从第一个结点起顺后继进行遍历或顺最后一个结点起顺前驱进行遍历。
遍历代码:
Status InOrderTraverse_Thr(BiThrTree T)
{BiThrTree p;p = T->lchild;while(p != T){while(p->LTag == Link)//当LTag==0时循环到中序序列的第一个结点p = p->lchild;printf("%c", p->data);//结点的操作while(p->RTag == Thread && p->rchlid != T)//结点有后继,访问后继{p = p->rchild;printf("%c", p->data);}p = p->rchild;//当该结点无后继指针时,则证明其有右子树,就直接访问其右子树}return OK;
}
树、森林和二叉树的转换
树转换为二叉树
- 加线:在所有兄弟之间加一条线
- 去线:对树中的每一个结点,只保留它与其第一个孩子的连线,删除它与其他孩子的连线
- 调整层次
森林转为二叉树
- 把每个树转为二叉树
- 第一棵树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树根结点的右孩子,用线连接起来
二叉树转为树
- 加线:若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点,以此类推所有的右孩子结点都作为此结点的孩子
- 去线:删除原二叉树中所有结点与其右孩子结点的连线
- 调整层次
二叉树转为森林
判断一棵二叉树能否转换成一棵树还是森林,需要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树
- 从根结点开始,若右孩子存在,则把与右孩子结点的连线删除
- 根结点分离后,再看分离出的二叉树的根结点是否有右孩子,若右孩子存在,在此把与右孩子的连线删除,知道不能在分离为止
- 将分离出的二叉树转换为树即可
树与森林的遍历
树:
(1)先根遍历。即先访问树的根结点,然后依次先根遍历根的每一棵子树
(2)后根遍历。即先后根遍历每棵子树,然后再访问根结点。
森林:
(1)前序遍历。先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样的的方式遍历除去第一棵树的剩余数构成的森林。
(2)后序遍历。先访问森林中的第一棵树,后根遍历的方式遍历每一棵子树,然后再访问根结点,依次用同样的方式遍历除去第一棵树的剩余树构成的森林。
森林为某一棵二叉树转化而成时,森林的前序遍历和二叉树的前序遍历结果相同,森林后序遍历和二叉树中序遍历的结果相同。
哈夫曼树
带权路径长度WPL最小的二叉树称做二叉树,也可以叫做最优二叉树。
- 从树中的一个结点到另一个结点之间的分支结构构成两个结点之间的路径,路径上的分支数目称做路径长度。
- 树的路径长度就是从树根到每一结点的路径长度之和。
- 如果结点带权,则将权与不带权时的路径长度相乘得到结点的带权的路径长度。
如何构造哈夫曼树
二叉树b的WPL=5 × 3+15 × 3+40 × 2+30 × 2+10 × 2=220
构造哈夫曼树步骤:
- 先把有权值的叶子结点从小到大排列成一个有序序列,即A5,E10,B15,D30,C40
- 取两个最小权值的结点作为一个新结点N1 的两个子节点(较小的为左孩子),新结点的权值为两个叶子结点权值的和。
- 将N1 替换A与E,插入有序序列中,保持从小到大的序列,即N1 15,B15, D30,C40。
- 重复步骤二,得到新结点N2 。
- 又将N2 替换N1 和B插入序列。
- 重复步骤二,得到新结点N3 。
- 将N3 替换N2和D,插入序列。
- 重复步骤二,最终得到哈夫曼树。即上图二叉树a。
哈夫曼树实际上就是把带权值的结点置为无左右子树的叶子结点。
此时二叉树a的WPL=401+302+153+104+5*4=205比二叉树b还要少,显然a才是最优的哈夫曼树。
(不过二叉树a在每次判断时都要比较两次,实际上总体性能不如b)
哈夫曼编码
前缀编码:所有编码都不是其他编码的前缀。
哈夫曼研究出这种最优二叉树是为了解决当年远距离通讯的数据传输的最优化问题。
一般地,设需要的编码字符集为{d1,d2,…,dn},各个字符在电文中出现的次数或频率的集合为{W1,W2,…,Wn},以d作为叶子结点,将W作为对应叶子的结点的权值来构造一棵哈夫曼树。规定哈夫曼树的左分支为0、右分支为1,则从根结点到叶子结点所经过的路径分支组成的0和1的序列便为该结点对应的字符编码,这就是哈夫曼编码。
特点:
前缀编码
频度越大离根越近,编码越短
平均编码长度:累加每一个(码长×概率)
数据结构-树(哈夫曼树)相关推荐
- 数据结构(哈夫曼树+KMP)之 数据加密+解密
数据结构(哈夫曼树+KMP)之 数据加密+解密 原理:参考趣学数据结构 代码: #include<stdio.h> #include<stdlib.h> #define N 1 ...
- 2020-10-1 //严蔚敏《数据结构》 //赫夫曼树及其应用:创建顺序赫夫曼树创建及得到赫夫曼编码
//严蔚敏<数据结构> //赫夫曼树及其应用:创建顺序赫夫曼树创建及得到赫夫曼编码 //(从叶子结点到根逆向求每个字符的赫夫曼编码)以及(无栈非递归遍历赫夫曼树,求赫夫曼编码) //自学中 ...
- 数据结构“基于哈夫曼树的数据压缩算法”的实验报告
一个不知名大学生,江湖人称菜狗 original author: jacky Li Email : 3435673055@qq.com Last edited: 2022.11.20 目录 数据结构& ...
- 数据结构 基于哈夫曼树的数据压缩算法
数据结构 基于哈夫曼树的数据压缩算法 实验目的 实验内容 实验提示 实验代码 实验小结 实验目的 1.掌握哈夫曼树的构造算法. 2.掌握哈夫曼编码的构造算法. 实验内容 问题描述 输入一串字符串,根据 ...
- 【数据结构】哈夫曼树
哈夫曼树 哈夫曼树:最优二叉树,带权路径长度最短的树 哈夫曼树中权值越大的结点离根结点越近 权:将树中的结点赋给一个有某种意义的值,这个值成为结点的权 结点的带权路径长度:从根结点到该结点之间的路径长 ...
- 数据结构与算法之Huffman tree(赫夫曼树 / 霍夫曼树 / 哈夫曼树 / 最优二叉树)
目录 赫夫曼树概述 定义 构造赫夫曼树步骤 代码实现 赫夫曼树概述 HuffmanTree因为翻译不同所以有其他的名字:赫夫曼树.霍夫曼树.哈夫曼树 赫夫曼树又称最优二叉树,是一种带权路径长度最短的二 ...
- 哈夫曼树(带权路径长度+树的带权路径长度+哈夫曼树定义+构造哈夫曼树+哈夫曼树性质+哈夫曼编码+计算平均码长-这里指WPL)
带权路径长度 树的带权路径长度WPL 哈夫曼树 哈夫曼树构造 哈夫曼树性质 哈夫曼编码 固定长度编码 可变长编码 前缀编码 固定长度编码.可变长编码.前缀编码.哈夫曼编码 思维倒图 试题
- 【数据结构】哈夫曼树与哈夫曼编码
定义 带权路径长度(WPL):设二叉树有n个叶子结点,每个叶子结点带有权值wkw_kwk,从根节点到每个叶子结点的长度为lkl_klk,则每个叶子结点的带权路径长度之和就是:WPLWPLWPL=∑ ...
- 【数据结构】赫夫曼树
数据结构赫夫曼树 /*名称:赫夫曼树语言:数据结构C语言版 编译环境:VC++ 6.0日期: 2014-3-26 */#include <stdio.h> #include <lim ...
- 数据结构学习——哈夫曼树
数据结构学习记录DAY13 :哈夫曼树(上) 哈(赫)夫曼树和哈(赫)夫曼编码 路径 一个结点到另外一个结点的通路,称为路径 (祖先结点到子孙结点) 路径长度: 每经过一个结点,路径长度就增加1,不包 ...
最新文章
- golang中的消息认证
- php中mb substr,php中中文截取函数mb_substr()详细
- 1148 Werewolf - Simple Version (20 分)
- 美团Android开发工程师岗位职能要求,高级面试题+解析
- 从编程语言排行来看:C/C++一直占有前三之位,为何C++不会消亡?
- android把js的注入和方法统一管理,android WebView 注入js 几种方式
- 二分搜索:lower_bound 与 upper_bound 函数
- index.jsp中文乱码问题
- 模板,宏,atuo关键字的简述
- 苹果手机的定向广告时代告终
- css 长单词不换行溢出容器的解决方法 word-wrap与word-break
- DiskPart分区工具命令详解
- C语言作用域(可见性)和生存期
- 兼容iOS 10 _升级xcode8_适配(一)
- python教材答案编写函数求成绩平均数_学好Python例题之求成绩平均分
- 西工大PAMI论文:发布大规模人群计数/定位基准平台
- Homekit智能家居创意DIY一智能灯
- CSS字体中英文名称对照表:如宋体对应SimSun
- 1024| 只为程序员们打Call(多重福利)
- acrobat PDF删除部分_锁住秘密,三个步骤,实现PDF文档加密
热门文章
- windows访问Ext4磁盘
- 使用Threejs创建几何体,动态添加几何体,删除几何体,添加坐标轴
- 【Proteus 8.9安装教程】
- 光动能表怎么维护_光动能手表是什么意思(一个修表师傅的忠告)
- 如何通过指标追踪和分析实现母婴店年底促销销售目标?
- FPKM值基因表达量的计算、基因ID转gene symbol的例子
- 机器学习之HMM模型
- 7bit解码 java_7bit编码 - 莫问viva的个人空间 - OSCHINA - 中文开源技术交流社区
- 3D视觉(三):双目摄像头的标定与校正
- java中char类型_Java-char类型详解