数据结构 学习总结6 树和二叉树

特点:非线性结构,一个直接前驱,但可能有多个直接后继(1:n)

树在计算机领域中广泛应用,例如:
在编译程序中,用树来表示源程序的语法结构;
在数据库系统中,可用树来组织信息;
在分析算法的行为时,可用树来描述其执行过程等等。

树的基本概念

由0个或多个(n≥0)结点组成的有限集合T,有且仅有一个结点称为根(root),当n>1时,其余的结点分为m(m≥0)个互不相交的有限集合T1,T2,…,Tm。每个集合本身又是棵树,被称作这个根的子树 。

注1:过去许多书籍中都定义树为n≥1,曾经有“空树不是 树”的说法,但现在树的定义已修改。
注2:树的定义具有递归性,即树中还有树。

树的抽象数据类型定义

ADT Tree{
数据对象D:D是具有相同特性的数据元素的集合。
数据关系R:
若D为空集,则称为空树;//允许n=0
若D中仅含一个数据元素,则R为空集;
其他情况下的R存在二元关系:
① root 唯一 //关于根的说明
② Dj∩Dk= Φ //关于子树不相交的说明
③ …… //关于数据元素的说明

基本操作 P:…
}ADT Tree

在这里插入图片描述
在这里插入图片描述

树的逻辑结构
(特点): 一对多(1:n),有多个直接后继(如家谱树、目录树等等),但只有一个根结点,且子树之间互不相交。

树的存储结构
树是非线性结构,该怎样存储?
————仍然有顺序存储、链式存储等方式

树的顺序存储方案应该怎样制定
可规定为:从上至下、从左至右将树的结点依次存入内存。
重大缺陷:复原困难(不能唯一复原就没有实用价值)。

树的链式存储方案应该怎样制定?
可用多重链表:一个前趋指针,n个后继指针。
细节问题:树中结点的结构类型样式该如何设计?
即应该设计成“等长”还是“不等长”?
缺点:等长结构太浪费(每个结点的度不一定相同);
不等长结构太复杂(要定义好多种结构类型)。
解决思路:先研究最简单、最有规律的树,然后设法把一般的树转化为简单树。

**要明确:

  1. 普通树(即多叉树)若不转化为二叉树,则运算很难实现。
  2. 二叉树的运算仍然是插入、删除、修改、查找、排序等,但这些操作必须建立在对树结点能够“遍历”的基础上!
    (遍历——指每个结点都被访问且仅访问一次,不遗漏不重复)。**

二叉树

定 义:是n(n≥0)个结点的有限集合,由一个根结点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成 。
逻辑结构: 一对二(1:2)
基本特征:
① 每个结点最多只有两棵子树(不存在度大于2的结点);
② 左子树和右子树次序不能颠倒(有序树)。
基本形态:

二叉树的性质

在这里插入图片描述

在这里插入图片描述

满二叉树的特点:
基本特点是每一层上的结点数总是最大结点数。
◆ 满二叉树的所有的支结点都有左、右子树。
◆ 可对满二叉树的结点进行连续编号,若规定从根结点开始,按“自上而下、自左至右”的原则进行。
在这里插入图片描述

完全二叉树:深度为k 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k 的满二叉树中编号从1至n的结点一一对应

完全二叉树的特点:
只有最后一层叶子不满,且全部集中在左边。
深度为K的完全二叉树,所有叶子结点都出现在第K层或K-1层。
为何要研究这两种特殊形式?
因为它们在顺序存储方式下可以复原!

在这里插入图片描述

二叉树的存储结构

顺序存储结构
按二叉树的结点“自上而下、从左至右”编号,用一组连续的存储单元存储。

顺序存储后能否复原成唯一对应的二叉树形状?
答:若是完全/满二叉树则可以做到唯一复原。
而且有规律:下标值为i的双亲,其左孩子的下标值必为2i,其右孩子的下标值必为2i+1(即性质5)
例如,对应[2]的两个孩子必为[4]和[5],即B的左孩子必是D,右孩子必为E。
不是完全二叉树怎么办?
一律转为完全二叉树!
方法很简单,将各层空缺处统统补上“虚结点”,其内容为空
缺点:①浪费空间;②插入、删除不便

链式存储结构用二叉链表即可方便表示。

一般从根结点开始存储。(相应地,访问树中结点时也只能从根开始)
注:如果需要倒查某结点的双亲,可以再增加一个双亲域(直接前趋)指针,将二叉链表变成三叉链表。

