860 likes | 958 Views
第五章 树及二叉树. 1 . 树的定义和术语 2 . 二叉树 : 定义、性质、存储 3 . 二叉树的遍历 4. 二叉树遍历的迭代器类 * 5. 中序穿线树 6. 最优二叉树及其应用 7. 树和森林. 高度为 4 、度为 3 的树. A. B. C. D. L. E. F. G. H. I. 树和森林. 树 : n > 0 个结点的集合,根 + 其余结点分为 m >= 0 个集合,每一个集合本身又是一棵树(子树) 结点的 度: 该结点的子树数目 树的度: 树中各结点度数的最大值 叶子、父结点、儿子结点、兄弟结点
E N D
第五章 树及二叉树 1.树的定义和术语2.二叉树:定义、性质、存储 3.二叉树的遍历4.二叉树遍历的迭代器类 *5.中序穿线树6.最优二叉树及其应用7.树和森林
高度为 4 、度为 3 的树 A B C D L E F G H I 树和森林 • 树:n > 0 个结点的集合,根+其余结点分为 m >= 0 个集合,每一个集合本身又是一棵树(子树) • 结点的度:该结点的子树数目 • 树的度:树中各结点度数的最大值 • 叶子、父结点、儿子结点、兄弟结点 • 祖先结点:从根结点到该结点的路径上所有结点 • 层次、高度:根为1, 依次往下数 • 有序树:规定所有结点的儿子结点次序,否则为无序树 • 森林:m >= 0 棵互不相交树的集合 其它表示方法: 1. ( A(B(L,E),C(F),D(G(I),H)) 2. 类似于书籍的目录表示法。 高度定义为层数或层数-1,都可以;本书定义为层数。
树和森林 ADT 5.1: 树的ADT 数据及关系: 具有相同数据类型的数据元素或结点的有限集合。树T的二元组形式为: T=(D,R) 其中D为树T中结点的集合,R为树中结点之间关系的集合。 D={Root}∪DF 其中,Root为树T的根结点,DF为树T的根Root的子树集合。 R={<Root,ri>,i=1,2,…,m} 其中,ri是树T的根结点Root的子树Ti的根结点。 操作: Constructor: 前提:已知根结点的数据元素之值。 结果:创建一棵树。 Getroot: 前提:已知一棵树。. 结果:得到树的根结点。 FirstChild: 前提:已知树中的某一指定结点p。 结果:得到结点p 的第一个儿子结点。 NextChild: 前提:已知树中的某一指定结点p 和它的一个儿子结点u。 结果:得到结点p 的儿子结点u 的下一个兄弟结点v。
B 例: 二叉树 C L D F E 例:结点总数为 3 时的所有二叉树的树的形状 二叉树的定义 • 空或有一个根,根有左子树、右子树;而左右子树本身又是二叉树。 • 和树的区别:可为空 • 左右子树有序,不可颠倒
A • 性质2:高度为 k 的二叉树至多有2 k- 1 个结点 C • 证:利用性质 1 ,将第 1 层至第 k 层的最多的结点数进行相加: • 1 + 2 + 22 + ………… + 2k-2 + 2k-1 = 2k - 1 L E F 二叉树的性质 • 性质1:在二叉树的第 i 层上至多有 2 i-1个结点 1层:结点个数 21-1=20个 2层:结点个数 22-1=21个 3层:结点个数 23-1=22个 A C L D B E F • 证:k = 1 时成立,设 k = i-1 时成立 则当 k = i 时在二叉树的第 i 层上至多有 • 2 i-1-1 * 2 = 2 i-1 个结点 • 性质3:二叉树的叶子结点数 n0 等于度为 2 的结点数n2 + 1 • 证:设度为 1 的结点数为 n1,树枝的总数为 B 则:B = n-1=n0+n1+n2-1 • 又 B = n1+2×n2; 原命题得证。
满二叉树: • 每层结点 • 数最多 A A C C L L B E F D B E F D P Q R S U V W X P Q R S U 完全二叉树 • 完全二叉树: • 1、满二叉树 • 2、从满二叉树最底层从右向左依次去掉若干结点形成的 • 性质4:具有 n 个结点的完全二叉树高度为 log2n + 1 • 证:根据性质 2 和完全二叉树的定义:设其高度为 k • 2k-1-1< n <= 2k -1 • 2k-1 < n + 1 <= 2k • 2k-1 <= n < 2k • 故:k-1 <= log2n < k ; 原命题得证。
完全二叉树 • 性质5:对一棵有 n 个结点的完全二叉树按照从第一层(根所在的层次〕到最后一层,并且 • 每一层都按照从左到右的次序进行编号。根结点的编号为 1,最后一个结点的编号 • 为 n。 • 1:对任何一个编号为 i 的结点而言,它的左儿子的编号为 2i( 若 2i <= n) ,而右儿子的 编号为 2i+1(若 2i +1 <= n)。 • 2:对任何一个编号为 j 的结点而言,它的父亲结点的的编号为 j/2 。根结点无父结 点。 • 证:对编号归纳 A 1 C L 3 2 B E F D 4 6 7 5 P Q R S U 8 10 11 12 9
data left right A 1 -1 B 9 2 C 5 3 D 6 -1 E -1 -1 F -1 -1 G 8 7 H -1 -1 I -1 -1 L -1 4 0 1 2 3 4 5 6 7 8 9 A B C L D F E G I H 二叉树的顺序存储 • 一般情况: • 应用范围:适用于二叉树上的结点个数已知,或不支持动态存储分配的高级语言。
二叉树的顺序存储 • 特殊情况:完全二叉树 data left right 0 1 2 3 4 5 6 7 8 9 10 0 1 2 3 4 5 6 7 8 9 A 2 3 B 4 5 C 6 7 D 8 9 E 10 0 F 0 0 G 0 0 H 0 0 I 0 0 L 0 0 A B C D E F G H I A B C D E F G H I L L • 应用范围:适用于完全二叉树,且结点个数已知。
二叉树的顺序存储 • 特殊情况:若不是完全二叉树,许多数据场为空,不合算。如下例所示: • 考虑浪费空间最多的情况,是一种什么情况? 浪费 2^k - 1 – k 个单元 0 1 2 3 4 5 6 7 8 9 A B ∧ D ∧ ∧ ∧ H I A B D H I
A B C L D F E G I H 二叉树的链接存储 • 仅定义结点的类型即可。结点之间的关系通过指针实现。 标准形式:(二叉链表) data left right 广义标准形式:(三叉链表) data left right Parent
A A B C B C ∧ ∧ D E ∧ D /\ E G F ∧ F ∧ ∧ G ∧ 二叉树的链接存储 • 例:
二叉树的ADT template <class Type> class BinaryTree; // 二叉树类BinaryTree 的向前说明 template <class Type> class BinaryNode { friend class BinaryTree < Type>; public: BinaryNode ( ) : left(NULL), right(NULL) { } // 二叉树结点的构造函数。 BinaryNode ( Type item, BinaryNode < Type> * L = NULL, BinaryNode < Type> * R = NULL ): data(item),left(L), right( R) { } ~BinaryNode ( ) { } TypeGetData ( ) const { return data;} // 得到二叉树结点的数据值。 BinaryNode<Type> * GetLeft( ) const { return left;} //得到二叉树结点的左儿子地址。 inaryNode<Type> * GetRight( ) const { return right; } //得到二叉树结点的左儿子地址。 void SetData( const Type & item ) { data = item; } // 设置二叉树结点的数据值。 void SetLeft(BinaryNode < Type> * L ) { left = L; } // 设置二叉树结点的左儿子地址。 void SetRight(BinaryNode < Type> * R ) { right = R; } // 设置二叉树结点的右儿子地址。
二叉树的ADT template <class Type> class BinaryTree; // 二叉树类BinaryTree 的向前说明 template <class Type> class BinaryNode { friend class BinaryTree < Type>; public: void PrintPreOrder( ) const; // 按前序打印二叉树的结点的数据值。 void PrintPostOrder( ) const; // 按后序打印二叉树的结点的数据值。 void PrintInOrder( ) const; // 按中序打印二叉树的结点的数据值。 BinaryNode<Type> * Duplicate( ) const; // 复制以本结点为根的子树。 private: BinaryNode < Type> * left , * right ; // 结点的左、右儿子的地址 Type data; // 结点的数据信息 };
二叉树的ADT template <class Type>class BinaryTree { public: BinaryTree( ) : root( NULL) { } // 构造空二叉树 BinaryTree ( const Type & value ) { root = new BinaryNode<Type> ( value ); } ~BinaryTree ( ) { DelTree ( root ); } int IsEmpty ( ) const { return root == NULL; } // 二叉树为空,返回非0,否则为0。 const BinaryNode<Type> * Getroot( )const { return root; } int MakeEmpty ( ) { DelTree( root); root == NULL; } // 使二叉树为空。 const BinaryTree<Type> & operator = ( const BinaryTree<Type> & T); private: BinaryNode<Type> * root; // 二叉树的根结点。 BinaryTree( const BinaryTree<Type> & ); void DelTree( BinaryNode<Type> * T ); }; template <class Type> const BinaryTree<Type> & BinaryTree<Type> :: operator = ( const BinaryTree<Type> & T ) { if ( this != &T ) { DelTree(root); // 其具体实现,见程序5.1。 if ( T.root != NULL ) root = T.root->Duplicate( ); } }
二叉树的ADT • ·二叉树的 ADT:求二叉树的结点个数和高度以及删除一棵二叉树。 template <class Type> Type Max( const Type u, const Type v ) { if ( u > v ) return u; else return v;} template <class Type> int BinaryNode < Type> :: Size ( const BinaryNode <Type> * T ) const { // 得到以T 为根的二叉树或子树的结点个数。 if ( T == NULL ) return 0; else return 1 + Size( T->left ) + Size( T->right); } template <class Type> int BinaryNode < Type> :: Height ( const BinaryNode < Type> * T ) const { // 得到以T 为根的二叉树的高度。 if ( T == NULL ) return 0; else return 1 + Max( Height( T->left ),Height( T->right)); }
二叉树的ADT • ·二叉树的 ADT:求二叉树的结点个数和高度以及删除一棵二叉树。 template <class Type> void BinaryTree<Type> :: DelTree ( BinaryNode < Type> * T ) { // 删除以T 为根的二叉树的所有结点。 if ( T != NULL ) { DelTree( T->left); DelTree( T->right); delete T; } }
A A L R L C L C L D B E R D B E R W L W R X X 二叉树遍历 • 设 N 代表根节点,L 代表左子树,R 代表右子树。 • a. 前序(或先序):如果二叉树为空,则操作为空:否则访问根结点;前序遍历左子树; • 前序遍历右子树。记为:NLR。 • b. 中序: 如果二叉树为空,则操作为空:否则中序遍历左子树;访问根结点; • 中序遍历右子树。记为:LNR。 • c. 后序: 如果二叉树为空,则操作为空:否则后序遍历左子树;后序遍历右子 • 树;访问根结点。记为:LRN。 前序:A、L、B、E、 C、D、W、X 中序:B、L、E、A、 C、W、X、D 后序:B、E、L、X、 W、D、C、A
二叉树遍历 • a. 前序分析:结点的左儿子、左孙子、左后代、…… 将连续输出。结点的右儿子将在结点、结点的左 子树全部输出之后才输出。 • b. 中序分析:最先输出的结点是根结点的最左的左后代。将二叉树中的结点投影到水平轴线上,则得到 中序遍历的序列。 • c. 后序分析:根结点(或子树的根结点)将在它的左、右子树的结点输出之后。因此,根结点(或子树 的根结点)在后序序列中的序号等于它的左右子树的结点个数 +左右子树中的最先被访问 的结点的序号。注意,结点、右父亲、右祖父、…… 将连续输出。 A A 前序:A、L、B、E、C、D、W、X L C 中序:B、L、E、A、C、W、X、D L C 后序:B、E、L、X、W、D、C、A B E D D B E W W X X B L E A C W X D
二叉树遍历的迭代器类 • 二叉树的迭代器:Tree Iterator ADT 5.3: 二叉树的迭代器类。 template <class Type> class TreeIterator { public: TreeIterator( const BinaryTree < Type > & BT ) : T( BT ), current( NULL) { } virtual ~TreeIterator ( ) { } virtual void First ( ) = 0; // 第一个被访问的结点地址送current virtual void operator ++ ( ) = 0; // 下一个被访问的结点地址送current int operator + ( )const{ return current != NULL;}//当前结点为空吗,非空返回True const Type & operator ( ) ( ) const; // 返回当前结点指针current 所指向的结点的数据值。 protected: const BinaryTree<Type > & T; const BinaryNode<Type > * current; // 指向当前结点的指针。 private: TreeIterator( const TreeIterator& ) { } const TreeIterator & operator = ( const TreeIterator& ); }; template <class Type> const Type&TreeIterator<Type> :: operator ( ) ( ) const { Exception( current == NULL, “This node is NULL!” ); return current->GetData( ); }
非递归前序遍历 • 前序的程序实现(非递归): • 1、根结点进栈 • 2、结点出栈,被访问 • 3、结点的右、左儿子(非空)进栈 • 4、反复执行 2、3 ,至栈空为止。 e.g: 前序:A、L、B、E、C、D A L C D B E D出栈访问后,栈空结束 C、L 进栈 E、B 进栈 A进栈 A出栈访问 L出栈访问 B出栈访问 E出栈访问 C出栈访问 D进栈 <B> <L> <E> <E> <A> <C> <C> <C> <C> <C> <D> 分析:p的前序的直接后继q: 1、p有左儿子,则q=该左儿子; 2、p无左儿子,有右儿子,则q=该右儿子; 3、 p无左儿子、右儿子 ,搜索其祖先结点的右儿子送q。找不到结束。
前序遍历的迭代器 • 遍历二叉树:Tree Iterator :前序的实现。 template <class Type> class Preorder : public TreeIterator < Type > { public: Preorder( const BinaryTree < Type > & R ); ~Preorder( ) { } void First( ); void operator ++ ( ); void Preorder_NLR ( ); protected: Stack < const BinaryNode<Type > * > s; }; template <class Type> Preorder<Type> ::Preorder( const BinaryTree <Type> & R ) : TreeIterator<Type >( R ) { s.Push( T.Getroot( ) ); } template <class Type> void Preorder <Type> ::First ( ) { s.MakeEmpty ( ); if ( T.Getroot( ) ) s.Push( T.Getroot( ) ); operator ++( ); } //堆栈清空。若二叉树T非空,则根结点进栈,并得到当前结点。
前序遍历的迭代器 • 遍历二叉树:Tree Iterator :前序的实现。 template <class Type> void Preorder <Type> :: operator ++ ( ) { if ( s.IsEmpty() ) { if ( current == NULL ) { cerr << “Advanced past end ” << endl; exit( 1 ) }; current = NULL; return; } current = s.Top( ); s.Pop( ); // 得到当前结点的地址,并进行出栈操作。 if ( current ->GetRight ( ) != NULL ) s.Push( current->GetRight( ) ); //非空右儿子进栈。 if ( current ->GetLeft ( ) != NULL ) s.Push( current->GetLeft( ) ); //非空左儿子进栈。 } template <class Type> void Preorder < Type > :: Preorder_NLR ( ) { First( ); // 将当前指针current 指向根结点。 while( operator +( ) ) { // 当前指针current 非空时继续执行。 cout << operator()() << endl; // 输出当前结点的数据场之值。 operator ++( ); // 使前序次序下的下一个结点成为当前结点。 } }
非递归后序遍历 • 遍历二叉树:设根结点初始时非空 • 后序算法:1、结点(初始时为根)进栈(标志0),沿左指针查找左儿子(标志1) • 2、左儿子非空,返回1。 • 3、退栈。若为标志1转 4 ,否则标志 2,转 5。 • 4、进栈转向右子树(标志2),如右儿子非空,则转向 1;空转向 3。 • 5、访问该结点,返回 3 • 6、反复 1 到 5 ,至栈空为止。 A e.g: 后序:E、L、D、C、A L C D E 分析:1、二叉树最先被访问的结点是二叉树中最左方的叶子。 2、p的后序的直接后继q: 1、p是父结点的右儿子,则q=该父结点。2、p是父结点的左 儿子,如果有右子树,则q=该右子树中最左的叶子。如果,无右子树,q=父结点。 3、p无父结点,那么q == NULL。
<B> <C> <B> <E> <B> <C> <B> <E> <D> <C> <B> <D> <E> <C> <B> <D> <C> <C> 1 2 2 0 1 2 2 0 1 2 2 1 1 0 1 2 1 0 <A> <A> <A> <A> <A> <A> <A> <A> <A> <A> <A> <A> <A> <A> <A> 2 1 1 2 1 1 1 1 0 2 1 2 2 2 2 非递归后序遍历 • 后序遍历时的实现实例;注意堆栈中保存的是当前结点的祖先。 A (1) (2) (3) (4) (5) B C (6) (7) (8) (9) (10) D E (11) (12) (13) (14) (15)
后序遍历的迭代器 • 遍历二叉树:Tree Iterator 后序的实现 template <class Type> struct StNode { const BinaryNode<Type > * Node; int TimesPop; StNode( const BinaryNode<Type > * N = NULL ): Node(N), TimesPop(0) { } }; template <class Type> class Postorder : public TreeIterator < Type > { public: Postorder( const BinaryTree < Type > & R ); ~Postorder( ) { } void First( ); // 后序遍历时的第一个结点的地址。 void operator ++ ( ); // 后序遍历时的下一个结点的地址。 protected: Stack < StNode<Type> > s; }; template <class Type> Postorder<Type> ::Postorder(const BinaryTree<Type> & R) : TreeIterator<Type> (R) { s.Push( StNode<Type>( T.Getroot( ) ) ); } template <class Type> void Postorder <Type> ::First ( ) { // 得到第一个访问的结点地址 s.MakeEmpty ( ); if ( T.Getroot( ) ) s.push ( StNode<Type>( T.Getroot( ) ); operator ++ ( ); } // 根结点地址及标志0 进栈,去寻找第一个被访问的最左方的叶子。
后序遍历的迭代器 • 遍历二叉树:Tree Iterator 后序的实现 template <class Type> void Postorder <Type>:: operator ++ ( ) { if ( s.IsEmpty() ){ // 当栈空且current 也为空时,遍历结束。 if ( current == NULL ) { cerr << “Advanced past end ” << endl; exit( 1 ); } current = NULL; return; } // 置当前指针为空,结束。 StNode<Type> Cnode; for ( ; ; ) { Cnode = s.Top( ); s.Pop( ); if ( ++ Cnode.TimePop == 3 ) { current = Cnode.Node; return; } //其左右子树处理完毕,该结点可访问。 s.Push( Cnode ); if ( Cnode. TimesPop == 1 ) // 转向访问左子树。 { if ( Cnode.Node -> GetLeft ( ) != NULL ) // 有左儿子,进栈。 s.Push(StNode<Type>( Cnode.Node -> GetLeft ( ) ) ); } else { // Cnode.TimePop == 2 访左结束,转向访问右子树。 if ( Cnode.Node -> GetRight ( ) != NULL ) s.Push(StNode<Type>( Cnode.Node -> GetRight ( ) ) ); } } }
非递归中序遍历 • 中序的程序实现: • 1、结点(初始时是根结点)进栈,沿左指针查找左儿子。 • 2、若有左儿子,返回第一步。 • 3、若无左儿子,判栈空?空则结束。非空,栈顶结点出栈 • 访问。转向右子树,返回1。 中序:B、L、E、A、C、W、X、D e.g: A 分析:1、二叉树最先被访问的结点是二叉树中最左方的 的左后代。 2、p的中序的直接后继q: 1、p有右子树,则q=p 的右子树的最左方的左后代。 2、p无右子树,如p是父结点的左儿子,则q=该 父结点。 3、p无右子树,如p是父结点的右儿子 则以p 的父结点为根的子树访问完毕。如该子树为其祖 先结点的左子树,那么q=其根。否则,继续寻 找,找不到结束。 L C D B E W X
中序遍历的迭代器 程序 5.5: 中序遍历迭代器类 template <class Type> class Inorder : public Postorder < Type > { public: Inorder( const BinaryTree < Type > & R ) : Postorder<Type>(R) { } void operator ++ ( ); // 中序时的下一个结点的地址。 }; template <class Type> void Inorder<Type>:: operator ++ ( ) { if ( s.IsEmpty() ){ // 当栈空且current 也为空时,遍历结束。 if ( current == NULL ) {cerr<< “Advanced past end ”<<endl;exit(1);} Cnode = NULL; return; } // 置当前指针为空,结束。 StNode<Type> Cnode; for ( ; ; ) { current = s.Top( ); s.Pop( ); if ( ++Cnode. TimesPop == 2 ) // 其左子树处理完毕,该结点可访问。 { current = Cnode.Node; if ( Cnode.Node -> GetRight ( ) != NULL ) // 有右儿子,进栈。 s.Push(StNode<Type>( Cnode.Node->GetRight() ) );return; } s.Push( Cnode); if ( Cnode.Node -> GetLeft ( ) != NULL ) s.Push(StNode<Type>( Cnode.Node->GetLeft() ) ); } }
A L C A D B E L C W D B E X W X 中序穿线树 • 为什么采用中序穿线树:二叉树中的空指针场多达 n + 1 个。 • 简单证明:设二叉树中结点的总数为 n,度为 2 的结点总数为 n2 。空指针场用 方框代表。增加了方 • 框结点的二叉树称之为扩展二叉树。在该二叉树之中,原来的 n 个结点全部成为度为 2 的结 • 点,方框结点成为叶子。根据二叉树的性质 3 ,原命题得证。 e.g: n = 8 的二叉树 扩展二叉树 • 怎样利用这 n + 1 个空指针场? • 中序穿线树 • 后序穿线树
中序穿线树的链接存储 • · • 中序穿线树(中序中序穿线树)的实现: • 在二叉树的结点的标准形式中,增加两个标志域,用于区别是否是穿线。如下所 • 示: ( 注意:在顺序存储一颗二叉树时,可不必使用额外的空间;见下页) • 其中 data 为数据场,leftThread = 0 时,leftchild 场指向本结点的真正的左儿子 • leftThread = 1 时,leftchild 场指向本结点的按中序遍历序列的前驱结点(穿线) • rightThread = 0 时,rightchild 场指向本结点的真正的右儿子 • rightThread = 1 时,rightchild 场指向本结点的按中序遍历序列的后继结点(穿线) leftchild leftThread data rightThread rightright e.g: n = 6 的二叉树 无前驱结点 无后继结点 A A 中序穿线树 L C L C D D B E B E
中序穿线树的链接存储 • · • 中序穿线树中的结点的数据结构: Root: 根结点指针 无前驱结点 无后继结点 A 0 A 0 表示 L C D 0 L 0 1 C 0 B E 1:穿线 0:实际儿子 1 B 1 1 E 1 1 D 1 ∧ ∧
data left right 0 A 1 A 2 5 C L 空 空 L 3 4 2 3 B 0 -2 B E D 4 E -2 -1 5 C 0 6 W 6 D 7 0 W -5 8 7 X X -7 -6 8 中序穿线树的顺序存储 • 中序穿线树的结点的顺序存储结构:
中序穿线树的中序遍历 • 在中序穿线树上进行中序遍历、不用堆栈的的算法及程序要点: • 1、最先输出最左的左后代结点 • 2、结点无右儿子,则该结点的中序的后继结点,由右儿子的指针场给出。但要注意以下情况: • 3、结点有右儿子,则该结点的中序的后继结点,是它的右子树中的最左的左后代结点。 • 4、最先输出的结点无前驱结点,最后输出的结点无后继结点。 无前驱结点 无后继结点 L A E B L C A D B E 指向当前结点 D 指向后继结点
中序穿线树的中序遍历 • · 中序穿线树遍历的实现算法。 template <class Type> int InorderThreadTree<Type> :: First ( ) { current = root; if ( !current ) return current;//为0是中序穿线二叉树空的标志。 while( arr[current].left >0 ) current = arr[current].left; return current; // 在中序次序下第一个被访问的结点的下标地址。 } template <class Type> int InorderThreadTree<Type> :: Next ( ) { aftcurrent = arr[current].right; if ( aftcurrent <= 0 ) { current = - aftcurrent; return current; } // 为0是中序遍历结束的标志,否则是中序次序下的下一个结点的地址。 while( arr[aftcurrent].left > 0 ) aftcurrent = arr[aftcurrent].left; current = aftcurrent; return current; //在中序次序下的下一个被访问的结点的地址。 } template <class Type> void InorderThreadTree<Type> :: Inorder ( ) { int p = First( ); while( p != 0 ) { cout << arr[p].data << endl; p = Next( ); } return; } root A L C D B E 无后继结点
中序穿线树 • · 中序穿线树在中序穿线树上插入新的结点的操作 • 本书中建立的实际上是中序穿线二叉排序树,所以新插入结点时必须寻找他的插入位置,即其父结点。考虑新插入的结点为父结点的右儿子的情况。新插入的结点为父结点的左儿子的情况,类似。 E E Q A U C 122 S D B D 250 99 新插入结点 D 作为叶子结点,其父结点的右儿子指针场应为非正数。 新插入结点 D 作为叶子结点,其父结点的左儿子指针场应为非正数。 300 200 110 E E Q A 105 230 U C 216 S B D D
中序穿线树 template <class Type> Istream & operator >> (istream & in, InorderThreadTree<Type> & x){ int size, j, k = 2;Type ele; cout << “Enter size of your InorderThreadTree array! ” << endl; in >> size; if (size > x.MaxSize ) { cout << “Error: out of spaces!”; return in; } cout << “ Enter data of every element one by one!” << endl; in >> ele; if (ele == x.flag) { cout << “Input data end!” << endl; return in; } x.arr[1].setdata (ele); x.root = 1; // 根结点单独生成。 while ( in >> ele, ele != x.flag) { j = x.root; x.arr[k].setdata (ele); while(1 ) { if ( x.arr[k].data < x.arr[j].data ) if ( x.arr[j].left > 0) j = x.arr[j].left; else {x.arr[k].left = x.arr[j].left; x.arr[k].right = -j; x.arr[j].left= k; break; } // 新结点k 插入后为结点j 的左儿子,是叶子。 else if ( x.arr[j].right > 0) j = x.arr[j].right; else {x.arr[k].right = x.arr[j].right; x.arr[k].left = -j; x.arr[j].right= k; break; } // 新结点k 插入后为结点j 的右儿子,是叶子。 } k++; } return in; }
最优二叉树 • 路径长度:结点之间的树枝的总数 • 树的路径长度:从根到每一结点的路径长度之和 • 树的带权路径长度:叶子结点的带权路径长度之和。设有 n 片叶子,它们的权值分别 • 为 w1、w2、…….wn, 相应的路径长度分别为 L1、L2、…….Ln。 • 则树的带权路径长度可记为: n • WPL =∑ wklk • k=1 G L 2 7 E L E G H H 5 7 5 2 4 4 WPL=46 WPL=35 G H WPL=36 L E 2 4 7 5 • 最优二叉树或赫夫曼树:树的带权路径长度 WPL 最小的二叉树。
4、A= b3,18 3、A= A, 7 b2,11 2、A= b1,6 A, 7 T, 5 T, 5 b1,6 C, 2 S, 4 C, 2 S, 4 赫夫曼算法 • 赫夫曼算法(产生最优二叉树的算法)的实现: • 1、给定一个具有 n 个权值 { w1,w2,………wn } 的结点的集合 • F = { T1,T2,………Tn } 。 • 2、初始时,设 A = F。 • 3、执行 i = 1 至 n-1 次循环,在每次循环时,做以下事情: • 从当前集合中选取权值最小、次最小的两个结点,以 这两个结点作为内部结 • 点 bi的左右儿子,bi 的权值为其左右儿子权值之和。在集合中删除这两个权 • 值最小、次最小的结点。这样,在集合 A 中,结点个数便减少了一个。 • e.g: 权值(此处为使用概率)分别为 { 2, 7, 4, 5 } 的结点集合 F= { C, A, S, T } 已知。 • 利用赫夫曼算法产生最优二叉树。 1、A= C, 2 A, 7 S, 4 T, 5
b3,18 0 1 b2,11 A, 7 0 1 T, 5 b1,6 1 0 C, 2 S, 4 赫夫曼编码 • 赫夫曼算法用于通信中的字符的编码。权值为使用概率。赫夫曼树的左分支 标记为 • 0,而右分枝标记为 1;这从根到每一个叶子结点的路经上标记的字符组成的字符串,即为该字符的赫夫曼编码。 • e.g: 权值(此处为使用概率)分别为 { 2, 7, 4, 5 } 的结点集合 F= { C, A, S, T } 已知。 • 请利用赫夫曼算法产生最优二叉树。 赫夫曼编码: A:0 T:10 C:110 S :111 赫夫曼编码优点: 占用二进制位少 e.g: 左图发送长度为 n 的字符串,等长表示需 2n 个比特。因共有四个字符,表示每个字符需 二个 比特。 采用赫夫曼编码后,总的比特数 35n/18, 因: A:1*7n /18 T: 2*5n /18 S: 3*4n /18 C:3*2n /18
最小化堆简介 • 堆:n 个元素的序列 { k1, k2,…… , kn },当且仅当满足以下关系时,称 之为堆: • ki <=k2i ki >=k2i • ki <=k2i+1 ki >=k2i+1 • ( i = 1,2, …… , n/2 ) • 且分别称之为最小化堆和最大化堆。 • e.g: 序列 { 96,83,27,11,9} 最大化堆 • { 12,36,24,85,47,30,53,91} 最小化堆 或 1 1 12 96 2 3 2 3 36 24 83 27 4 5 6 7 4 5 85 47 30 53 11 9 8 91
7 最小化堆的最小元素 root 62 16 24 50 88 77
7 1 50 5 77 7 最小化堆的顺序存储 [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] root 7 16 62 24 50 88 77 62 3 16 2 24 4 88 6
7 1 50 5 77 7 建堆的复杂性 • 建堆的时间耗费:比较次数 -4n 次 O(n) 级 (课本第 241页) [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] 7 16 62 24 50 88 77 root 62 3 16 2 24 4 88 6
50 1 7 5 77 7 建最小化堆 [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] 数组 h 50 16 62 24 7 88 77 root 符合堆的定义,不需调整。即最小化小堆已符合定义。建立的第一个小堆的堆顶的下标为 n/2 62 3 16 2 24 4 88 6 叶子结点,符合堆的的定义。
50 1 7 5 77 7 建最小化堆 [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] 数组 h 50 16 62 24 7 88 77 root 不合堆的定义,需调整。建立以 L[ 2 ] 为堆顶的正确的最小化小堆。 62 3 16 2 24 4 88 6
50 1 16 5 77 7 建最小化堆 [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] 数组 h 50 7 62 24 16 88 77 root 不合堆的定义,需调整。建立以 L[ 2 ] 为堆顶的正确的最小化小堆。 62 3 7 2 24 4 88 6
50 1 16 5 77 7 建最小化堆 [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] 不合堆的定义,需调整。建立以 L[ 1 ] 为堆顶的正确的最小化堆。 数组 h 50 7 62 24 16 88 77 root 62 3 7 2 24 4 88 6
7 1 16 5 77 7 建最小化堆 [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ] [ 6 ] [ 7 ] 不合堆的定义,需调整。建立以 L[ 1 ] 为堆顶的正确的最小化堆。 数组 h 7 50 62 24 16 88 77 root 62 3 50 2 24 4 88 6
1 10 1 2 3 60 12 2 6 4 7 5 40 30 8 70 3=h 建堆的复杂性 • 时间耗费的代价:建堆的时间耗费 + 排序的时间耗费 • 建堆的时间耗费:设树根处于第 1 层,该堆共有 h 层。建堆从第 h-1 层开始进行。只 要知道了每一层的结点数(建的小堆的个数),和每建一个小堆所 需的比较次数,就可以求得建堆的时间耗费。 • 建的小堆的性质:第 i 层上小堆的个数 = 第 i 层上结点的个数 = 最多 2i-1 • 第 i 层上小堆的高度 = h-i+1 • 建第 i 层上每个小堆最多所需的比较次数 = 2×(h-i)次 • 建堆的时间耗费: • T(n) <= ∑ 2i-1×(h-i)×2 = ∑ 2i×(h-i) = ∑ 2h-j×j • = 2h ∑ j/2j • 注意:即使是 高度为 h 的完全的二叉树,满足以下式子: • 2h-1 <= n <= 2h-1 故: 2h <= 2n • 又: ∑ j/2j < 2 • 所以:建堆的时间复杂性为 4n=O(n) 级 h-1 1 1 i=h-1 j=1 i=h-1 h-1 j=1 ∞ j=1