数据结构(树和二叉树)

1.树和二叉树的定义

(1) 树的定义

树是n (n≥0) 个结点的有限集。 n=0 时称为空树。在任意一棵非空树中:

  • 有且仅有一个特定的称为根(Root) 的结点;
  • 当n>1 时,除了根结点以外其余结点可分为m(m>0) 个互不相交的有限集T1、 T2、 ……、 Tm, 其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。

(2) 树的基本术语

结点:树中的一个独立单元。
结点的度:结点拥有的子树数称为结点的度。
树的度:树的度是树内各结点度的最大值。
叶子:度为0的结点称为叶子或者终端结点。
非终端结点:度不为0的结点称为非终端结点或者分支结点。除根结点以外,非终端结点也称为内部结点。
双亲和孩子:结点的子数的根称为该结点的孩子,相应地,该结点称为孩子地双亲。
兄弟:同一个双亲地孩子之间互称为兄弟。
祖先:从根到该结点所经分支上所有结点。
子孙:以某结点为根地子数中任一结点都称为该结点地子孙。
层次:结点地层次从根开始定义起,根称为第一层,根地孩子为第二层。树中任一结点层次等于其双亲结点地层次加1。
堂兄弟:双亲在同一层次地结点互为堂兄弟。
树地深度:树中结点地最大层次称为树地深度或高度。
有序树和无序树:如果将树中结点地各个子树看成从左至右是有次序地,不能互换,则称为有序树,否则称为无序树。在有序树中最左边地子树的根称为第一个孩子,最右边的称为最后一个孩子。
森林:森林(Forest) 是m(m≥0) 棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。
二叉树:是n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。


(3) 二叉树的定义

二叉树是n个结点所构成的集合,它或为空树(n=0),或为非空树,对于非空树T:

  • 有且只有一个称为根的结点。
  • 除根结点以外的其余结点分为两个互不相交的子集T1和T2,分别称为T的左子树和右子树,且T1和T2本身又是二叉树。

二叉树和树的区别:
* 二叉树每个结点至多只有两颗子树。
* 二叉树的子树有左右之分,其次序不能任意颠倒。

2. 二叉树的性质和存储结构

(1) 二叉树的性质

性质1: 在二叉树上的第i层至多有2^(i-1)个结点。(i>=1)
性质2: 深度为k 的二叉树至多有2^k -1个结点(k>=1)。
性质3:对于任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。
证明:
二叉树结点总数:n = n0+n1+n2;
除了根结点外,其余结点都有一个分支进入,设B为分支总数,则n = B + 1;由于这些分支是由度为1或2的结点射出,所以B=n1+2*n2
于是:n = n1+2*n2+1 => n0 = n2+1

满二叉树:深度为k且含有2^k - 1个结点的二叉树。
满二叉树的特点:每一层上的结点数都是最大结点数,即每一层i的结点数都具有最大值2^(i-1)。
对满二叉树结点进行编号,约定编号从根结点起,自上而下,自左至右。

完全二叉树:深度为k对,由n个结点的二叉树,当且仅当每一个结点都与深度为k的满二叉树中编号1至n的结点一一对应,称之为完全二叉树。
完全二叉树的特点:
* 叶子结点只能在层次最大的两层上出现。
* 对任一结点,若其右分支下的子孙的最大层次为l,则其左分支下的子孙的最大层次必为l或l+1。


(2) 二叉树的存储结构

1.顺序存储结构:使用一组地址连续的存储单元来存储数据元素,将二叉树的结点依照自上而下,自左至右存储结点元素。

#define MAXSIZE 100;
typedef TElemType SqBiTree[MAXSIZE];//0号单元存储根结点
SqBiTree bt;

2.链式存储结构:结点包含3个域:数据域,左右指针。

typedef struct BiTNode {
    TElemType data;
    Struct BiTree *lchild,*rchild;
}BiTNode,*BiTree;

3. 遍历二叉树和线索二叉树

(1) 遍历二叉树

遍历二叉树是指按某条搜索路径巡访树中每个结点,使的每个结点均被访问一次,而且仅被访问一次。遍历的实质是对二叉树进行线性化的过程。

  1. 前序遍历:规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树, 再前序遍历右子树。如下最左图,遍历的顺序为:ABDGHCEIF。
void PreOrderTraverse(BiTree T){    
    if(T)
    {   
        printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */  
        PreOrderTraverse(T->lchild); /* 再先序遍历左子树 */    
        PreOrderTraverse(T->rchild); /* 最后先序遍历右子树 */
    }
}
  1. 中序遍历:规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。 如下中左图,遍历的顺序为: GDHBAEICF。