typedef struct node *tree_pointer;
typedef struct node {
 int data;
 tree_pointer  left_child, right_child;
} node;

遍历二叉树和线索二叉树

遍历定义——指按某条搜索路线遍访每个结点且不重复(又称周游)。
遍历用途——它是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心。
遍历方法——牢记一种约定,对每个结点的查看都是“先左后右” 。

二叉树由根、左子树、右子树构成,定义为D、 L、R
D、 L、R的组合定义了六种可能的遍历方案:
LDR, LRD, DLR, DRL, RDL, RLD
若限定先左后右,则有三种实现方案:
DLR LDR LRD
先 (根)序遍历 中 (根)序遍历 后(根)序遍历
注:“先、中、后”的意思是指访问的结点D是先于子树出现还是后于子树出现。

DLR—先序遍历,即先根再左再右
LDR—中序遍历,即先左再根再右
LRD—后序遍历,即先左再右再根

先序遍历算法
DLR( liuyu *root )
{ if (root !=NULL) //非空二叉树
   { cout<<root->data; //访问D
      DLR(root->lchild); //递归遍历左子树
      DLR(root->rchild); //递归遍历右子树
   }  return(0); 
}
中序遍历算法
  LDR(x*root)
   { if(root !=NULL)
     { LDR(root->lchild);
       cout <<root->data;
       LDR(root->rchild); 
      } 
   return(0);
   }
后序遍历算法
  LRD (x*root)
  { if(root !=NULL) 
    { LRD(root->lchild);
      LRD(root->rchild);
      cout<<root->data; 
     } 
  return(0);
 }

从前面的三种遍历算法可以知道:如果将print语句抹去,从递归的角度看,这三种算法是完全相同的,或者说这三种遍历算法的访问路径是相同的,只是访问结点的时机不同。

二叉树遍历的时间效率和空间效率
时间效率:O(n) //每个结点只访问一次
空间效率:O(n) //栈占用的最大辅助空间
(精确值:树深为k的递归遍历需要k+1个辅助单元!)

编写递归算法,计算二叉树中叶子结点的数目

思路:输出叶子结点比较简单,用任何一种遍历算法,凡是左右指针均空者,则为叶子,将其统计并打印出来。

DLR(liuyu *root)     //采用先序遍历的递归算法
{ if ( root!=NULL )  //非空二叉树条件,还可写成if(root)
 {if(!root->lchild&&!root->rchild)  //是叶子结点则统计并打印
   {  sum++; 
       cout<<root->data;}
    DLR(root->lchild);      //递归遍历左子树,直到叶子处;
    DLR(root->rchild); } //递归遍历右子树,直到叶子处;
} return(0);  }

建树
思路:利用前序遍历来建树 (结点值陆续从键盘输入,用DLR为例)

void createBTpre(Bintree &T)
{   char ch;
     cin>>ch;
     if(ch == ’#’)
         T=NULL; 
     else 
     {    T = new BinTNode;
           T->data = ch;
           createBTpre(T->lchild);
           createBTpre(T->rchild); 
       }
}

求二叉树深度,或从x结点开始的子树深度。

算法思路:只查各结点后继链表指针,若左(右)孩子
的左(右)指针非空,则层次数加1;否则
函数返回。
当T= NULL时,深度为0;
否则, T的深度= MAX{左子树深度,右子树深度}+1;

按层次输出二叉树中所有结点。
算法思路:既然要求从上到下,从左到右,则利用队列存放各子树结点的指针是个好办法,而不必拘泥于递归算法。
技巧:当根结点入队后,令其左、右孩子结点入队,而根结点以外的结点出队时又令它的左右孩子结点入队,……由此便可产生按层次输出的效果。

中序遍历的非递归(迭代)算法
算法思路:若不用递归,则要实现二叉树遍历的“嵌套”规则,必用堆栈。

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 );   // 将右儿子入栈,则下次循环时打印右儿子
           } // if
     } //while  
    return  OK;
} // InOrderTraverse 

判别给定二叉树是否为完全二叉树(即顺序二叉树)
算法思路:完全二叉树的特点是:没有左子树空而右子树单独存在的情况(前k-1层都是满的,且第k层左边也满)。
技巧: 按层序遍历方式,先把所有结点(不管当前结点是否有左右孩子)都入队列.若为完全二叉树,则层序遍历时得到的肯定是一个连续的不包含空指针的序列.如果序列中出现了空指针,则说明不是完全二叉树。

