zoukankan      html  css  js  c++  java
  • 【数据结构专题-03】树

    摘要:最近在学习数据结构,使用的教材是《大话数据结构》,在这里分享自己所做的笔记,作为巩固所学和交流分享之用!


    1.树的定义

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

      • 有且仅有一个称为根的结点
      • 当n>1时,其余结点可分为m个互不相交的有限集T1,T2,...Tm ,其中每一个集合本身又是一棵树,称为根的子树
    • 结点分类:结点拥有的子树数称为结点的度。度为0的结点称为叶结点或终端结点。树的度是树内各结点的度的最大值

    • 结点间关系:结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲。同一个双亲的孩子之间互称兄弟。结点的祖先是从根到该结点所经分支上的所有结点

    • 结点的层次:从根开始定义,根为第一层,根的孩子为第二层,以此类推。树中结点的最大层次称为树的深度。

    • 有序树与无序树:如果将树中结点的各子树看成是从左至右是有序的,不能互换的,则称该树为有序树,否则为无序树

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

    2.树的存储结构

    • 双亲表示法 :使用一组连续空间存储树的结点,同时在每个结点中附设一个指示器指示其双亲结点在链表中的位置,也就是说,每个结点除了知道自己在哪以外,他还知道其双亲在哪儿。

      代码实现如下:

      ////树的双亲表示法
      //1.结点结构定义
      const int MAX_TREE_SIZE=100;
      typedef char TElemType;
      typedef struct PTNode
      {
      TElemType data;
      int parent;//双亲位置
      } PTNode;
      
      //2.树结构定义
      typedef struct
      {
      PTNode nodes[MAX_TREE_SIZE];
      int r,n;//根的位置和结点数
      } PTree;

      我们来测试如下一棵树:

      将这棵树用上面的数据结构存储并打印,代码如下:

      
      #include <iostream>
      
      using namespace std;
      
      //树的双亲表示法
      //1.结点结构定义
      const int MAX_TREE_SIZE=100;
      typedef char TElemType;
      typedef struct PTNode
      {
      TElemType data;
      int parent;//双亲位置
      } PTNode;
      
      //2.树结构定义
      typedef struct
      {
      PTNode nodes[MAX_TREE_SIZE];
      int r,n;//根的位置和结点数
      } PTree;
      
      int main()
      {
      PTree Tree1={{{'A',-1},{'B',1},{'C',1},{'D',2},{'E',2},{'F',2},{'G',3},{'H',3},{'I',3},{'J',3}},1,10};//初始化一棵树
      cout<<"Executing...."<<endl;
      cout<<"root location:"<<Tree1.r<<endl<<"node numbers:"<<Tree1.n<<endl;
      for(int i=0;i<Tree1.n;i++)
        cout<<"data: "<<Tree1.nodes[i].data<<",parent: "<<Tree1.nodes[i].parent<<endl;
      cout<<"Done...."<<endl;
      return 0;
      }

      测试结果参考:

      双亲表示法的优点:使用线性关系存储,实现方便,易于找到孩子结点的双亲位置

      双亲表示法的缺点:寻找孩子结点困难

    • 孩子表示法

      由于树中每个结点可能有多棵子树,可以考虑使用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们称这种方法叫做多重链表表示法。由此我们指出,孩子表示法,即是把每个结点的孩子结点排列起来以单链表作储存结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中。

      我们仍旧以前面的树结构为例来示意,如下:

      写出该类型的结构定义代码如下:

      //孩子链表结构定义
      const int MAX_TREE_SIZE=100;
      typedef struct CTNode
      {
      int child;//结点的度
      CTNode *next;
      }*ChildPtr;
      
      //表头结构
      typedef struct
      {
      char data;
      ChildPtr firstchild;
      }CTBox;
      
      //树结构
      typedef struct
      {
      CTBox nodes[MAX_TREE_SIZE];
      int r,n;
      }CTree;
      

      下面,我们使用上面的结构来储存前面例子中的树结构并打印:

      //树的孩子表示法
      
      #include<iostream>
      
      using namespace std;
      
      //孩子链表结构定义
      const int MAX_TREE_SIZE = 100;
      typedef struct CTNode
      {
      int child;//结点的度
      CTNode *next;
      }*ChildPtr;
      
      //表头结构
      typedef struct
      {
      char data;
      ChildPtr firstchild;
      }CTBox;
      
      //树结构
      typedef struct
      {
      CTBox nodes[MAX_TREE_SIZE];
      int r, n;
      }CTree;
      
      int main()
      
      {
      
          //g1
      
          ChildPtr g1 = (ChildPtr)malloc(sizeof(CTNode));
      
          g1->child = 2;
      
          g1->next = NULL;
      
          //g2
      
          ChildPtr g2 = (ChildPtr)malloc(sizeof(CTNode));
      
          g2->child = 1;
      
          ChildPtr g2_2 = (ChildPtr)malloc(sizeof(CTNode));
      
          g2_2->child = 2;
      
          g2_2->next = NULL;
      
          g2->next = g2_2;
      
          //g3
      
          ChildPtr g3 = (ChildPtr)malloc(sizeof(CTNode));
      
          ChildPtr g3_2 = (ChildPtr)malloc(sizeof(CTNode));
      
          ChildPtr g3_3 = (ChildPtr)malloc(sizeof(CTNode));
      
          g3->child = 3; g32->child = 1; g33->child = -1;
      
          g3->next = g32; g32->next = g33; g33->next = NULL;
      
          //g4
      
          ChildPtr g4 = (ChildPtr)malloc(sizeof(CTNode));
      
          ChildPtr g4_2 = (ChildPtr)malloc(sizeof(CTNode));
      
          ChildPtr g4_3 = (ChildPtr)malloc(sizeof(CTNode));
      
          ChildPtr g4_4 = (ChildPtr)malloc(sizeof(CTNode));
      
          g4->child = -1; g42->child = -1; g43->child = -1; g4_4->child = -1;
      
          g4->next = g42; g42->next = g43; g43->next = g44; g44->next = NULL;
      
          //CBox
      
          CTBox c1 = { 'A' ,g2 };
      
          CTBox c2 = { 'B' ,g3 };
      
          CTBox c3 = { 'C' ,g3_2 };
      
          CTBox c4 = { 'D' ,g4 };
      
          CTBox c5 = { 'E' ,g4_4 };
      
          CTBox c6 = { 'F' ,NULL };
      
          CTBox c7 = { 'G' ,NULL };
      
          CTBox c8 = { 'H' ,NULL };
      
          CTBox c9 = { 'I' ,NULL };
      
          CTBox c10 = { 'J' ,NULL };
      
          CTree Tree1;
      
          Tree1.r = 1;
      
          Tree1.n = 10;
      
          Tree1.nodes[0] = c1;
      
          Tree1.nodes[1] = c2;
      
          Tree1.nodes[2] = c3;
      
          Tree1.nodes[3] = c4;
      
          Tree1.nodes[4] = c5;
      
          Tree1.nodes[5] = c6;
      
          Tree1.nodes[6] = c7;
      
          Tree1.nodes[7] = c8;
      
          Tree1.nodes[8] = c9;
      
          Tree1.nodes[9] = c10;
      
          cout << "Executing..." << endl;
      
          cout << "Root: " << Tree1.r << endl;
      
          cout << "Node: " << Tree1.n << endl;
      
          for (int i = 0; i < Tree1.n - 1; i++)
      
              cout << "Tree Node: " << i << "->" << Tree1.nodes[i].data << endl;
      
          cout << "Done..." << endl;
      
          system("pause");
      
          return 0;
      
      }
      

    测试结果参考:

    从该实例中,聪明的读者应该已经发现,孩子表示法的优点在于其可以方便的访问其孩子结点,而访问双亲结点则需要遍历才行。

    此时,一个自然的想法是可不可以把孩子表示法和双亲表示法结合起来从而保证算法既可以方便的访问双亲结点又可以访问孩子结点,答案是肯定的。程序如下,仍旧以上面的树结构为例

    实际上,我们仅需在孩子表示法程序里的CTBox结构中加入指示双亲位置的域即可:

    c++
    //2.表头结构
    typedef struct
    {
    char data;
    ChildPtr firstchild;//保存第一个孩子结点位置
    int parent;//保存双亲结点位置
    }CTBox;

    完整测试代码如下:

    “`c++
    //孩子双亲表示法
    #include
    using namespace std;

    const int MAX_TREE_SIZE=100;

    //1.孩子链表结构定义
    typedef struct CTNode
    {
    int child;//结点的度
    CTNode *next;
    }*ChildPtr;

    //2.表头结构
    typedef struct
    {
    char data;
    ChildPtr firstchild;//保存第一个孩子结点位置
    int parent;//保存双亲结点位置
    }CTBox;

    //3.树结构
    typedef struct
    {
    CTBox nodes[MAX_TREE_SIZE];
    int r,n;
    }CTree;

    int main()
    {

    //g1
    ChildPtr g1=(ChildPtr)malloc(sizeof(CTNode));
    g1->child=2;
    g1->next=NULL;
    //g2
    ChildPtr g2=(ChildPtr)malloc(sizeof(CTNode));
    g2->child=1;
    ChildPtr g2_2= (ChildPtr)malloc(sizeof(CTNode));
    g2_2->child=2;
    g2_2->next= NULL;
    g2->next=g2_2;
    //g3
    ChildPtr g3=(ChildPtr)malloc(sizeof(CTNode));
    ChildPtr g3_2=(ChildPtr)malloc(sizeof(CTNode));
    ChildPtr g3_3=(ChildPtr)malloc(sizeof(CTNode));
    g3->child=3;g3_2->child=1;g3_3->child=-1;
    g3->next=g3_2;g3_2->next=g3_3;g3_3->next= NULL;
    //g4
    ChildPtr g4=(ChildPtr)malloc(sizeof(CTNode));
    ChildPtr g4_2=(ChildPtr)malloc(sizeof(CTNode));
    ChildPtr g4_3=(ChildPtr)malloc(sizeof(CTNode));
    ChildPtr g4_4=(ChildPtr)malloc(sizeof(CTNode));
    g4->child=-1;g4_2->child=-1;g4_3->child=-1;g4_4->child=-1;
    g4->next=g4_2;g4_2->next=g4_3;g4_3->next=g4_4;g4_4->next= NULL;
    
    //CBox
    CTBox c1 = { 'A' ,g2,-1};
    CTBox c2 = { 'B' ,g3,0};
    CTBox c3 = { 'C' ,g3_2,0};
    CTBox c4 = { 'D' ,g4,1};
    CTBox c5 = { 'E' ,g4_4,2};
    CTBox c6 = { 'F' ,NULL,2};
    CTBox c7 = { 'G' ,NULL,3};
    CTBox c8 = { 'H' ,NULL,3};
    CTBox c9 = { 'I' ,NULL,3};
    CTBox c10 = { 'J' ,NULL,4};
    
    CTree Tree1;
    Tree1.r=0;
    Tree1.n=10;
    Tree1.nodes[0]=c1;
    Tree1.nodes[1]=c2;
    Tree1.nodes[2]=c3;
    Tree1.nodes[3]=c4;
    Tree1.nodes[4]=c5;
    Tree1.nodes[5]=c6;
    Tree1.nodes[6]=c7;
    Tree1.nodes[7]=c8;
    Tree1.nodes[8]=c9;
    Tree1.nodes[9]=c10;
    
    cout<<"Executing..."<<endl;
    cout<<"Root: "<<Tree1.r<<endl;
    cout<<"Node: "<<Tree1.n<<endl;
    for (int i = 0; i < Tree1.n; i++)
        cout<<"Tree Node: "<<i<<"->"<<Tree1.nodes[i].data<<"	Parent Location: "<<Tree1.nodes[i].parent<<endl;
    cout<<"Done..."<<endl;
    system("pause");
    return 0;
    

    }
    “`

    运行结果参考:

    image

    • 孩子兄弟表示法

      树的另一种存储方法称之为孩子兄弟表示法,这种表示法的思想在于把树存储为二叉树结构,二叉树又是什么概念呢?下面将会重点讨论。

    3.二叉树

    • 定义:二叉树,是n(n>=0)个结点的有限集合,该集合或为空集,或者由一个根结点和两棵互不相交的分别称为根结点的左子树和右子树的树组成

    • 二叉树特点

      • 每个结点最多有两棵子树,故二叉树中不存在度大于2的结点
      • 左子树和右子树是有顺序的,不可随意颠倒
    • 二叉树的五种基本形态

      • 空二叉树
      • 只有一个根结点
      • 根结点只有左子树
      • 根结点只有右子树
      • 根结点既有左子树又有右子树
    • 特殊二叉树

      • 斜数:所有的结点都只有左子树的二叉树称为左斜树,所有的结点只有右子树的称为右斜树,二者统称为斜树
      • 满二叉树:在一棵二叉树中,如果所有结点都存在左子树和右子树,而且所有叶子都在同一层上,这样的二叉树称为满二叉树
      • 完全二叉树:对一棵具有n个结点二叉树按层序编号,如果编号为i的结点与满二叉树中编号为i的结点的位置相同,则这样的二叉树称为完全二叉树
    • 二叉树的性质

      • 性质一:在二叉树的第i层上至多有2i1 个结点

      • 性质二:深度为k的二叉树至多有2k1 个结点

      • 性质三:对任何一棵二叉树T,如果其终端结点数为n0 ,度为2的结点数为n2 ,则n0=n2+1

      • 性质四:具有n个结点的完全二叉树的深度为log2n+1 ,(x 表示不大于x的最大整数)

      • 性质五:如果对一棵有n个结点的完全二叉树的结点按层编号,对任一结点有:

      • 如果i=1,则结点是二叉树的根,无双亲;如果i>1,则其双亲是结点i/2

      • 如果2i>n,则结点i无左孩子,否则其左孩子是结点2i

      • 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1

      • 示例:

        image

        如上图

        • i=1结点显然为根结点;对于5结点,双亲结点为2=5/2
        • 对于6,7,8,9和10号结点,2i>10,而由图易见其都为叶子结点
        • ….
    • 二叉树的储存结构

      • 顺序储存结构:即使用一维数组储存二叉树中的结点

      我们通过一棵完全二叉树来示例:

      可见,一棵完全二叉树在存储上是有着极大的优势的,仅通过数组便可存储,对于非完全的二叉树,我们可以先把其补全为完全二叉树,在补充的地方使用一个约定的符号代替,这样也就实现了非完全二叉树的数组存储,不过,在极端情况下,这样的储存会造成极大地内存浪费。

      • 二叉链表:二叉树每个结点最多有两个孩子,故可以为它设计一个数据域和两个指针,称这样的储存方式即为二叉链表,代码定义如下:
      //二叉链表
      typedef struct BiTNode
      {
          char data;
          BiTNode *lChild;
          BiTNode *rChild;
      }*BiTree;

      如果有必要,也可以为其增加一个指向其双亲的指针,此时则称之为三叉链表:

      //三叉链表
      typedef struct BiTNode
      {
          char data;
          BiTNode *lChild;
          BiTNode *rChild;
          BiTNode *parent;
      }*BiTree;
    • 遍历二叉树

      • 前序遍历:若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树

      image

      上图的遍历顺序为:ABDGHCEIF

      • 中序遍历:若二叉树为空,则空操作返回,否则从根结点开始,中序遍历根结点的左子树,然后访问根结点,最后中序遍历右子树

      image

      上图的遍历顺序为:GDHBAEICF

      • 后序遍历:若二叉树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根结点

      image

      上图的访问顺序为:GHDBIEFCA

      • 层序遍历:若二叉树为空,则空操作返回,否则从树的第一层开始访问从上而下逐层遍历

      image

      上图的遍历顺序为:ABCDEFGHIJ

    • 前序遍历算法:使用递归算法

      void PreOrderTraverse(BiTree T)
      {
      if(T==NULL)
          return;
      cout<<T->data;//输出数据
      PreOrderTraverse(T->lChild);//先序遍历左子树
      PreOrderTraverse(T->rChild);//先序遍历右子树
      }

      测试示例:

      代码:

      ////前序遍历算法
      
      #include<iostream>
      
      using namespace std;
      
      //二叉链表
      typedef struct BiTNode
      {
      char data;
      BiTNode *lChild;
      BiTNode *rChild;
      }*BiTree;
      
      void PreOrderTraverse(BiTree T)
      {
      if(T==NULL)
          return;
      cout<<T->data;//输出数据
      PreOrderTraverse(T->lChild);//先序遍历左子树
      PreOrderTraverse(T->rChild);//先序遍历右子树
      }
      
      int main()
      {
      //为结点申请空间
      BiTree n1=(BiTree)malloc(sizeof(BiTNode));
      BiTree n2=(BiTree)malloc(sizeof(BiTNode));
      BiTree n3=(BiTree)malloc(sizeof(BiTNode));
      BiTree n4=(BiTree)malloc(sizeof(BiTNode));
      BiTree n5=(BiTree)malloc(sizeof(BiTNode));
      BiTree n6=(BiTree)malloc(sizeof(BiTNode));
      BiTree n7=(BiTree)malloc(sizeof(BiTNode));
      BiTree n8=(BiTree)malloc(sizeof(BiTNode));
      BiTree n9=(BiTree)malloc(sizeof(BiTNode));
      BiTree n10=(BiTree)malloc(sizeof(BiTNode));
      BiTree n11=(BiTree)malloc(sizeof(BiTNode));
      
      //赋值及链接
      n1->data='A';n2->data='B';n3->data='C';n4->data='D';
      n5->data='E';n6->data='F';n7->data='G';n8->data='H';
      n9->data='I';n10->data='J';n11->data='K';
      
      n1->lChild=n2;n1->rChild=n3;
      n2->lChild=n4;n2->rChild=n5;
      n3->lChild=n6;n3->rChild=n7;
      n4->lChild=n8;n4->rChild=NULL;
      n5->lChild=NULL;n1->rChild=NULL;
      n6->lChild=n9;n6->rChild=NULL;
      n7->lChild=NULL;n7->rChild=n10;
      n8->lChild=NULL;n8->rChild=n11;
      n9->lChild=NULL;n9->rChild=NULL;
      n10->lChild=NULL;n10->rChild=NULL;
      n11->lChild=NULL;n11->rChild=NULL;
      
      //前序遍历
      cout<<"Executing..."<<endl;
      PreOrderTraverse(n1);
      cout<<"Done..."<<endl;
      
      }
    • 中序遍历算法:中序遍历仅需在前序遍历的算法中稍加修改即可,如下:

      void MidOrderTraverse(BiTree T)
      {
      if(T==NULL)
          return;
      MidOrderTraverse(T->lChild);//先序遍历左子树
      cout<<T->data<<"--";//输出数据
      MidOrderTraverse(T->rChild);//先序遍历右子树
      }

      如上可看出,中序遍历仅是交换了一下前序遍历的一行代码,真是奇妙呢!

      仍旧,我们给出测试示例,二叉树仍参考上图:

      //中序遍历算法
      //
      
      #include<iostream>
      
      using namespace std;
      
      //二叉链表
      typedef struct BiTNode
      {
      char data;
      BiTNode *lChild;
      BiTNode *rChild;
      }*BiTree;
      
      void MidOrderTraverse(BiTree T)
      {
      if(T==NULL)
          return;
      MidOrderTraverse(T->lChild);//先序遍历左子树
      cout<<T->data<<"--";//输出数据
      MidOrderTraverse(T->rChild);//先序遍历右子树
      }
      
      int main()
      {
      //为结点申请空间
      BiTree n1=(BiTree)malloc(sizeof(BiTNode));
      BiTree n2=(BiTree)malloc(sizeof(BiTNode));
      BiTree n3=(BiTree)malloc(sizeof(BiTNode));
      BiTree n4=(BiTree)malloc(sizeof(BiTNode));
      BiTree n5=(BiTree)malloc(sizeof(BiTNode));
      BiTree n6=(BiTree)malloc(sizeof(BiTNode));
      BiTree n7=(BiTree)malloc(sizeof(BiTNode));
      BiTree n8=(BiTree)malloc(sizeof(BiTNode));
      BiTree n9=(BiTree)malloc(sizeof(BiTNode));
      BiTree n10=(BiTree)malloc(sizeof(BiTNode));
      BiTree n11=(BiTree)malloc(sizeof(BiTNode));
      
      //赋值及链接
      n1->data='A';n2->data='B';n3->data='C';n4->data='D';
      n5->data='E';n6->data='F';n7->data='G';n8->data='H';
      n9->data='I';n10->data='J';n11->data='K';
      
      n1->lChild=n2;n1->rChild=n3;
      n2->lChild=n4;n2->rChild=n5;
      n3->lChild=n6;n3->rChild=n7;
      n4->lChild=n8;n4->rChild=NULL;
      n5->lChild=NULL;n5->rChild=NULL;
      n6->lChild=n9;n6->rChild=NULL;
      n7->lChild=NULL;n7->rChild=n10;
      n8->lChild=NULL;n8->rChild=n11;
      n9->lChild=NULL;n9->rChild=NULL;
      n10->lChild=NULL;n10->rChild=NULL;
      n11->lChild=NULL;n11->rChild=NULL;
      
      //中序遍历
      cout<<"Executing..."<<endl;
      MidOrderTraverse(n1);
      cout<<"Done..."<<endl;
      system("pause");
      return 0;
      }
    • 后序遍历算法:聪明的伙伴此时应该已经猜出来了后序遍历的写法,看下面:

      void PostOrderTraverse(BiTree T)
      {
      if(T==NULL)
          return;
      PostOrderTraverse(T->lChild);//先序遍历左子树
        PostOrderTraverse(T->rChild);//先序遍历右子树
      cout<<T->data<<"--";//输出数据
      }

      完整代码示例:

      //后序遍历算法
      //
      
      #include<iostream>
      
      using namespace std;
      
      //二叉链表
      typedef struct BiTNode
      {
      char data;
      BiTNode *lChild;
      BiTNode *rChild;
      }*BiTree;
      
      void PostOrderTraverse(BiTree T)
      {
      if(T==NULL)
          return;
      PostOrderTraverse(T->lChild);//先序遍历左子树
      PostOrderTraverse(T->rChild);//先序遍历右子树
      cout<<T->data<<"--";//输出数据
      }
      
      int main()
      {
      //为结点申请空间
      BiTree n1=(BiTree)malloc(sizeof(BiTNode));
      BiTree n2=(BiTree)malloc(sizeof(BiTNode));
      BiTree n3=(BiTree)malloc(sizeof(BiTNode));
      BiTree n4=(BiTree)malloc(sizeof(BiTNode));
      BiTree n5=(BiTree)malloc(sizeof(BiTNode));
      BiTree n6=(BiTree)malloc(sizeof(BiTNode));
      BiTree n7=(BiTree)malloc(sizeof(BiTNode));
      BiTree n8=(BiTree)malloc(sizeof(BiTNode));
      BiTree n9=(BiTree)malloc(sizeof(BiTNode));
      BiTree n10=(BiTree)malloc(sizeof(BiTNode));
      BiTree n11=(BiTree)malloc(sizeof(BiTNode));
      
      //赋值及链接
      n1->data='A';n2->data='B';n3->data='C';n4->data='D';
      n5->data='E';n6->data='F';n7->data='G';n8->data='H';
      n9->data='I';n10->data='J';n11->data='K';
      
      n1->lChild=n2;n1->rChild=n3;
      n2->lChild=n4;n2->rChild=n5;
      n3->lChild=n6;n3->rChild=n7;
      n4->lChild=n8;n4->rChild=NULL;
      n5->lChild=NULL;n5->rChild=NULL;
      n6->lChild=n9;n6->rChild=NULL;
      n7->lChild=NULL;n7->rChild=n10;
      n8->lChild=NULL;n8->rChild=n11;
      n9->lChild=NULL;n9->rChild=NULL;
      n10->lChild=NULL;n10->rChild=NULL;
      n11->lChild=NULL;n11->rChild=NULL;
      
      //后序遍历
      cout<<"Executing..."<<endl;
      PostOrderTraverse(n1);
      cout<<"Done..."<<endl;
      system("pause");
      return 0;
      }
      • 几点注意事项:已知前序和中序遍历结果或者已知中序和后序遍历结果可唯一确定一个二叉树,但已知前序和后序遍历结果不能唯一确定一棵二叉树
    • 二叉树的建立

      从前面我们已经指出,对于一棵不完全的二叉树,我们无法通过一个遍历序列来确定,为了能保证使得一个遍历序列确定一棵二叉树,我们引入拓展二叉树的概念,即是使用约定的符号进行空位补充,如下图:

      image

      这样,上面的二叉树前序遍历表示为:AB#D##C##,使用代码实现是极为容易的,在此不再赘述

    • 线索二叉树

      在我们设计的二叉链表存储结构中,叶子结点的两个指针指向了空,可想而知,每个二叉链表都会有大量这样的指针占用了内存但没有储存有意义的内容,同时,在这些链表的叶子结点中,我们无法方便的获得遍历时每个结点的前驱和后继,为了解决上面的这些矛盾,我们自然想到,可不可以把这些空的指针利用起来,指向其前驱和后继呢?答案是肯定的。特别的,我们把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为二叉链表,相应的二叉树称为线索二叉树 。我们用下面两张图片来进行示意:

      image

      image

      上面的图片示意了叶子结点后继结点的链接,前驱结点亦然。经过这样的改进,实际上我们把二叉链表改进成了一个双向链表结构,如下图:

      image

      我们指出:对二叉树以某种次序遍历使其变为线索二叉树的过程称为线索化

      实际上,实际问题比这个要复杂一些,当对叶子结点添加前驱和后继之后,我们如何区分某结点是中间结点还是叶子结点?为了解决这个麻烦,我们需要给结点引入标签:ltag、rtag为真时表示结点属于叶子结点。

    • 线索二叉树结构的实现

      结构定义代码:

      //线索二叉树
      typedef enum{Link,Thread}PointTag;
      
      typedef struct BiThrNode
      {
      char data;
      BiThrNode *lChild,*rChild;
      PointTag LTag;
      PointTag RTag;
      }*BiThrTree;

      使用中序遍历线索化二叉树算法实现:

      //中序遍历线索化算法实现
      //线索二叉树
      typedef enum{Link,Thread}PointTag;
      
      typedef struct BiThrNode
      {
      char data;
      BiThrNode *lChild,*rChild;
      PointTag LTag;
      PointTag RTag;
      }*BiThrTree;
      
      BiThrTree pre;//指向刚刚访问过的结点
      
      void InThreading(BiThrTree p)
      {
      if(p)
      {
          InThreading(p->lChild);//递归左子树线索化
          if(!p->lChild)
          {
              p->LTag=Thread;
              p->lChild=pre;
          }
          if(!pre->rChild)
          {
              pre->RTag=Thread;
              pre->rChild=p;
          }
          pre=p;
          InThreading(p->rChild);
      }
      
      }

      现在我们可以像操作双向链表一样来操作二叉线索链表:

      //二叉线索链表的遍历
      
      //线索二叉树
      typedef enum{Link,Thread}PointTag;
      typedef struct BiThrNode
      {
      char data;
      BiThrNode *lChild,*rChild;
      PointTag LTag;
      PointTag RTag;
      }*BiThrTree;
      
      int InOrderTraverse_Thr(BiThrTree T)
      {
      BiThrTree p;
      p=T->lChild;//p指向根结点
      while(p!=T)//空树或遍历结束时p==T
      {
          while(p->LTag==Link)//当LTag==0时循环到中序序列第一个结点
              p=p->lChild;
              cout<<p->data<<"--";
              while(p->RTag==Thread&&p->rChild!=T)
              {
                  p=p->rChild;
                  cout<<p->data<<"--";
              }
              p=p->rChild;//p进入其右子树根
      }
      return 1;
      }

    4.树、森林与二叉树的转换

    • 树转换为二叉树

      • 加线:在所有兄弟结点之间加一条连线

      • 去线:对树中每个结点,只保留他与第一个孩子结点的连线,而删除他与其他孩子之间的连线

      • 层次调整:以树的根结点为轴心,将整棵树顺时针旋转一定角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子

      如下图所示:

      image

    • 森林转换为二叉树

      森林由若干棵树组成,故而在转换时可以把不同树理解为兄弟,则按以下步骤进行转换:

      • 把每棵树转换为二叉树

      • 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。如此操作便可得到结果。

      如下图:

      image

    • 二叉树转换为树

      • 加线。若某结点的左孩子存在,则将这个左孩子的右孩子结点,右孩子的所有递归右孩子结点都作为此结点的孩子,将该结点与这些右孩子结点用线连接起来。

      • 去线。删除原二叉树中所有结点与其右孩子的连线。

      • 层次调整。

      如下图:

      image

    • 二叉树转换为森林

      判断一棵二叉树能否转换为树还是森林的标准是看这棵二叉树根结点有没有右孩子,有就可以转换为森林,没有就只能转换为树。

      转换为森林的步骤为:

      • 从根结点开始,若右孩子存在,则把与右孩子的连线删除在查看分离后的二叉树,递归地,若右孩子存在,则连线删除,直到删除所有的右孩子,此时得到分离的二叉树。

      • 将二叉树分别转换为树即可

      如下图:

      image

    • 树与森林的遍历

      树的遍历有两种方式:

      • 先根遍历
      • 后根遍历

      森林的遍历也有两种方式:

      • 前序遍历
      • 后序遍历

      具体算法类似于二叉树,故在此不再详述。

    5.赫夫曼树及其应用

    • 赫夫曼树

      我们给出这样的定义:一棵二叉树,其结点与孩子之间的路径带有权重,把树的路径长度定义为一棵树的根结点到每一个结点的路径长度之和,在这些结点的所有组合中,使得带权路径长度WPL最小的二叉树就称为赫夫曼树。

    • 构造赫夫曼树:

      我们以下面的二叉树为例子,来说明如何构造赫夫曼树:

      image

      • 先把有权值的叶子结点按照从小到大的顺序排列成一个有序序列,即:A5E10B15D30C40

      • 取头两个最小权值的结点作为一个新结点N1的两个子结点,新结点的权值为两孩子权值的和,注意相对较小的是左孩子,如图:

      image

      • 将N1替换A与E,插入有序序列中,此时为:N115 、B15、D30、C40

      • 重复上述步骤直至完成,如下图:

      image

    • 赫夫曼编码

      一般的,设需要编码的字符集为{d1,d2,...,dn} ,各个字符在电文中出现的频率集合为{w1,w2,...,wn} ,以d1,d2,...,dn 作为叶子结点,以w1,w2,...,wn 作为相应叶子结点的权值来构造一棵赫夫曼树。规定赫夫曼树的左分支代表0,右分支代表1,则从根根结点到叶子结点所经过的路径分支组成的0和1的序列便为该结点对应字符的编码,这就是赫夫曼编码。

      我们通过一个示例来理解赫夫曼编码:

      假设现在有一串字符:BADCADFEED需要编码进行传送,使用一般二进制发送,我们容易想到这样的编码方式:

      image

      这样编码后的数据为:001000011010000011101100100011

      现在我们使用赫夫曼编码来设计这样一种编码方式,假设这几个字母的频率为:A27B8C15D15E30F5

      构造赫夫曼树如下:

      image

      这样编码表便为:

      image

      编码后信息为:1001010010101001000111100

      我们对比一下这两个编码结果:

      第一个001000011010000011101100100011

      第二个1001010010101001000111100

      显然,赫夫曼编码压缩了数据量。

  • 相关阅读:
    AngularJS 简介
    Java基础知识学习(九)
    Java基础知识学习(八)
    算法(二)
    Java基础知识学习(七)
    Java基础知识学习(六)
    Java基础知识学习(五)
    Java基础知识学习(四)
    Java基础知识学习(三)
    Java基础知识学习(二)
  • 原文地址:https://www.cnblogs.com/yczha/p/13160268.html
Copyright © 2011-2022 走看看