[数据结构]树和二叉树

来源:互联网 发布:网络压力测试工具 pre 编辑:程序博客网 时间:2024/06/04 00:22

第六章 树和二叉树

6.1树的类型定义

数据对象 D:D是具有相同特性的数据元素的集合。

数据关系 R:若D为空集,则称为空树 。否则:

  (1) 在D中存在唯一的称为根的数据元素root;

  (2) 当n>1时,其余结点可分为m (m>0)个互不相交的有限集T1, T2, …, Tm,其中每一棵

     子集本身又是一棵符合本定义的树,称为根root的子树。

 

 基本操作:

·查  找  类

·插  入  类

·删  除  类

查找类:

Root(T) // 求树的根结点

Value(T, cur_e) // 求当前结点的元素值

Parent(T, cur_e) // 求当前结点的双亲结点

LeftChild(T, cur_e) // 求当前结点的最左孩子

RightSibling(T, cur_e)  // 求当前结点的右兄弟

TreeEmpty(T)  // 判定树是否为空树

TreeDepth(T)  // 求树的深度

TraverseTree( T, Visit() )  // 遍历

插入类:

InitTree(&T)  // 初始化置空树

CreateTree(&T, definition)   // 按定义构造树

CreateTree(&T, definition)   // 按定义构造树

InsertChild(&T, &p, i, c)     // 将以c为根的树插入为结点p的第i棵子树

删除类:

ClearTree(&T) // 将树清空

DestroyTree(&T)  // 销毁树的结构

DeleteChild(&T, &p, i)  // 删除结点p的第i棵子树

 

树形图表示的例子

 

对比树型结构和线性结构的结构特点

线性结构                                     树型结构

第一个数据元素                                根结点

      (无前驱)                                  (无前驱)

 

最后一个数据元素                           多个叶子结点

       (无后继)                                (无后继)

 

其它数据元素                              其它数据元素    

(一个前驱、一个后继)                      (一个前驱、多个后继)

 

基  本  术  语

结点:  数据元素+若干指向子树的分支

结点的度:分支的个数

树的度:   树中所有结点的度的最大值

叶子结点:度为零的结点

分支结点:度大于零的结点

 

(从根到结点的)路径:由从根到该结点所经分支和结点构成

孩子结点、双亲结点

兄弟结点、堂兄弟

祖先结点、子孙结点

结点的层次:假设根结点的层次为1,第l 层的结点的子树根结点的层次为l+1

树的深度:树中叶子结点所在的最大层次

 

有向树:(1) 有确定的根;

        (2) 树根和子树根之间为有向关系。

有序树:子树之间存在确定的次序关系。

无序树:子树之间不存在确定的次序关系。

森林:是m(m≥0)棵互不相交的树的集合

任何一棵非空树是一个二元组Tree = (root,F)

其中:root 被称为根结点F 被称为子树森林

 

 

6.2 二叉树的类型定义

 二叉树或为空树,或是由一个根结点加上两棵分别称为左子树和右子树的、互不交的二叉树组成。

 

二叉树的五种基本形态:

 

 二叉树的主要基本操作:

·查  找  类

·插  入  类

·删  除  类

Root(T);    Value(T, e);    Parent(T, e);

LeftChild(T, e);     RightChild(T, e);

LeftSibling(T, e);     RightSibling(T, e);

BiTreeEmpty(T);      BiTreeDepth(T);

PreOrderTraverse(T, Visit());

InOrderTraverse(T, Visit());

PostOrderTraverse(T, Visit());

LevelOrderTraverse(T, Visit());

 

InitBiTree(&T);

Assign(T, &e, value);

CreateBiTree(&T, definition);

InsertChild(T, p, LR, c);

 

ClearBiTree(&T);

DestroyBiTree(&T);

DeleteChild(T, p, LR);

性质 1 : 在二叉树的第 i 层上至多有2i-1 个结点。         (i≥1)

用归纳法证明:

归纳基:i = 1 层时,只有一个根结点:2i-1 = 20 = 1;

归纳假设:假设对所有的 j,1≤ j < i,命题成立;