若已知先序/后序遍历结果和中序遍历结果,能否“恢复”出二叉树?
证明:由一棵二叉树的先序序列和中序序列可唯一确定这棵二叉树。

例:已知一棵二叉树的中序序列和后序序列分别是BDCEAFHG 和 DECBHGFA,请画出这棵二叉树。
分析:
①由后序遍历特征,根结点必在后序序列尾部(即A);
②由中序遍历特征,根结点必在其中间,而且其左部必全部是左子树子孙(即BDCE),其右部必全部是右子树子孙(即FHG);
③继而,根据后序中的DECB子树可确定B为A的左孩子,根据HGF子串可确定F为A的右孩子;以此类推。

用二叉链表法(l_child, r_child)存储包含n个结点的二叉树,结点的指针区域中会有多少个空指针? N+1

分析:用二叉链表存储包含n个结点的二叉树,结点必有2n个链域(见二叉链表数据类型说明)。
除根结点外,二叉树中每一个结点有且仅有一个双亲(直接前驱),所以只会有n-1个结点的链域存放指针,指向非空子女结点(即直接后继)。
所以, 空指针数目=2n-(n-1)=n+1个

线索二叉树(Threaded Binary Tree)

普通二叉树只能找到结点的左右孩子信息,而该结点的直接前驱和直接后继只能在遍历过程中获得。
若将遍历后对应的有关前驱和后继预存起来,则从第一个结点开始就能很快“顺藤摸瓜”而遍历整个树了。

例如中序遍历结果:B D C E A F H G,实际上已将二叉树转为线性排列,显然具有唯一前驱和唯一后继。

1)若结点有左子树,则lchild指向其左孩子;
否则, lchild指向其直接前驱(即线索)
2)若结点有右子树,则rchild指向其右孩子;
否则, rchild指向其直接后继(即线索) 。
在这里插入图片描述

为区别两种不同情况,特增加两个标志域(各1bit)
约定:当Tag域为0时,表示正常情况;
当Tag域为1时,表示线索情况.
线索链表:用含Tag的结点样式所构成的二叉链表
线 索:指向结点前驱和后继的指针
线索二叉树:加上线索的二叉树
线 索 化:对二叉树以某种次序遍历使其变为线索二叉树的过程

讨论:增加了前驱和后继等线索有什么好处?
——能方便找出当前结点的前驱和后继,不用堆栈也能遍历整个树。

线索化过程就是在遍历过程中修改空指针的过程:
将空的lchild改为结点的直接前驱;
将空的rchild改为结点的直接后继。

在这里插入图片描述
目的:在依某种顺序遍历二叉树时修改空指针,添加前驱或后继
注解:为方便添加结点的前驱或后继,需要设置两个指针:
p指针→当前结点之指针; pre指针→前驱结点之指针
技巧:当结点p的左/右域为空时,只改写它的左域(装入前驱pre),而其右域(后继)留给下一结点来填写。
或者说,当前结点的指针p应当送到前驱结点的空右域中

若p->lchild=NULL,则{p->Ltag=1;p->lchild=pre;}
//p的前驱结点指针pre存入左空域
若pre->rchild=NULL, 则{pre->Rtag=1;pre->rchild=p;}
//p存入其前驱结点pre的右空域

void InorderThreading(BiThrTree  & Thrt, BiThrTree  T)
{ //中序遍历二叉树T,并将其中序线索化, Thrt 指向头结点.
   if ( ! (Thrt = new BiThrnode) )
     exit ( OVERFLOW ) ;
   Thrt ->LTag = Link; // 建头结点 Link =0, Thead=1
   Thrt ->RTag = Thead; 
   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;
            }
} // InOrderThreading

void InThreading (BiThrTree p)
 { if (p)
    {   InThreading( p->lchild );  // 左子树线索化
        if ( !p->lchild )     // 前驱线索
         { p->LTag=Thread; 
           p->lchild=pre; }
        else p->LTag=Link;
       if ( !pre->rchild )
         { pre->RTag=Thread; pre->rchild=p; } //后继线索
       else p->RTag=Link
        pre = p;                         // 保持pre指向p的前驱
        InThreading(p->rchild);      //右子树线索化
     }
  } // InThreading

