文章目录
============================ 【说明】 ===================================================
大家好,本专栏是 数据结构与算法, 该科目是计算机类专业必修课之一,比较重要也比较基础,有想从事算法研究的同学,这些内容是专/本科、甚至硕士期间较为基础的内容,适用范围较广:大学专业课学习、考研复习等。
通过自己的理解进行整理,希望大家积极交流、探讨,多给意见。后面也会给大家更新其他一些知识。若有侵权,联系删除!共同维护网络知识权利!
1、二叉树定义
1.1 二叉树特点
1、每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的。
2、左子树和右子树是有顺序的,次序不能任意颠倒。就像人是双手、双脚,但显然左手、左脚和右手、右脚是不一样的,右手戴左手套、右脚穿左鞋都会极其别扭和难受。
3、即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
二叉树五种形态
1.空二叉树。
2,只有一个根结点。
3.根结点只有左子树。
4.根结点只有右子树。
5、根结点既有左子树又有右子树。
1.2 特殊二叉树
1.斜树
顾名思义,斜树一定要是斜的,但是往哪斜还是有讲究。所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。斜树有很明显的特点,就是每一层都只有一个结点,结点的个数与二叉树的深度相同。
2.满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树 。
单是每个结点都存在左右子树,不能算是满二叉树,还必须要所有的叶子都在同一层上,这就做到了整棵树的平衡。因此,满二叉树的特点有:
(1)叶子只能出现在最下一层。出现在其他层就不可能达成平衡。
(2)非叶子结点的度一定是2。否则就是“缺胳膊少腿”了。
(3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
3.完全二叉树
对一棵具有n个结点的二叉树按层序编号,如果编号为i (1<i< n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。
首先从字面上要区分,“完全”和“满“的差异,满二叉树一定是一棵完全二叉树,但完全二叉树不一定是满的。
其次,完全二叉树的所有结点与同样深度的满二叉树,它们按层序编号相同的结点,是一一对应的。
这里有个关键词是按层序编号,像树1,因为5结点没有左子树,却有右子树,那就使得按层序编号的第10个编号空档了。同样道理,树2,由于3结点没有子树,所以使得6、7编号的位置空档了。树3又是因为5编号下没有子树造成第10和第11位置空档。只有上图的树,尽管它不是满二叉树,但是编号是连续的,所以它是完全二叉树。
从这里我也可以得出一些完全二叉树的特点:
(1) 叶子结点只能出现在最下两层。
(2)最下层的叶子一定集中在左部连续位置。
(3)倒数二层,若有叶子结点,一定都在右部连续位置。
(4)如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况。
(5)同样结点数的二叉树,完全二叉树的深度最小。
1.3 二叉树的性质
举例:下图中,结点总数为10,它是由A、B、C、D等度为2结点,F、 G、H、I、J等度为0的叶子结点和E这个度为1的结点组成。总和为4+ 1+5= 10。
我们换个角度,再数一数它的连接线数,由于根结点只有分支出去,没有分支进入,所以分支线总数为结点总数减去1。图中就是9个分支。对于A、B、c、D 结点来说,它们都有两个分支线出去,而£结点只有一个分支线出去。所以总分支线为4 × 2 + 1 × 1=9。
例:下图这是一个完全二叉树,度为4,结点总数是10
对于第一条来说是很显然的,i=1时就是根结点。i>1时,比如结点7,它的双亲就是7/2(向下取整)=3,结点9,它的双亲就是9/2(向下取整) =4。
第二条,比如结点6,因为2 × 6=12超过了结点总数10,所以结点6无左孩子, 它是叶子结点。同样,而结点5,因为2 × 5= 10正好是结点总数10,所以它的左孩子是结点10。
第三条,比如结点5,因为2 × 5+ 1=11,大于结点总数10,所以它无右孩子。而结点3,因为2 × 3 + 1=7小于10,所以它的右孩子是结点7。
1.4 二叉树存储结构
前面我们已经谈到了树的存储结构,并且谈到顺序存储对树这种一对多的关系结构实现起来是比较困难的。但是二叉树是一种特殊的树,由于它的特殊性,使得用顺序存储结构也可以实现。
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系,比如双亲与孩子的关系,左右兄弟的关系等。
1.二叉树顺序存储
完全二叉树的顺序存储,一棵完全二叉树如图所示。
例:完全二叉树的顺序存储,一棵完全二叉树如图所示。
将这棵二叉树存入到数组中,相应的下标对应其同样的位置。
当然对于一般的二叉树,尽管层序编号不能反映逻辑关系,但是可以将其按完全二叉树编号,只不过,把不存在的结点设置为“ ^ "而已。
考虑一种极端的情况,一棵深度为k的右斜树,它只有k个结点,却需要分配2k一1 个存储单元空间,这显然是对存储空间的浪费。
2.二叉链表
既然顺序存储适用性不强,我们就要考虑链式存储结构。二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法,我们称这样的链表叫做二叉链表。
其中data是数据域,和rchild都是指针域,分别存放指向左孩子和右孩子的指针。
1.5 遍历二叉树
定义:二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
这里有两个关键词:访问和次序。
访问其实是要根据实际的需要来确定具体做什么,比如对每个结点进行相关计算,输出打印等,它算作是一个抽象操作。在这里我们可以简单地假定就是输出结点的数据信息。
二叉树的遍历次序不同于线性结构,最多也就是从头至尾、循环、双向等简单的遍历方式。树的结点之间不存在唯一的前驱和后继关系,在访问一个结点后,下一个被访问的结点面临着不同的选择。就像你人生的道路上,高考填志愿要面临哪个城市、哪所大学、具体专业等选择,由于选择方式的不同,遍历的次序就完全不同了。
1.前(先)序遍历(DLR/NLR)
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。
遍历的顺序为:ABDGHCEIF
2.中序遍历(LDR/LNR)
规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。
遍历的顺序为:GDHBAEICF
3.后序遍历(LRD/LRN)
规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。
遍历的顺序为:GHDBIEFCA
4.层序遍历
规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问, 从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。
遍历的顺序为:ABCDEFGHI
目的:我们用图形的方式来表现树的结构,应该说是非常直观和容易理解,但是对于计算机来说,它只有循环、判断等方式来处理,也就是说,它只会处理线性序列,而我们刚才提到的四种遍历方法,其实都是在把树中的结点变成某种意义的线性序列,这就给程序的实现带来了好处。
另外不同的遍历提供了对结点依次处理的不同方式,可以在遍历过程中对结点进行各种处理。
遍历算法详解:(前序为例)
1、前序
(1)、调用PreOrderTraverse (T),T根结点不为null,所以执行printf,打印字母 A。
(2)、调用PreOrderTraverse (T->lchild);访问了A结点的左孩子,不为null,执行 printf显示字母B。
(3)、此时再次递归调用PreOrderTraverse (T->lchild);访问了B结点的左孩子,执行prin忏显示字母D。
(4)、再次递归调用PreOrderTraverse (T->lchild);访问了D结点的左孩子,执行 print显示字母H。
(5)、再次递归调用PreOrderTraverse (T->lchild);访问了H结点的左孩子,此时因为H结点无左孩子,所以T==null,返回此函数,此时递归调用PreOrderTraverse(T->rchild) 访问了H结点的右孩子,printf显示字母K。
(6)、 再次递归调用PreOrderTraverse (T->lchild);访问了K结点的左孩子,K结点无左孩子,返回,调用PreOrderTraverse (T->rchild);访问了K结点的右孩子、也是null,返回。于是此函数执行完毕,返回到上一级递归的函数(即打印H结点时的函数),也执行完毕,返回到打印结点D时的函数,调用PreOrderTraverse (T->rchild);访问了D结点的右孩子,不存在,返回到B 结点,调用PreOrderTraverse (T->rchild);找到了结点E,打印字母E。
(7)、由于结点E没有左右孩子,返回打印结点B时的递归函数,递归执行完毕,一 返回到最初的PreOrderTraverse,调用PreOrderTraverse (T->rchild);访问结点A的右孩子,打印字母C。
(8)、之后类似前面的递归调用,依次继续打印F、I、G、J,步骤略。
综上,前序遍历这棵二叉树的节点顺序是:ABDHKECFIGJ。
2、中序
3、后序
1.6 推导遍历结果(已知两种遍历结果,画出唯一二叉树)
有一种题目为了考查你对二叉树遍历的掌握程度,是这样出题的。
考型:已知两种遍历序列,求另一种遍历序列
已知一棵二叉树的前序遍历序列为ABCDEF,中序遍历序列为CBAEDF,请问这棵二叉树的后序遍历结果是多少?
练习:二叉树的中序序列是ABCDEFG,后序序列是 BDCAFGE,求前序序列。
两个二叉树遍历的性质:
已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。
已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。
但要注意了,已知前序和后序遍历,是不能确定一棵二叉树的,原因也很简单,比如前序序列是ABC,后序序列是CBAO我们可以确定A一定是根结点,但接下来,我们无法知道,哪个结点是左子树,哪个是右子树。如图四种可能。
1.7 线索二叉树
我们现在提倡节约型社会,一切都应该节约为本。对待我们的程序当然也不例外,能不浪费的时间或空间,都应该考虑节省。
我们再来观察下图,会发现指针域并不是都充分的利用了,有许许多多的“ ^ ”,也就是空指针域的存在,这实在不是好现象,应该要想办法利用起来。
首先我们要来看看这空指针有多少个呢?对于一个有n个结点的二叉链表,每个结点有指向左右孩子的两个指针域,所以一共是2n个指针域。而n个结点的二叉树 一共有n-1条分支线数,也就是说,其实是存在2n-(n-1) =n+1个空指针域。比如上图有10个结点,而带有“ ^ "空指针域为11。这些空间不存储任何事物,白白的浪费着内存的资源。
另一方面,我们在做遍历时,比如对上图做中序遍历时,得到了HDIBJEAFCG这样的字符序列,遍历过后,我们可以知道,结点I的前驱是D,后继是B,结点F的前驱是A,后继是C。也就是说,我们可以很清楚的知道任意一个结点,它的前驱和后继是哪一个。
可是这是建立在已经遍历过的基础之上的。在二叉链表上,我们只能知道每个结点指向其左右孩子结点的地址,而不知道某个结点的前驱是谁,后继是谁。要想知道,必须遍历一次。以后每次需要知道时,都必须先遍历一次。为什么不考虑在创建时就记住这些前驱和后继呢,那将是多大的时间上的节省。
综合刚才两个角度的分析后,我们可以考虑利用那些空地址,存放指向结点在某种遍历次序下的前驱和后继结点的地址。
就好像GPS导航仪一样,我们开车的时候,哪怕我们对具体目的地的位置一无所知,但它每次都可以告诉我从当前位置的下一步应该走向哪里。
这就是我们现在要研究的问题。我们把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树。
请看下图,我们把这棵二又树进行中序遍历后,将所有的空指针域中的 rchild,改为指向它的后继结点。
于是我们就可以通过指针知道H的后继是D(图中①),I的后继是B(图中②),J的后继是E(图中③),E的后继是A(图中④),F的后继是C(图中⑤),G的后继因为不存在而指向NULL(图中⑥)。此时共有6个空指针域被利用。
再看下图,我们将这棵二叉树的所有空指针域中的lchild,改为指向当前结点的前驱。
因此H的前驱是NULL(图中①), I的前驱是D(图中②),J的前驱是B (图中③),F的前驱是A(图④), G的前驱是C(图中⑤)。一共5个空指针域被利用,正好和上面的后继加起来是11个。
如下图(空心箭头实线为前驱,虚线黑箭头为后继),就更容易看出,其实线索二叉树,等于是把一棵二叉树转变成了一个双向链表,这样对我们的插人删除结点、查找某个结点都带来了方便。
所以我们对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化。
我们如何知道某一结点的lchild是指向它的左孩子还是指向前驱?rchild是指向右孩子还是指向后继?
比如E结点的lchild 是指向它的左孩子J,而rchild却是指向它的后继A。显然我们在决定lchild是指向 左孩子还是前驱,rchild是指向右孩子还是后继上是需要一个区分标志的。
因此,我们在每个结点再增设两个标志域Itag和rtag,注意Itag和rtag只是存放0或1数字的布尔型变量,其占用的内存空间要小于像lchild和rchild的指针变量。
ltag为0时指向该结点的左孩子,为1时指向该结点的前驱。
rtag为0时指向该结点的右孩子,为1时指向该结点的后继。
充分利用了空指针域的空间(这等于节省了空间) ,又保证了创建时的一次遍历就可以终生受用前驱后继的信息(这意味着节省了时间)。
所以在实际问题中,如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。
1.8 树、森林与二叉树之间的转换
我们前面已经讲过了树的定义和存储结构,对于树来说,在满足树的条件下可以是任意形状,一个结点可以有任意多个孩子,显然对树的处理要复杂得多,去研究关于树的性质和算法,真的不容易有没有简单的办法解决对树处理的难题呢?
我们前面也讲了二叉树,尽管它也是树,但由于每个结点最多只能有左孩子和右孩子,面对的变化就少很多了。因此很多性质和算法都被研究了出来。如果所有的树都像二叉树一样方便就好了。显然可以实现。
在讲树的存储结构时,我们提到了树的孩子兄弟法可以将一棵树用二叉链表进行存储,所以借助二叉链表,树和二叉树可以相互进行转换。从物理结构来看,它们的二叉链表也是相同的,只是解释不太一样而已。
因此,只要我们设定一定的规则,用二叉树来表示树,甚至表示森林都是可以的,森林与二叉树也可以互相进行转换。
1、树转换二叉树
将树转换为二叉树的步骤如下
1、加线。在所有兄弟结点之间加一条连线。
2、去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
3、层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。
注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。
例题:一棵树经过三个步骤转换为一棵二叉树。
2、森林转换二叉树
森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作。
1.把每个树转换为二叉树。
2.第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来当所有的二叉树连接起来后就得到了由森林转换来的二叉树。
例题:
3、二叉树转换为树
二叉树转换为树是树转换为二叉树的逆过程。
1.加线。若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点…….,反正就是左孩子的n个右孩子结点都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来。
2.去线。删除原二叉树中所有结点与其右孩子结点的连线。
3.层次调整。使之结构层次分明。
4、二叉树转换为森林
判断一棵二叉树能够转换成一棵树还是森林,标准很简单,那就是只要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树。
那么如果是转换成森林,步骤如下:
1.从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除……,直到所有右孩子连线都删除为止,得到分离的二叉树。
2.再将每棵分离后的二叉树转换为树即可。
树和森林的遍历
最后我们再谈一谈关于树和森林的遍历问题。
树的遍历分为两种方式。
1.一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树。
2,另一种是后根遍历,即先依次后根遍历每棵子树,然后再访问根结点。
比如下图中的树,它的先根遍历序列为ABEFCDG,后根遍历序列为 EFBCGDA。
森林的遍历也分为两种方式:
1.前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样方式遍历除去第一棵树的剩余树构成的森林。
比如下图中三棵树的森林,前序遍历序列的结果就是ABCDEFGHJI
2,后序遍历:是先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林。
上图,三棵树的森林,后序遍历序列的结果就是BCDAFEJHIG。
这也就告诉我们,当以二叉链表作树的存储结构时,树的先根遍历和后根遍历完全可以借用二叉树的前序遍历和中序遍历的算法来实现。
1.9 哈夫曼树及其应用
生活中,压缩包。
现在我们都是讲究效率的社会,什么都要求速度,在不能出错的情况下,做任何事情都讲究越快越好。在计算机和互联网技术中,文本压缩就是一个非常重要的技术。玩电脑的人几乎都会应用压缩和解压缩软件来处理文档。因为它除了可以减少文档在磁盘上的空间外,还有重要的一点,就是我们可以在网络上以压缩的形式传输大量数据,使得保存和传递都更加高效。
那么压缩而不出错是如何做到的呢?简单说,就是把我们要压缩的文本进行重新编码,以减少不必要的空间。尽管现在最新技术在编码上已经很好很强大,但这一切都来自于曾经的技术积累,介绍一下最基本的压缩编码方法–赫(哈)夫曼编码。
在介绍赫夫曼编码前,我们必须得介绍赫夫曼树,而介绍赫夫曼树,我们不得不提这样一个人,美国数学家赫夫曼(David Huffman),也有的翻译为哈夫曼。
他在 19S2年发明了赫夫曼编码,为了纪念他的成就,于是就把他在编码中用到的特殊的二叉树称之为赫夫曼树,他的编码方法称为赫夫曼编码。也就是说,我们现在介绍的知识全都来自于近60年前这位伟大科学家的研究成果,而我们平时所用的压缩和解压缩技术也都是基于赫夫曼的研究之上发展而来。
什么叫做赫夫曼树呢?我们先来看一个例子。
过去我们小学、中学一般考试都是用百分制来表示学科成绩的。这带来了一个弊端,就是很容易让学生、家长,甚至老师自己都以分取人,让分数代表了一切。有时想想也对,90分和95分也许就只是一道题目对错的差距,但却让两个孩子可能受到完全不同的待遇,这并不公平。于是在如今提倡素质教育的背景下,我们很多的学科,特别是小学的学科成绩都改作了优秀、良好、中等、及格和不及格这样模糊的词语,不再通报具体的分数。
不过对于老师来讲,他在对试卷评分的时候,显然不能凭感觉给优良或及格不及格等成绩,因此一般都还是按照百分制算出每个学生的成绩后,再根据统一的标准换算得出五级分制的成绩。比如下面的代码就实现了这样的转换。
问题:通常都认为,一张好的考卷应该是让学生成绩 大部分处于中等或良好的范围,优秀和不及格都应该较少才对。而上面这样的程序,就使得所有的成绩都需要先判断是否及格,再逐级而上得到结果。输人量很大的时候,其实算法是有效率问题的。
如果在实际的学习生活中,学生的成绩在5个等级上的分布规律如下表:
70分以上大约占总数80%的成绩都需要经过3次以上的判断才可以得到结果,这显然不合理。
有没有好一些的办法,仔细观察发现,中成绩(70一79分之间)比例最高,其次是良好成绩,不及格的所占比例最少,换个图。
效率提高了不少。
我们先把这两棵二叉树简化成叶子结点带权的二叉树,如下图所示。其中A 表示不及格、B表示及格、C表示中等、D表示良好、E表示优秀。每个叶子的分支线上的数字就是刚才我们提到的五级分制的成绩所占比例数。
赫夫曼说,从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称做路径长度。上图的二叉树a中,根结点到结点D的路径长度就为4,二叉树b中根结点到结点D的路径长度为2。树的路径长度就是从树根到每一结点的路径长度之和。二叉树a的树路径长度就为 1+ 1+ 2 +2+3 + 3+4+4=20。二叉树b的树路径长度为1+2 +3+3+2 + 1+2 +2=16。
如果考虑到带权的结点,结点的带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积。树的带权路径长度为树中所有叶子结点的带权路径长度之和。
假设有n个权值{WI,W2,…,Wn),构造一棵有n个叶子结点的二叉树,每个叶子结点带权Wk,每个叶子的路径长度为lk,则其中带权路径长度WPL最小的二叉树称做赫夫曼树。也有不少书中也称为最优二叉树。
有了赫夫曼对带权路径长度的定义,我们来计算一下上图这两棵树的WPL 值。
二叉树a的WPL=5 × 1+15 × 2 + 40 × 3 +30 × 4+ 10 × 4=315
注意:这里5是A结点的权,1是A结点的路径长度,其他同理。
二叉树b的WPL=5 × 3 +15 × 3+40 × 2+30 × 2+10 × 2=220
这样的结果意味着什么呢?如果我们现在有10000个学生的百分制成绩需要计算五级分制成绩,用二叉树a的判断方法,需要做31500次比较,而二叉树b的判断方法,只需要22000次比较,差不多少了三分之一量,在性能上提高不是一点点。
那么现在的问题就是,上图的二叉树b这样的树是如何构造出来的,这样的二叉树是不是就是最优的赫夫曼树呢?赫夫曼给了我们解决的办法。
步骤
1.先把有权值的叶子结点按照从小到大的顺序排列成一个有序序列,即:A5, E10,B15,D30,C40。
2.取头两个最小权值的结点作为一个新节点N1的两个子结点,注意相对较小的是左孩子。这里就是A为N1的左孩子,E为N1的右孩子,如下图所示。新结点的权值为两个叶子权值的和5+ 10= 15。
3.将N1替换A与E,插入有序序列中,保持从小到大排列。即:N1-15,B-15, D30,C40。
还 结构
4.重复步骤2。将N1与B作为一个新节点N2的两个子结点。如下图所示。N2的权值= 15 +15=30。
5.N2替换N1与B,插人有序序列中,保持从小到大排列。即:N2-30,D30, C40
6.重复步骤2。将N2与D作为一个新节点N3的两个子结点。如下图所示。N3的权值=30+30=60。
7.将N3替换N2与D,插入有序序列中,保持从小到大排列。即:C40,N3-60。
8.重复步骤2。将c与N3作为一个新节点T的两个子结点,如下图所示。
由于T即是根结点,则完成赫夫曼树的构造。
二叉树的带权路径长度WPL=40 x 1 +30 × 2+15× 3+ 10 × 4+5 × 4=205。与之前的二叉树b的WPL值220相比,还少了1显然此时构造出来的二叉树才是最优的赫夫曼树。
通过刚才的步骤,我们可以得出构造赫夫曼树的赫夫曼算法描述。
1.根据给定的n个权值{ W1,w2, …,wn }构成n棵二叉树的集合F={ T1,T2,…,Tn },其中每棵二叉树Ti中只有一个带权为根结点,其左右子树均为空。
2.在F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二又树的根结点的权值为其左右子树上根结点的权值之和。
3.在F中删除这两棵树,同时将新得到的二叉树加入F中。
4.重复2和3步骤,直到F只含一棵树为止。这棵树便是赫夫曼树
哈夫曼编码
当然,赫夫曼研究这种最优树的目的不是为了我们可以转化一下成绩。他的更大目的是为了解决当年远距离通信(主要是电报)的数据传输的最优化问题。
比如我们有一段文字内容为"BADCADFEED"要网络传输给别人,显然用二进制的数字(0和1)来表示是很自然的想法。我们现在这段文字只有六个字母ABCDEF,那么我们可以用相应的二进制数据表示:
这样真正传输的数据就是编码后的“ 001000011010000011101100100011气对方接收时可以按照3位一分来译码。
如果一篇文章很长,这样的二进制串也将非常的可怕。而且事实上,不管是英文、中文或是其他语言,字母或汉字的出现频率是不相同的,比如英语中的几个元音字母"aeiou气中文中的“有了有在"等汉字都是频率极高。
假设六个字母的频率为A 27,B 8,C 15,D 15,E 30,F 5,合起来正好是100%。那就意味着,我们完全可以重新按照赫夫曼树来规划它们。
左图为构造赫夫曼树的过程的权值显示。右图为将权值左分支改为0,右分支改为1后的赫夫曼树。(左0右1)
此时,我们对这六个字母用其从树根到叶子所经过路径的0或1来编码。
我们将文字内容为"BADCADFEED"再次编码,对比可以看到结果串变小了。
也就是说,我们的数据被压缩了,节约了大约17%的存储或传输成本。随着字符的增加和多字符权重的不同,这种压缩会更加显出其优势。