void InOrderTraverse(BiTree T){     
    if(T)
    {   
        InOrderTraverse(T->lchild); /* 中序遍历左子树 */
        printf("%c",T->data);/* 访问根结点 */    
               
        InOrderTraverse(T->rchild); /* 中序遍历右子树 */
    }
}
  1. 后序遍历:规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。 如下中右图,遍历的顺序为: GHDBIEFCA。
void PostOrderTraverse(BiTree T){   
    if(T)
    {   
        InOrderTraverse(T->lchild); /* 后序遍历左子树 */          
        InOrderTraverse(T->rchild); /* 后序遍历右子树 */
        printf("%c",T->data);/* 访问根结点 */    
    }
}
  1. 层序遍历:规则是若树为空, 则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中, 按从左到右的颇用才结点逐个访问。如下最右图所示,遍历的顺序为:ABCDEFGHI。

1. 根据遍历顺序确定二叉树

己知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树;已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树;但已知前序和后序遍历,是不能确定一棵二叉树的。



2. 先序遍历的顺序建立二叉树

(1) 扫描字符序列,读入字符ch。
(2) 如果ch是一个“#”字符,则表明该二叉树为空树,即T为NULL,否则执行以下操作:

  • 申请一个结点空间T。
  • 将ch赋给T->data。
  • 递归创建T的左子树。
  • 递归创建T的右子树。
void CreateBiTree(BiTree &T)
{//先按次序输入二叉树中结点的值(一个字符),创建二叉链表示的二叉树T
    cin>>ch;
    if(ch=='#')  T=NULL;    //  递归结束,建空树
    else    //递归创建二叉树
    {       
        T = new BiTNode; /* 生成根结点 */    
        T->data=ch; /* 根结点数据域为ch */     
        CreateBiTree(T->lchild); /* 递归创建左子树 */
        CreateBiTree(T->rchild); /* 递归创建右子树 */  
      } 
}
 

3. 复制二叉树

如果是空树,递归结束,否则执行以下操作:

  • 申请一个新的结点空间,复制根结点。
  • 递归复制左子树。
  • 递归复制右子树。
 void Copy(BiTree T,BiTree &NewT)
{//复制一棵和T完全相同的二叉树
    if(T == NULL)
    {
          NewT = NULL;
          return;
    }
    else
    {
          NewT = new BiTNode; 
          New->data = T->data; //复制根结点
          Copy(T->lchild,NewT->lchild);//递归复制左子树
          copy(T->rchild,NewT->rchild);//递归复制右子树
    }
}

4. 计算二叉树的深度

如果是空树,递归结束,深度为0,否则执行以下操作:

  • 递归计算左子树的深度计为m。
  • 递归计算右子树的深度为n。
  • 如果m大于n,二叉树的深度为m+1,否则为n+1;

int Depth(BiTree T)
{
    if(T == NULL) return 0;
    else
    {
          m = Depth(T->lchild);
          n  = Depyh(T->rchild);
          if(m>n) return(m+1);
          else  return (n+1);
     }
}
5. 统计二叉树中结点的个数
int NodeCount(BiTree T)
{
      if(T == NULL) return 0;
      else return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}

(2) 线索二叉树

1. 线索二叉树的基本概念

对于一个有n 个结点的二叉链表,每个结点有指向左右孩子的两个指针域,所以一共是2n个指针域。而n个结点的二叉树一共有n-1 条分支线数,也就是说,其实是存在2n-(n-1)=n+1个空指针域。可以考虑利用那些空地址,存放指向结点在某种遍历次序下的前驱和后继结点的地址。这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树。线索二叉树等于是把一棵二叉树转变成一个双向链表。这样对我们插入删除结点、查找某个结点带来了方便。所以我们对二叉树以某种次序遍历使其变为线索二叉树的过程称作是线索化。

若结点有左子树,则其lchild域指示其左孩子,否则令lchild指示其前驱,若结点有右子树,则其rchild域指示其右孩子,否则令rchild域指示其后继。为了避免混淆,需要改变结点结构,增加两个标志域,LTag、RTag。
LTag:
0->lchild域指示结点的左孩子
1-> lchild域指示结点的前驱

RTag:
0->Rchild域指示结点的右孩子
1-> Rchild域指示结点的后继

//二叉树的二叉线索存储结构
typedef struct BithrNode
{
    TElemType data;
    struct BiThrNode *lchid,*rchild;//左右孩子指针
    int LTag,RTag//左右标志
}BiThrNode,*BiThrTree;

2. 构造线索二叉树

