400 likes | 520 Views
7.4 二叉树类. 二叉树类设计如下:. template <class T> class BiTree { private: BiTreeNode<T> *root; // 根结点指针 void Destroy(BiTreeNode<T>* &t); void PreOrder(BiTreeNode<T>* &t, void (*Visit)(T item)); void InOrder(BiTreeNode<T>* &t, void (*Visit)(T item));
E N D
7.4 二叉树类 二叉树类设计如下: template <class T> class BiTree { private: BiTreeNode<T> *root; //根结点指针 void Destroy(BiTreeNode<T>* &t); void PreOrder(BiTreeNode<T>* &t, void (*Visit)(T item)); void InOrder(BiTreeNode<T>* &t, void (*Visit)(T item)); void PostOrder(BiTreeNode<T>* &t, void (*Visit)(T item)); public: BiTree(void):root(NULL){}; //构造函数 ~BiTree(void){}; //析构函数
//构造二叉树 void MakeTree(const T item, BiTree<T> &left, BiTree<T> &right); void Destroy(void); //撤消二叉树 void PreOrder(void (*Visit)(T item)); //前序遍历 void InOrder(void (*Visit)(T item)); //中序遍历 void PostOrder(void (*Visit)(T item)); //后序遍历 };
template <class T> void BiTree<T>::MakeTree(const T item, BiTree<T> &left, BiTree<T> &right) //构造数据域为item左子树为left右子树为right的二叉树 { root = new BiTreeNode<T>(item, left.root, right.root); } template <class T> void BiTree<T>::Destroy(void) //撤消二叉树 { Destroy(root); }
设计说明: (1)和单链表类似,一个二叉链结构的二叉树也可以由指向该二叉树的根指针表示,所以,二叉树类的成员变量为指向该二叉树的根指针root,root设计为私有访问权限 (2)撤消操作是一个递归执行过程,需要通过递归调用来实现,而递归调用时需要给出每次调用时的参数,因此,撤消成员函数也设计了两个,公有的撤消成员函数没有参数,私有的撤消成员函数带一个参数,公有的撤消成员函数通过调用私有的撤消成员函数实现二叉树的撤消。 (3)二叉树是递归定义的,即二叉树是由根结点和左右子二叉树构成的,所以,按照上述方法建立二叉树的撤消过程不能通过析构函数来实现,因为如果二叉树首先通过自动调用析构函数撤消后,则意味着包含在该二叉树中的所有子二叉树也已被撤消,则子二叉树对象再自动调用析构函数撤消子二叉树时就会出错。
7.5 二叉树的分步遍历 二叉树的遍历有两种情况,一种是一次性遍历;另一种是分步遍历。分步遍历是指在规定了一棵二叉树的遍历方法后,每次只访问当前结点的数据域值,然后使当前结点为当前结点的后继结点,直到到达二叉树的最后一个结点为止。分步遍历方法提供了对二叉树进行循环遍历操作的工具。 1.二叉树遍历游标类 为满足分步遍历操作的需要,我们引入了二叉树遍历游标类。使用纯虚函数和抽象类方法,把二叉树遍历游标类设计为抽象类形式的基类,把二叉树中序遍历游标类、二叉树前序遍历游标类、二叉树后序遍历游标类和二叉树层序遍历游标类设计为该基类的派生类,这样,一方面对于派生类中共同的成员变量和成员函数可一次性设计完成,另一方面,各种不同的二叉树遍历游标类由于都是基类的派生类,所以其遍历的控制过程完全相同,从而可方便使用和维护。
2.二叉树中序遍历游标类 二叉树的中序递归遍历算法是算法本身的不断自调用实现的,所以中序递归遍历算法是不可分解的。若借助一个堆栈模仿系统的运行时栈,非递归的二叉树中序遍历算法如下: (1)设置一个堆栈并初始化; (2)使临时指针等于二叉树根结点指针,如二叉树根结点不空令结束标记为0;否则为1; (3)当临时指针的左孩子结点指针不空时循环;否则转向步骤(4); (3.1)把临时指针入堆栈; (3.2)临时指针等于临时指针的左孩子结点指针; (4)如果临时指针不存在则令结束标记为1; (5)如果结束标记为1转步骤(8);否则继续执行; (6)访问临时指针所指的结点; (7)如果临时指针的右孩子结点指针不空则使临时指针等于临时指针的右孩子结点指针,转到步骤(3);否则如果堆栈不空则退栈使临时指针等于栈顶结点指针,转向步骤(5);否则令结束标记为1,转向步骤(5); (8)算法结束。
A A A 访 问 B t B C B C B C 访 问 D 访 问 E t D D E D t E E 堆 栈 & B & A 堆 栈 & A & A 堆 栈 (b)下一个结点 (a)中序遍历的第一个结点 ( c )下一个结点 访 问 A t A A A 访 问 C t B C B C B C D t = N U L L D E D E E 堆 栈 堆 栈 堆 栈 下一个结点 下一个结点 结束标记等于1 ( d) ( e) ( f) 下图给出了上述算法的一个有5个结点二叉树例子的上述算法执行过程
二叉树中序遍历游标类设计如下: #include "LinStack.h" //包含链式堆栈类 template <class T> class BiTrInIterator: public BiTreeIterator<T>//二叉树中序遍历游标类 { private: LinStack<BiTreeNode<T> *> S; //存放结点指针的堆栈 BiTreeNode<T> *GoFarLeft(BiTreeNode<T>* &t);//寻找最左孩子结点 public: BiTrInIterator(BiTreeNode<T>* &tree): //构造函数 BiTreeIterator<T>(tree){} //调用基类的构造函数 virtual void Next(void); virtual void Reset(void); };
3.二叉树层序遍历游标类 二叉树层序遍历的次序是根结点,根结点的左子树结点,根结点的右子树结点,根结点的左子树结点的左子树结点,根结点的左子树结点的右子树结点,如此等等,一直到最下层最右边的结点为止。 二叉树层序遍历游标类的设计如下:
#include "LinQueue.h" //包含链式队列类 template <class T> class BiTrLeIterator: public BiTreeIterator<T> { protected: LinQueue<BiTreeNode<T> *> Q; // 保存二叉树结点指针的队列 public: BiTrLeIterator(BiTreeNode<T>* &tree): BiTreeIterator<T>(tree){} virtual void Next(void); virtual void Reset(void); };
7.6 线索二叉树 线索二叉树是另一种分步遍历二叉树的方法。它既可以从前向后分步遍历二叉树,又可以从后向前分步遍历二叉树。 当按某种规则遍历二叉树时,保存遍历时得到的结点的后继结点信息和前驱结点信息的最常用的方法是建立线索二叉树。 对二叉链存储结构的二叉树分析可知,在有n个结点的二叉树中必定存在n+1个空链域。 如下规定:当某结点的左指针为空时,令该指针指向按某种方法遍历二叉树时得到的该结点的前驱结点;当某结点的右指针为空时,令该指针指向按某种方法遍历二叉树时得到的该结点的后继结点。仅仅这样做会使我们不能区分左指针指向的结点到底是左孩子结点还是前驱结点,右指针指向的结点到底是右孩子结点还是后继结点。因此我们再在结点中增加两个线索标志位来区分这两种情况。线索标志位定义如下:
leftThread leftChild data rightChild rightThread 每个结点的结构就如下所示: 结点中指向前驱结点和后继结点的指针称为线索。在二叉树的结点上加上线索的二叉树称作线索二叉树。对二叉树以某种方法(如前序、中序或后序方法)遍历使其变为线索二叉树的过程称作按该方法对二叉树进行的线索化。
root 0 1 0 A 0 0 C 0 0 B 1 A B C 1 E 1 1 F 1 1 D 0 D E F 1 G 1 G (a) (b) root root 0 1 0 1 0 A 0 0 A 0 0 C 0 0 B 1 0 C 0 0 B 1 1 E 1 1 F 1 1 D 0 1 E 1 1 F 1 1 D 0 1 G 1 1 G 1 (d) (c) 线索二叉树
7.7 哈夫曼树 1.哈夫曼树的基本概念 从A结点到B结点所经过的分支序列叫做从A结点到B结点的路径;从A结点到B结点所经过的分支个数叫做从A结点到B结点的路径长度;从二叉树的根结点到二叉树中所有叶结点的路径长度之和称作该二叉树的路径长度。设二叉树有n个带权值的叶结点,定义从二叉树的根结点到二叉树中所有叶结点的路径长度与相应叶结点权值的乘积之和称作该二叉树的带权路径长度(WPL),即: WPL = 其中,wi为第i个叶结点的权值,li为从根结点到第i个叶结点的路径长度。
7 7 1 5 3 5 7 1 1 3 5 7 1 3 3 5 ( a ) ( b ) ( c ) ( d ) 下图所示二叉树带权路径长度分别为: (a)WPL = 1×2+3×2+5×2+7×2 = 32 (b)WPL = 1×2+3×3+5×3+7×1 = 33 (c)WPL = 7×3+5×3+3×2+1×1 = 43 (d)WPL = 1×3+3×3+5×2+7×1 = 29 具有最小带权路径长度的二叉树称作哈夫曼(Huffman)树(或称最优二叉树)。图(d)的二叉树是一棵哈夫曼树。
要使一棵二叉树的带权路径长度WPL值最小,必须使权值越大的叶结点越靠近根结点。哈夫曼树构造算法为:要使一棵二叉树的带权路径长度WPL值最小,必须使权值越大的叶结点越靠近根结点。哈夫曼树构造算法为: (1)由给定的n个权值{w1,w2,…,wn}构造n棵只有根结点的二叉树,从而得到一个二叉树森林F={T1,T2,…,Tn}。 (2)在二叉树森林F中选取根结点的权值最小和次小的两棵二叉树作为新的二叉树的左右子树构造新的二叉树,新的二叉树的根结点权值为左右子树根结点权值之和。 (3)在二叉树森林F中删除作为新二叉树左右子树的两棵二叉树,将新二叉树加入到二叉树森林F中。 (4)重复步骤(2)和(3),当二叉树森林F中只剩下一棵二叉树时,这棵二叉树就是所构造的哈夫曼树。
字符 字符 编码 编码 A A 0 00 B B 01 110 C C 10 10 D D 111 11 2.哈夫曼编码问题 将传送的文字转换为二进制字符0和1组成的二进制串的过程为编码。 例:假设要传送的电文为ABACCDA,电文中只有A,B,C,D四种字符,若这四个字符采用表(a)所示的编码方案,则电文的代码为00 01 00 10 10 11 00,代码总长度为14。若这四个字符采用表(b)所示的编码方案,则电文的代码为0 110 0 10 10 111 0,代码总长度为13。 (a) (b)
哈夫曼树可用于构造代码总长度最短的编码方案。具体构造方法如下:设需要编码的字符集合为{d1,d2,…,dn},各个字符在电文中出现的次数集合为{w1,w2,…,wn},以d1,d2,…,dn作为叶结点,以w1,w2,…,wn作为各叶结点的权值构造一棵二叉树,规定哈夫曼树中的左分支为0,右分支为1,则从根结点到每个叶结点所经过的分支对应的0和1组成的序列便为该结点对应字符的编码。代码总长度最短的不等长编码称之为哈夫曼编码。哈夫曼树可用于构造代码总长度最短的编码方案。具体构造方法如下:设需要编码的字符集合为{d1,d2,…,dn},各个字符在电文中出现的次数集合为{w1,w2,…,wn},以d1,d2,…,dn作为叶结点,以w1,w2,…,wn作为各叶结点的权值构造一棵二叉树,规定哈夫曼树中的左分支为0,右分支为1,则从根结点到每个叶结点所经过的分支对应的0和1组成的序列便为该结点对应字符的编码。代码总长度最短的不等长编码称之为哈夫曼编码。
weight start Bit[0] flag parent Bit[1] leftChild … Bit[MaxBit-1] rightChild 3.哈夫曼编码的软件设计 一、哈夫曼编码的数据结构设计 为了在构造哈夫曼树时能方便的实现从双亲结点到左右孩子结点的操作,在进行哈夫曼编码时能方便的实现从孩子结点到双亲结点的操作。设计哈夫曼树的结点存储结构为双亲孩子存储结构。采用仿真指针实现,每个结点的数据结构设计为: 从哈夫曼树求叶结点的哈夫曼编码实际上是从叶结点到根结点路径分支的逐个遍历,每经过一个分支就得到一位哈夫曼编码值。存放哈夫曼编码的数据元素结构为:
二、哈夫曼编码的算法实现 struct HaffNode //哈夫曼树的结点结构 { int weight; //权值 int flag; //标记 int parent; //双亲结点下标 int leftChild; //左孩子下标 int rightChild; //右孩子下标 }; struct Code //存放哈夫曼编码的数据元素结构 { int bit[MaxN]; //数组 int start; //编码的起始下标 int weight; //字符的权值 };
哈夫曼树构造算法如下: void Haffman(int weight[], int n, HaffNode haffTree[]) //建立叶结点个数为n权值为weight的哈夫曼树haffTree { int j, m1, m2, x1, x2; //哈夫曼树haffTree初始化。n个叶结点的哈夫曼树共有2n-1个结点 for(int i = 0; i < 2 * n - 1 ; i++) { if(i < n) haffTree[i].weight = weight[i]; else haffTree[i].weight = 0; haffTree[i].parent = 0; haffTree[i].flag = 0; haffTree[i].leftChild = -1; haffTree[i].rightChild = -1; }
//构造哈夫曼树haffTree的n-1个非叶结点 for(i = 0;i < n-1;i++) { m1 = m2 = MaxValue; x1 = x2 = 0; for(j = 0; j < n+i;j++) { if(haffTree[j].weight < m1 && haffTree[j].flag == 0) { m2 = m1; x2 = x1; m1 = haffTree[j].weight; x1 = j; }
else if(haffTree[j].weight < m2 && haffTree[j].flag == 0) { m2 = haffTree[j].weight; x2 = j; } }
//将找出的两棵权值最小的子树合并为一棵子树//将找出的两棵权值最小的子树合并为一棵子树 haffTree[x1].parent = n+i; haffTree[x2].parent = n+i; haffTree[x1].flag = 1; haffTree[x2].flag = 1; haffTree[n+i].weight = haffTree[x1].weight+haffTree[x2].weight; haffTree[n+i].leftChild = x1; haffTree[n+i].rightChild = x2; } }
哈夫曼编码算法如下: void HaffmanCode(HaffNode haffTree[], int n, Code haffCode[]) //由n个结点的哈夫曼树haffTree构造哈夫曼编码haffCode { Code *cd = new Code; int child, parent; //求n个叶结点的哈夫曼编码 for(int i = 0; i < n; i++) { cd->start = n-1; //不等长编码的最后一位为n-1 cd->weight = haffTree[i].weight; //取得编码对应权值的字符 child = i; parent = haffTree[child].parent;
//由叶结点向上直到根结点 while(parent != 0) { if(haffTree[parent].leftChild == child) cd->bit[cd->start] = 0; //左孩子结点编码0 else cd->bit[cd->start] = 1; //右孩子结点编码1 cd->start--; child = parent; parent = haffTree[child].parent; }
//保存叶结点的编码和不等长编码的起始位 for(int j = cd->start+1; j < n; j++) haffCode[i].bit[j] = cd->bit[j]; haffCode[i].start = cd->start; haffCode[i].weight = cd->weight; //保存编码对应的权值 } }
下标 下标 weight weight leftChild leftChild rightChild rightChild parent parent flag flag 0 0 1 1 -1 -1 -1 -1 4 -1 1 0 1 1 3 3 -1 -1 -1 -1 -1 4 0 1 2 2 5 5 -1 -1 -1 -1 -1 -1 0 0 3 3 7 7 -1 -1 -1 -1 -1 -1 0 0 4 4 4 0 0 -1 1 -1 -1 -1 0 0 5 5 0 0 -1 -1 -1 -1 -1 -1 0 0 6 6 0 0 -1 -1 -1 -1 -1 -1 0 0 例:设有字符集{A, B, C, D},各字符在电文中出现的次数集为{1, 3, 5, 7},则哈夫曼树构造过程如下图所示: (a) (b)第一步的结果
下标 下标 weight weight leftChild leftChild rightChild rightChild parent parent flag flag 0 0 1 1 -1 -1 -1 -1 4 4 1 1 1 1 3 3 -1 -1 -1 -1 4 4 1 1 2 2 5 5 -1 -1 -1 -1 5 5 1 0 3 3 7 7 -1 -1 -1 -1 6 -1 0 1 4 4 4 4 0 0 1 1 5 5 1 1 5 5 9 9 4 4 2 2 6 -1 1 0 6 6 0 16 3 -1 5 -1 -1 -1 0 0 (c)第二步的结果 (d)哈夫曼树构造结果
1 0 0 7 1 1 0 1 7 3 1 1 8 5 0 9 7 0 1 2 3 4 5 6 7 8 9 bit start weight (e)哈夫曼编码结果
例7-5 设有字符集{A, B, C, D},各字符在电文中出现的次数集为{1, 3, 5, 7},设计各字符的哈夫曼编码。 程序设计如下: #include <iostream.h> #include <stdlib.h> const int MaxValue = 10000; //初始设定的权值最大值 const int MaxBit = 4; //初始设定的最大编码位数 const int MaxN = 10; //初始设定的最大结点个数 #include "HaffmanTree.h"
void main(void) { int i, j, n = 4; int weight[] = {1,3,5,7}; HaffNode *myHaffTree = new HaffNode[2*n+1]; Code *myHaffCode = new Code[n]; if(n > MaxN) { cout << "定义的n越界,修改MaxN! " << endl; exit(0); } Haffman(weight, n, myHaffTree); HaffmanCode(myHaffTree, n, myHaffCode);
//输出每个叶结点的哈夫曼编码 for(i = 0; i < n; i++) { cout << "Weight = " << myHaffCode[i].weight << " Code = "; for(j = myHaffCode[i].start+1; j < n; j++) cout << myHaffCode[i].bit[j]; cout << endl; } }
7.8 树与二叉树的转换 1.树转换为二叉树 树转换为二叉树的方法是: (1)树中所有相同双亲结点的兄弟结点之间加一条连线。 (2)对树中不是双亲结点第一个孩子的结点,只保留新添加的该结点与左兄弟结点之间的连线,删去该结点与双亲结点之间的连线。 (3)整理所有保留的和添加的连线,使每个结点的第一个孩子结点连线位于左孩子指针位置,使每个结点的右兄弟结点连线位于右孩子指针位置。
A A A A C D B B B C D B C D E C E F G E F G E F G D F G ( d ) ( a ) ( b ) ( c ) 树转换为二叉树的过程 (a)树;(b)相临兄弟加连线;(c)删除双亲与非第一个孩子连线;(d)二叉树
2.二叉树还原为树 二叉树还原为树的方法是: (1)若某结点是其双亲结点的左孩子,则把该结点的右孩子、右孩子的右孩子……都与该结点的双亲结点用线连起来。 (2)删除原二叉树中所有双亲结点与右孩子结点的连线。 (3)整理所有保留的和添加的连线,使每个结点的所有孩子结点位于相同层次高度。
A A A A B B B C D B E C E C E C G E F D D D F F F G G G ( a ) ( b ) ( c ) ( d ) 二叉树还原为树的过程 (a)二叉树;(b)双亲与非第一个孩子加连线; (c)删除结点与右孩子连线;(d)树
7.9 树的遍历 树的遍历操作是指按某种方式访问树中的每一个结点且每一个结点只被访问一次。树的遍历算法主要有先根遍历算法和后根遍历算法两种。因为树是递归定义的,因此树的遍历算法都可以设计成递归算法。 1.先根遍历 树的先根遍历递归算法为: (1)访问根结点; (2)按照从左到右的次序先根遍历根结点的每一棵子树。
A C D B E F H G I K L J 上图中所示树,先根遍历得到的结点序列为: A B E J F C G K L D H I 注意:树的先根遍历序列一定和该树转换的二叉树的先序遍历序列相同。
2.后根遍历 树的后根遍历递归算法为: (1)按照从左到右的次序后根遍历根结点的每一棵子树; (2)访问根结点。 上页图中所示树,后根遍历得到的结点序列为: J E F B K L G C H I D A 注意:树的后根遍历序列一定和该树转换的二叉树的中序遍历序列相同。