归纳证明:二叉树上每个结点至多有两棵子树,则第 i 层的结点数 = 2i-2´ 2 = 2i-1

性质 2 :深度为 k 的二叉树上至多含 2k-1 个结点(k≥1)。

性质 3 :对任何一棵二叉树,若它含有n0 个叶子结点、n2 个度为 2 的结点,则必存在关系式:n0 = n2+1。

证明:设 二叉树上结点总数 n = n0 + n1 + n2又 二叉树上分支总数 b = n1+2n2 而 b = n-1 = n0 + n1 + n2 - 1由此, n0 = n2 + 1 。

性质 4:

两类特殊的二叉树:

满二叉树:指的是深度为k且含有2k-1个结点的二叉树。

 

完全二叉树:树中所含的 n 个结点和满二叉树中编号为 1 至 n 的结点一一对应。

 

性质 5 :

若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点:
(1) 若 i=1,则该结点是二叉树的根,无双亲,

否则,编号为 ëi/2û 的结点为其双亲结点;
(2) 若 2i>n,则该结点无左孩子,
  否则,编号为 2i 的结点为其左孩子结点;
(3) 若 2i+1>n,则该结点无右孩子结点,
  否则,编号为2i+1 的结点为其右孩子结点。

 

6.3 二叉树的存储结构

一、 二叉树的顺序存储表示

#define  MAX_TREE_SIZE  100      

             // 二叉树的最大结点数

typedef TElemType  SqBiTree[MAX_

                TREE_SIZE];   

             // 0号单元存储根结点

SqBiTree  bt;

二、二叉树的链式存储表示

1. 二叉链表

 

C 语言的类型描述如下:

typedef struct BiTNode { // 结点结构

    TElemType      data;

    struct BiTNode  *lchild, *rchild;

                                     // 左右孩子指针

} BiTNode, *BiTree;

 

2.三叉链表

 

C 语言的类型描述如下:

     typedef struct TriTNode { // 结点结构

      TElemType       data;

      struct TriTNode  *lchild, *rchild;

                                     // 左右孩子指针

      struct TriTNode  *parent;  //双亲指针

   } TriTNode, *TriTree;

 

6.4二叉树的遍历

一、问题的提出

顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次。

“访问”的含义可以很广,如:输出结点的信息等。

“遍历”是任何类型均有的操作,对线性结构而言,只有一条搜索路径(因为每个结点均有一个后继),故不需要另加讨论。而二叉树是非线性结构,每个结点有两个后继,则存在如何遍历即按什么样的搜索路径遍历的问题。

对“二叉树”而言,可以有三条搜索路径:

1.先上后下的按层次遍历;

2.先左(子树)后右(子树)的遍历;

3.先右(子树)后左(子树)的遍历。

二、按层次遍历二叉树

实现方法为从上层到下层,每层中从左侧到右侧依次访问每个结点。下面我们将给出一棵二叉树及其按层次顺序访问其中每个结点的遍历序列。

按层次遍历该二叉树的序列为:    

A B E C F D G H K

 

三、先左后右的遍历算法

先(根)序的遍历算法

中(根)序的遍历算法

后(根)序的遍历算法

 

先(根)序的遍历算法:

若二叉树为空树,则空操作;否则,

(1)访问根结点;

(2)先序遍历左子树;

(3)先序遍历右子树。

void Preorder (BiTree T,

                  void( *visit)(TElemType& e))

{ // 先序遍历二叉树

   if (T) {

      visit(T->data);            // 访问结点

      Preorder(T->lchild, visit); // 遍历左子树

      Preorder(T->rchild, visit);// 遍历右子树

   }

}

 

中(根)序的遍历算法:

若二叉树为空树,则空操作;否则,

(1)中序遍历左子树;

(2)访问根结点;

(3)中序遍历右子树。

void Inorder (BiTree T,

                  void( *visit)(TElemType& e))

{ // 中序遍历二叉树

   if (T) {

      Inreorder(T->lchild, visit); // 遍历左子树

      visit(T->data);             // 访问结点

     Inreorder(T->rchild, visit);// 遍历右子树

   }

}

 

后(根)序的遍历算法:

若二叉树为空树,则空操作;否则,

(1)后序遍历左子树;

(2)后序遍历右子树;

(3)访问根结点。

void Postorder (BiTree T,

                  void( *visit)(TElemType& e))

{ // 后序遍历二叉树

   if (T) {

      Postreorder(T->lchild, visit); // 遍历左子树

      Postreorder(T->rchild, visit);// 遍历右子树

      visit(T->data);             // 访问结点

   }

}

 

 

 

四、中序遍历算法的非递归描述中序遍历示意图


算法一:

Status InorderTraverse(Bitree T,

Status(*Visit)(TElemType e)) {

      InitStack(S); Push(S,T);

      while(!StackEmpty(S)) {

while(GetTop(S,p) && p) Push(S,p->lchild);

Pop(S,p);

if (!StackEmpty(S)) {

             Pop(S,p);

             if(!Visit(p->data))  return ERROR;

    Push(S,p->rchild);}   }

        return OK;

}

算法二:

Status InorderTraverse(Bitree T,

Status(*Visit)(TElemType e)) {

    InitStack(S); p=T;

    while(p||!StackEmpty(S)){

       if(p) {Push(S,p); p=p->lchild;}

      else {

 Pop(S,p); if(!Visit(p->data)) return ERROR;

  p=p->rchild;

}

    }

    return OK;

}

五、遍历算法的应用举例

1、输入结点值,构造二叉树

算法基本思想:

 先序(或中序或后序)遍历二叉树,读入一个字符,若读入字符为空,则二叉树为空,若读入字符非空,则生成一个结点。将算法中“访问结点”的操作改为:生成一个结点,输入结点的值。

Status CreateBiTree (BiTree &T){

   scanf( &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);

} // CreateBiTree

2、统计二叉树中叶子结点的个数

算法基本思想:

先序(或中序或后序)遍历二叉树,在遍历过程中查找叶子结点,并计数。

由此,需在遍历算法中增添一个“计数”的参数,并将算法中“访问结点”的操作改为:若是叶子,则计数器增1。

void CountLeaf (BiTree T,  int& count){

   if ( T ) {

      if ((!T->lchild)&& (!T->rchild))

         count++;     // 对叶子结点计数

      CountLeaf( T->lchild, count);  

      CountLeaf( T->rchild, count);

   } // if

} // CountLeaf

3、求二叉树的深度(后序遍历)

算法基本思想:

首先分析二叉树的深度和它的左、右子树深度之间的关系。

从二叉树深度的定义可知,二叉树的深度应为其左、右子树深度的最大值加1。由此,需先分别求得左、右子树的深度,算法中“访问结点”的操作为:求得左、右子树深度的最大值,然后加1 。

int Depth (BiTree T ){ // 返回二叉树的深度

   if ( !T )    depthval = 0;

   else   {

     depthLeft = Depth( T->lchild );

     depthRight= Depth( T->rchild );

     depthval = 1 + (depthLeft > depthRight  ?

                               depthLeft : depthRight);

   }

   return depthval;

}

6.5线索二叉树

一、何谓线索二叉树?

遍历二叉树的结果是,求得结点的一个线性序列。

例如:

先序序列:  A B C D E F G H K

中序序列:   B D C A H G K F E

后序序列:   D C B H K G F E A

 

指向该线性序列中的“前驱”和“后继” 的指针,称作“线索”

·包含 “线索” 的存储结构,称作 “线索链表”

·与其相应的二叉树,称作 “线索二叉树”

对线索链表中结点的约定:

在二叉链表的结点中增加两个标志域,并作如下规定:

·若该结点的左子树不空,则Lchild域的指针指向其左子树,且左标志域的值为“指针 Link”或0;否则,Lchild域的指针指向其“前驱”,且左标志的值为“线索 Thread”或1 。

 

·若该结点的右子树不空,则rchild域的指针指向其右子树,且右标志域的值为 “指针 Link”或0;否则,rchild域的指针指向其“后继”,且右标志的值为“线索 Thread”或1。

 

如此定义的二叉树的存储结构称作“线索链表”。

 

线索链表的结点结构

 