由于线索二叉树构造的实质是将二叉链表中的空指针指向前驱或后继的线索,而前驱或后继的信息只有在遍历时才能得到,因此线索化的过程即为在遍历过程中修改空指针的过程。

为了纪下遍历过程中访问结点的先后关系,附设一个指针pre始终指向刚刚访问过的结点,而指针p指向当前访问的结点,由此纪录遍历过程中访问结点的先后关系。

以p结点为根的子树中序线索化

  1. 如果p非空,左子树递归线索化。
  2. 如果p的左孩子为空,则p加上左线索,将其LTag置为1,让p的左孩子指针指向pre(前驱),否则将p的LTag置为0。
  3. 如果pre的右孩子为空,则pre加上右线索,将其RTag置为1,让pre的右孩子指向p(后继),否则将pre的RTag置为0。
  4. 将pre指向刚访问过的结点p,即pre = p 。
  5. 右子树递归线索化。
void InThreading(BiThrTree p)
{//pre是全局变量,初始化时其右孩子指针为空,便于在树的左点开始建线索。
    InThreading(p->lchild); // 左子树递归线索化
    if(!p->lchild)//p的左孩子为空
    {
        p->LTag = 1;//给p加上左线索
        p->lchild = pre;//p的左孩子指针指向pre(前驱)
    }
    else p->LTag = 0;
    if(!pre->rchild) //pre的右孩子为空
    {
          pre->RTag =1;//给pre加上右线索
          pre->rchild = p;//pre的右孩子指针指向p(后继)
    }
    else pre->RTag = 0;
    pre = p;//保持pre指向p的前驱
    InThreading(p->rchild);//右子树递归线索化
}

带头结点的二叉树中序线索化

void InOrderThreading(BiThrTree &Thrt, BiThrTree T)
{    //中序遍历二叉树T,并将其中序线索化,Thrt指向头结点
    Thrt =  new BiThrNode; //建头结点
    Thrt->LTag = 0; //头结点有左孩子,若树非空,则其左孩子为树根
    Thrt->RTag = 1; //头结点的右孩子指针为右线索
    Thrt->rchild = Thrt;    //初始化时右指针指向自己
    if (!T)     Thrt->lchild = Thrt;    //若树非空,左指针也指向自己
    else    
    {       
          Thrt->lchild = T;     pre = Thrt;//头结点的左孩子指向根,pre初始值指向头结点
          InThreading(T);//以T为根的二叉树进行中序线索化      
          pre->rchild = Thrt;   //结束后,pre为最右结点,pre的右线索指向头结点 
          pre->RTag = 1;        
          Thrt->rchild = pre;   //头结点的右线索指向pre
      } 
 }

 

3. 遍历线索二叉树




遍历中序线索二叉树

  1. 指针p指向根结点
  2. p为非空树或遍历未结束,循环执行以下操作:
  • 沿左孩子向下,到达最左下结点*p,它是中序的第一个结点。
  • 访问*p。
  • 沿右线索反复查找当前结点*p的后继结点并访问后继结点,直至右线索为0或者遍历结束。
  • 转向p的右子树。
void InOrderTraverse_Thr(BiThrTree T)
{//T指向头结点,头结点的左链lchild指向根结点。
//中序遍历二叉线索树T的非递归算法,对每个数据元素直接输出。
    BiThrTree p;    
    p = T->lchild;   //p指向根结点
    while (p != T)  //空树或遍历结束,p == T
    {       
         while (p->LTag == 0)  p = p->lchild;   //沿左孩子向下    
          cout<<p->data; //访问其左子树为空的结点      
          while (p->RTag == 1 && p->rchild != T)        
          {         
              p = p->rchild;cout<<p->data;//沿右线索访问后继结点  
           }        
            p = p->rchild;  //转向右子树
        }   
}

4. 树和森林

(1) 树的存储结构

1. 双亲表示法

假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。也就是说,每个结点除了知道自己是谁以外,还知道它的双亲在哪里。结点结构为表所示:



其中data是数据域,存储结点的数据信息。而parent是指针域,存储该结点的双亲在数组中的下标。

#define MAX_TREE_SIZE 100 
typedef int TElemType;  /*树结点的数据类型,目前暂定为整型*/
typedef struct PTNode   /*结点结构*/
{   
    TElemType data;     /*结点数据*/    
    int parent;         /*双亲位置*/
}PTNode;
typedef struct{ 
    PTNode nodes[MAX_TREE_SIZE];    /*结点数组*/    
    int r,n;                        /*根的位置和结点数*/
}PTree

根结点因为没有双亲,我们约定根结点的位置域设置为-1,则每个结点都存有其双亲的位置。