线索二叉树的遍历
理论上,只要找到序列中的第一个结点,然后依次访问结点的后继直到后继为空时结束。
但是,在线索化二叉树中,并不是每个结点都能直接找到其后继的,当标志为0时,R_child=右孩子地址指针,并非后继!需要通过一定运算才能找到它的后继。
以中序线索二叉树为例:
对叶子结点(RTag=1),直接后继指针就在其rchild域内;
对其他结点(RTag=0),直接后继是其右子树最左下的结点;
(因为中序遍历规则是LDR,先左再根再右)

线索二叉树的中序遍历算法
程序注解 (非递归,且不用栈):


P=T->lchild;   //从头结点进入到根结点;
while( p!=T)           // 空树或遍历结束时,p==T
{   while(p->LTag==link)  p=p->lchild;  //先找到中序遍历起点
     cout << p->data;  //访问最左结点
     while(p->RTag==Thread && p->rchild!=T) //p->rchild=T即为
     { p=p->rchild;                                                   // 最后一个结点
        cout << p->data;}
     //若有后继标志,则直接提取p->rchild中线索并访问后继结点;
      p=p->rchild;  //当前结点右域不空或已经找好了后继,则一律从结点的右子树开始重复{  }的全部过程。
 }

树和森林

树有三种常用存储方式:
①双亲表示法 ②孩子表示法 ③孩子兄弟表示法

(1)用双亲表示法来存储
思路:用一组连续空间来存储树的结点,同时在每个结点中附设一个指示器,指示其双亲结点在链表中的位置。

缺点:求结点的孩子时需要遍历整个结构。

(2)用孩子表示法来存储
思路:将每个结点的孩子排列起来,形成一个带表头(装父结点)的线性表(n个结点要设立n个链表);
再将n个表头用数组存放起来,这样就形成一个混合结构。

(3)用孩子兄弟表示法来存储

思路:用二叉链表来表示树,但链表中的两个指针域含义不同。
左指针指向该结点的第一个孩子;
右指针指向该结点的下一个兄弟结点。

问:树转二叉树的“连线—抹线—旋转” 如何由计算机自动实现?
答:用“左孩子右兄弟”表示法来存储即可。
存储的过程就是转换的过程!

讨论:树如何转为二叉树?
转换步骤:
step1: 将树中同一结点的兄弟相连;兄弟相连
step2: 保留结点的最左孩子连线,删除其它孩子连线;长兄为父
step3: 将同一孩子的连线绕左孩子旋转45度角。 孩子靠左

森林如何转为二叉树?

即F={T1, T2, …,Tm} B={root, LB, RB}

法一:
① 各森林先各自转为二叉树;
② 依次连到前一个二叉树的右子树上。
法二:森林直接变兄弟,再转为二叉树

树的遍历
先根遍历:先访问树的根节点,然后依次先根遍历根的每棵子树;

后根遍历:先依次后根遍历根的每棵子树,然后访问根节点;

森林的遍历

先序遍历
若森林为空,返回;
访问森林中第一棵树的根结点;
先序遍历第一棵树中根结点的子树森林;
先序遍历除去第一棵树之后剩余的树构成的森林。
中序遍历
若森林为空,返回;
中序遍历森林中第一棵树的根结点的子树森林;
访问第一棵树的根结点;
中序遍历除去第一棵树之后剩余的树构成的森林。

Huffman树及其应用

最优二叉树(霍夫曼树)
术语
路 径
路径长度
树的路径长度
带权路径长度
树的带权路径长度
霍 夫 曼 树
哈夫曼树是:WPL 最小的树。
构造霍夫曼树的基本思想:
权值大的结点用短路径,权值小的结点用长路径
构造Huffman树的步骤(即Huffman算法):

(1) 由给定的 n 个权值{w0, w1, w2, …, wn-1},构造具有 n 棵扩充二叉树的森林F = { T0, T1, T2, …, Tn-1 },其中每一棵扩充二叉树 Ti 只有一个带有权值 wi 的根结点,其左、右子树均为空。
(2) 重复以下步骤, 直到 F 中仅剩下一棵树为止:
① 在 F 中选取两棵根结点的权值最小的扩充二叉树, 做为左、右子树构造一棵新的二叉树。置新的二叉树的根结点的权值为其左、右子树上根结点的权值之和。
② 在 F 中删去这两棵二叉树。
③ 把新的二叉树加入 F。