ltag和rtag是增加的两个标志域,用来区分结点的左、右指针域是指向其左、右孩子的指针,还是指向其前驱或后继的线索。


线索链表的类型描述:

   typedef enum { Link, Thread } PointerThr;  // Link==0:指针,Thread==1:线索

 typedef struct BiThrNod {

   TElemType        data;

   struct BiThrNode  *lchild, *rchild;  // 左右指针

   PointerThr         LTag, RTag;    // 左右标志

} BiThrNode, *BiThrTree;

例如:

ABCDEFGHI


二、线索链表的遍历算法:

由于在线索链表中添加了遍历中得到的“前驱”和“后继”的信息,从而简化了遍历的算法。

for ( p = firstNode(T); p; p = Succ(p) )

      Visit (p);

例如:对中序线索化链表的遍历算法

中序遍历的第一个结点 ?

左子树上处于“最左下”(没有左子树)的结点。

在中序线索化链表中结点的后继 ?

若无右子树,则为后继线索所指结点;否则为对其右子树进行中序遍历时访问的第一个结点。

void InOrderTraverse_Thr(BiThrTree T,

                                  void (*Visit)(TElemType e)) {

 p = T->lchild;       // p指向根结点

 while (p != T) {     // 空树或遍历结束时,p==T

 while (p->LTag==Link)  p = p->lchild;  // 第一个结点

    Visit(p->data);

     while (p->RTag==Thread && p->rchild!=T)

        { p = p->rchild;  Visit(p->data); }  // 访问后继结点

     p = p->rchild;          // p进至其右子树根

  }

} // InOrderTraverse_Thr

三、如何建立线索链表?

在中序遍历过程中修改结点的左、右指针域,以保存当前访问结点的“前驱”和“后继”信息。遍历过程中,附设指针pre,  并始终保持指针pre指向当前访问的、指针p所指结点的前驱。

void InThreading(BiThrTree p) {

  if (p) {    // 对以p为根的非空二叉树进行线索化

    InThreading(p->lchild);      // 左子树线索化

    if (!p->lchild)      // 建前驱线索

      { p->LTag = Thread;    p->lchild = pre; }

    if (!pre->rchild)   // 建后继线索

      { pre->RTag = Thread;   pre->rchild = p; }

    pre = p;             // 保持 pre 指向 p 的前驱

    InThreading(p->rchild);      // 右子树线索化

  } // if

} // InThreading

Status InOrderThreading(BiThrTree &Thrt,

                    BiThrTree T) { // 构建中序线索链表

 if (!(Thrt = (BiThrTree)malloc(sizeof( BiThrNode))))

       exit (OVERFLOW);

Thrt->LTag = Link; Thrt->RTag =Thread; Thrt->rchild = Thrt;      

 if (!T)  Thrt->lchild = Thrt;

else {             

  Thrt->lchild = T;   pre = Thrt;

  InThreading(T);                 

  pre->rchild = Thrt;  pre->RTag = Thread;

  Thrt->rchild = pre; }  

 return OK;  

} // InOrderThreading

 

6.6  树和森林的表示方法

树的三种存储结构

一、双亲表示法:

 

C语言的类型描述:

#define MAX_TREE_SIZE  100

 

typedef struct PTNode {

      Elem  data;

      int    parent;   // 双亲位置域

   } PTNode;

树结构:

typedef struct {

     PTNode  nodes

                      [MAX_TREE_SIZE];

     int    r, n;     

            // 根结点的位置和结点个数

   } PTree;

二、孩子链表表示法:

 

C语言的类型描述:

孩子结点结构:

typedef struct CTNode {

     int          child;

     struct CTNode *next;

   } *ChildPtr;

双亲结点结构

 typedef struct {

     Elem    data;

     ChildPtr  firstchild;

                     // 孩子链的头指针

   } CTBox;

树结构:

typedef struct {

     CTBox  nodes[MAX_TREE_SIZE];

     int    n, r;     

           // 结点数和根结点的位置

   } CTree;

三、树的二叉链表 (孩子-兄弟)存储表示法

 

C语言的类型描述:

 