这样的结构容易根据结点的parent指针找到它的双亲结点,时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根。但不易找到结点的孩子结点,除非遍历整个结构,如上图,要找到E的孩子J,必须遍历结点,找到parent为E的下标4,才能找到E的孩子J。

2. 孩子表示法

由于树中每个结点可能有多棵子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,这种方法叫多重链表表示法。不过,树的每个结点的度,也就是它的孩子个数是不同的。 所以可以设计两种方案来解决。

方案一:
一种是指针域的个数就等于树的度,复习一下,树的度是树各个结点度的最大值。其结构如图:



其中data是数据域。child1到childd是指针域,用来指向该结点的孩子结点。上述度为3的树若用此法表示,指针域的个数为3,如图。



这种方法对于树中各结点的度相差很大时,容易浪费空间,因为有很多的结点,它的指针域都是空的。不过如果树的各结点度相差很小时,意味着开辟的空间被充分利用了,这时存储结构的缺点反而变成了优点。

方案二:
第二种方案每个结点指针域的个数等于该结点的度,我们专门取一个位置来存储结点指针域的个数,其结构如下图:



其中data为数据域,degree 为度域,也就是存储该结点的孩子结点的个数,child1到childd为指针域,指向该结点的各个孩子的结点。方案一的表示可更改如下。



这种方法克服了浪费空间的缺点,对空间利用率是很高了,但是由于各个结点的链表是不相同的结构,加上要维护结点的度的数值,在运算上就会带来时间上的损耗。

能否有更好的方法,既可以减少空指针的浪费又能使结点结构相同。可以用孩子表示法。具体办法是,把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中,如图:


孩子链表

为此,设计两种结点结构,一个是孩子链表的孩子结点,其中child是数据域,用来存储某个结点在表头数组中的下标。next是指针域,用来存储指向某结点的下一个孩子结点的指针。另一个是表头数组的表头结点,其中也data是数据域,存储某结点的数据信息。firstchild是头指针域, 存储该结点的孩子链表的头指针。

/*树的孩子表示法结构定义*/
#define MAX_TREE_SIZE 100
typedef struct CTNode/*孩子结点*/
{   
    int child;  
    struct CTNode *next;
}*ChildPtr; 
typedef struct/*表头结构*/
{   
    TElemType data; 
    ChildPtr firstchild;
}CTBox;
 typedef struct/*树结构*/
{   
    CTBox nodes[MAX_TREE_SIZE];     /*结点数组*/    
    int r, n;           /*根的位置和结点数*/
}CTree;

此结构便于查找某结点的孩子、兄弟,只需查找这个结点的孩子单链表即可。遍历整棵树也比较方便,对头结点的数组循环即可。问题是,不易知道某个结点的双亲,为此可以改进成双亲孩子表示法。结构如下图:


带双亲的孩子链表

3. 孩子兄弟法

对树观察发现,任意一棵树, 它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。 因此,我们设置两个指针, 分别指向该结点的第一个孩子和此结点的右兄弟。结构如下图



其中data是数据域,firstchild为指针域,存储该结点的第一个孩子结点的存储地址, rightsib是指针域,存储该结点的右兄弟结点的存储地址。结构定义代码如下。

/*树的孩子兄弟表示法结构定义*/
typedef struct CSNode
{
    TElemType data;
    struct CSNode *firstchild, *rightsib;
}CSNode,*CSTree;

此法方便找某个结点的孩子,只需要通过fistchild 找到此结点的长子,然后再通过长子结点的rightsib 找到它的二弟,接着一直下去,直到找到具体的孩子。如果想找到某个结点的双亲,这个表示法就有缺陷了,可以考虑加个parent指针域来解决。其实此法最大的好处就是,把一棵复杂的树变成了二叉树。上图可以变形如下:


(2) 森林与二叉树的转换

1. 树转换成二叉树

转化方法:
1.把所有兄弟结点连接起来
2.删掉除了结点第一个左孩子外的连线


2. 森林转换成二叉树

将森林转换成二叉树的规则与树类似。先将森林中的每一棵树转换成二叉树,再将第一棵树的根作为转换后的二叉树的根,第一棵树的左子树作为转换后二叉树根的左子树,第二棵树作为转换后二叉树根的右子树,第三棵树作为转换后二叉树根的右子树的右子树,以此类推。


3. 二叉树转换成森林

若二叉树非空,则二叉树根及其左子树为第一棵树的二叉树形式,二叉树根的右子树可以看作是一个由除第一棵树外的森林转换后的二叉树,应用同样的方法,直到最后产生一颗没有右子树的二叉树为止,这样就得到了原森林。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345