例1:设有4个字符d,i,a,n,出现的频度分别为7,5,2, 4,怎样编码才能使它们组成的报文在网络中传得最快?
法1:等长编码。例如用二进制编码来实现。
取 d=00,i=01,a=10,n=11
法2:不等长编码,例如用哈夫曼编码来实现。
取 d=0; i=10, a=110, n=111
霍夫曼编码的基本思想是:概率大的字符用短码,概率小的用长码。由于霍夫曼树的WPL最小,说明编码所需要的比特数最少。这种编码已广泛应用于网络通信中。

在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 期末复习数据结构讲义pdf是指一份包含数据结构相关知识的教材或讲义的电子文档。 首先,这份讲义是为期末考试而准备的,因此它会涵盖这门课程所讲授的数据结构的主要概念和内容。它通常会包括各种数据结构的定义、特性、操作等内容,例如数组、链表、栈、队列、、图等等。此外,这份讲义还可能包括一些与数据结构相关的算法和问题,如排序算法、查找算法、遍历算法等。 其次,这份讲义的目的是帮助学生复习数据结构的知识,提供一个系统化的学习资料。因此,它通常会按照逻辑顺序组织,从基础的概念开始,逐渐深入,直至较为复杂的内容。学生可以通过研读这份讲义,回顾和巩固课堂上所学的内容,理解数据结构的原理和应用。 此外,这份讲义可能会包含一些实例和习题,以帮助学生加深对数据结构知识的理解和应用能力。这些实例和习题可以用来训练学生解决实际问题的能力,并提供一些思路和方法。 最后,这份讲义以pdf格式呈现,具有电子化的特点。学生可以方便地通过电脑、平板电脑或手机等设备随时随地查阅,并进行标注、批注等操作,方便复习和学习。 总之,期末复习数据结构讲义pdf是一份针对数据结构课程的复习资料,通过系统、全面地总结数据结构的相关知识,帮助学生进行复习,并提供了一些实例和习题,方便学生加深对数据结构的理解和应用。 ### 回答2: 期末复习数据结构讲义pdf是一份非常重要的学习资料。在期末考试前进行复习时,它可以作为一个很好的参考工具。 首先,数据结构是计算机科学中的一门核心课程,涉及到很多基础的知识和概念。这份讲义中记录了数据结构的各种基本概念、定义和性质,以及常见的数据结构类型,如数组、链表、栈、队列、等。对于复习阶段来说,这些内容对于回顾和加深理解非常有帮助。 此外,这份讲义还包括了数据结构的一些重要算法和操作,例如查找、排序和插入等。这些算法是数据结构中的关键,理解它们的原理和实现方式对于提高代码效率和解决实际问题至关重要。 最后,这份讲义可能还包括一些实例或编程题目,供学生进行练习和巩固应用知识。这是非常有用的,因为通过实际操作和编程实践,学生可以更深入地理解数据结构的概念和应用,并提升自己的编程能力。 总之,期末复习数据结构讲义pdf是一份极其重要的学习资料,它汇集了数据结构的基本概念、算法和实例,为学生提供了一个全面深入的复习和巩固知识的工具。我们应该认真阅读、理解和应用这份讲义,希望能够在期末考试中取得优异的成绩。 ### 回答3: 数据结构是计算机科学中的重要基础课程,掌握数据结构对于学习和应用计算机算法具有至关重要的作用。期末复习数据结构讲义PDF是一种非常有效的学习资料。以下是对该讲义的回答: 期末复习数据结构讲义PDF对学习数据结构有很大帮助。首先,该讲义系统地总结了各种常见的数据结构及其应用,如链表、栈、队列、二叉树、图等。通过讲义中的讲解和示例,可以清晰地了解每种数据结构的定义、特点和操作。 其次,该讲义提供了大量的例题和练习题,能够帮助学生巩固对数据结构的理论知识和运用能力。通过讲义中的习题,学生可以对所学知识进行实际的应用,深化对数据结构的理解,并培养解决实际问题的能力。 此外,该讲义还包含了一些常见算法的讲解,如排序、查找、图的遍历等。这些算法与数据结构密切相关,掌握这些算法能够提升学生的算法设计和分析能力。 最后,该讲义的PDF格式方便学生进行随时随地的学习。学生可以通过电脑、平板或手机等设备随时打开讲义进行学习,非常方便。 综上所述,期末复习数据结构讲义PDF具有很高的教学价值。它能够帮助学生系统地学习和巩固数据结构的知识,提高算法设计和分析能力。同时,讲义的PDF格式也很方便学生进行学习

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值