typedef struct CSNode{

     Elem          data;

     struct CSNode  

           *firstchild, *nextsibling;

} CSNode, *CSTree;

森林和二叉树的对应关系

设森林F = ( T1, T2, …, Tn );

     T1 = (root,t11, t12, …, t1m);

二叉树  

B =( LBT, Node(root), RBT );

由森林转换成二叉树的转换规则为:

若 F = Φ,则 B = Φ;否则,

   由 ROOT( T1 ) 对应得到 Node(root);

   由 (t11, t12, …, t1m ) 对应得到 LBT;

   由 (T2, T3,…, Tn ) 对应得到 RBT。

例如:

 

由二叉树转换为森林的转换规则为:

若 B = Φ, 则 F = Φ;否则,

由 Node(root) 对应得到 ROOT( T1 );

由LBT 对应得到 ( t11, t12, …,t1m);

由RBT 对应得到 (T2, T3, …, Tn)。

 

由此,树的各种操作均可对应二叉树的操作来完成。

应当注意的是,和树对应的二叉树,其左、右子树的概念已改变为: 左是孩子,右是兄弟。

 

 

6.7树和森林的遍历

一、树的遍历

树的遍历可有三条搜索路径:

先根(次序)遍历:

若树不空,则先访问根结点,然后依次先根遍历各棵子树。

后根(次序)遍历:

若树不空,则先依次后根遍历各棵子树,然后访问根结点。

按层次遍历:

若树不空,则自上而下自左至右访问树中每个结点。

先根遍历时顶点的访问次序:A B C E F G D

后根遍历时顶点的访问次序:B E G F C D A

层次遍历时顶点的访问次序:A B C D E F G

 

 

二、森林的遍历

森林由三部分构成:

1.森林中第一棵树的根结点;

2.森林中第一棵树的子树森林;

3.森林中其它树构成的森林。

 

森林的遍历

1.  先序遍历

若森林不空,则访问森林中第一棵树的根结点;先序遍历森林中第一棵树的子树森林;先序遍历森林中(除第一棵树之外)其余树构成的森林。

即:依次从左至右对森林中的每一棵树进行先根遍历。

2.  中序遍历

若森林不空,则中序遍历森林中第一棵树的子树森林;访问森林中第一棵树的根结点;中序遍历森林中(除第一棵树之外)其余树构成的森林。

即:依次从左至右对森林中的每一棵树进行后根遍历。

树的遍历和二叉树遍历的对应关系 ?

 

6.8  哈 夫 曼 树 与 哈 夫 曼 编 码

 一、最优树的定义

结点的路径长度定义为:从根结点到该结点的路径上分支的数目。

树的路径长度定义为:树中每个结点的路径长度之和。

结点的带权路径长度定义为:从根结点到该结点的路径长度与结点上权的乘积。

树的带权路径长度定义为:树中所有叶子结点的带权路径长度之和WPL(T) = Swklk (对所有叶子结点)。

例如:

在所有含 n 个叶子结点、并带相同权值的 m 叉树中,必存在一棵其带权路径长度取最小值的树,称为“最优树”。

 

WPL(T)= 7´2+5´2+2´2+4´2      =36

WPL(T)= 7´3+5´3+4´2+2´1      =46

WPL(T)= 7´1+5´2+2´3+4´3      =35

二、如何构造最优树

(哈夫曼算法)   以二叉树为例:

(1)根据给定的 n 个权值 {w1, w2, …, wn},构造 n 棵二叉树的集合

F = {T1,   T2,  … , Tn},其中每棵二叉树Ti中均只含一个带权值 为 wi 的根结点,其左、右子树为空树;

(2)在 F 中选取其根结点的权值为最小的两棵二叉树,分别作为左、右子树构造一棵新的二叉树,并置这棵新的二叉树根结点的权值为其左、右子树根结点的权值之和;

(3)从F中删去这两棵树,同时加入刚生成的新树;

(4)重复 (2) 和 (3) 两步,直至 F 中只含一棵树为止。这棵树便是哈夫曼树

例如: 已知权值 W={ 5, 6, 2, 9, 7 }

 

 

注意:

 ① 初始森林中的n棵二叉树,每棵树有一个孤立的结点,它们既是根,又是叶子
 ② n个叶子的哈夫曼树要经过n-1次合并,产生n-1个新结点。最终求得的哈夫曼树中共有2n-1个结点。

③ 哈夫曼树是严格的二叉树,没有度数为1的分支结点。

前缀编码

在电文传输中,需要将电文中出现的每个字符进行二进制编码。在设计编码时需要遵守两个原则:

(1)发送方传输的二进制编码,到接收方解码后必须具有唯一性,即解码结果与发送方发送的电文完全一样;

(2)发送的二进制编码尽可能地短。下面我们介绍两种编码的方式。

1. 等长编码

这种编码方式的特点:每个字符的编码长度相同。

设字符集只含有4个字符A,B,C,D,用两位二进制表示的编码分别为00,01,10,11若现在电文为:ABACCDA,则应发送二进制序列:00010010101100,总长度为14位。当接收方接收到这段电文后,将按两位一段进行译码。

这种编码的特点:译码简单且具有唯一性,但编码长度并不是最短的。

2. 不等长编码

在传送电文时,为了使其二进制位数尽可能地少,可以将每个字符的编码设计为不等长的,使用频度较高的字符分配一个相对比较短的编码,使用频度较低的字符分配一个比较长的编码。例如,可以为A,B,C,D四个字符分别分配0,00,1,01,并可将上述电文用二进制序列:000011010发送,其长度只有9个二进制位,但随之带来了一个问题,接收方接到这段电文后无法进行译码,因为无法断定前面4个0是4个A,1个B、2个A,还是2个B,即译码不唯一,因此这种编码方法不可使用。

哈夫曼编码

利用哈夫曼树可以构造一种不等长的二进制编码,并且构造所得的哈夫曼编码是一种最优前缀编码,即使所传电文的总长度最短。称为哈夫曼编码

哈夫曼编码的构造方法 :

(1)利用字符集中每个字符的使用频率作为权值构造一个哈夫曼树;

(2)从根结点开始,为到每个叶子结点路径上的左分支赋予0,右分支赋予1,并从根到叶子方向形成该叶子结点的编码。

例如:

假设有一个电文字符集中有8个字符,每个字符的使用频率分别为{0.05,0.29,0.07,0.08,0.14,0.23,0.03,0.11},现以此为例设计哈夫曼编码。为方便计算,将所有字符的频度乘以100,使其转换成整型数值集合,得到{5,29,7,8,14,23,3,11};

哈夫曼编码设计如下图:

 

哈夫曼树的存储结构

用一个大小为2n-1的向量来存储哈夫曼树中的结点,其存储结构为:

  typedef struct { //结点类型
      int  weight; //权值,不妨设权值均大于零
      int  lchild,rchild,parent;

                                     //左右孩子及双亲指针
    }HTNode,*HuffmanTree;

哈夫曼编码的存储结构

Typedef  char  **Huffmancode;

 

1. 熟练掌握二叉树的结构特性,了解相应的证明方法。

2. 熟悉二叉树的各种存储结构的特点及适用范围。

3. 遍历二叉树是二叉树各种操作的基础。实现二叉树遍历的具体算法与所采用的存储结构有关。掌握各种遍历策略的递归算法,灵活运用遍历算法实现二叉树的其它操作。层次遍历是按另一种搜索策略进行的遍历。

4. 理解二叉树线索化的实质是建立结点与其在相应序列中的前驱或后继之间的直接联系,熟练掌握二叉树的线索化过程以及在中序线索化树上找给定结点的前驱和后继的方法。二叉树的线索化过程是基于对二叉树进行遍历,而线索二叉树上的线索又为相应的遍历提供了方便。

5. 熟悉树的各种存储结构及其特点,掌握树和森林与二叉树的转换方法。建立存储结构是进行其它操作的前提,因此读者应掌握 1 至 2 种建立二叉树和树的存储结构的方法。

6. 学会编写实现树的各种操作的算法。

7. 了解最优树的特性,掌握建立最优树和哈夫曼编码的方法。

 

0 2
原创粉丝点击