zoukankan      html  css  js  c++  java
  • DS博客作业03--树

    0.PTA得分截图

    树题目集总得分,请截图,截图中必须有自己名字。题目至少完成2/3(不包括选择题),否则本次作业最高分5分。

    1.本周学习总结(0-5分)

    1.1 总结树及串内容

    1.1.1串的BFKMP算法

    串的BF算法与KMP算法,解决的是模式匹配的问题,即如何在已给的字符串中找到对应的子串。根据我们之前的知识,如果想要找子串,我们通常是这样子做的:有两个变量i和j分别记录主串和模式串的下标,我们只需要从左到右比较i指针指向的字符和j指针指向的字符是否一致。如果一致就都向后移动,如果不一致,将子串重新返回到最初的位置,模式串向后平移一个单位。这样依次向下进行比较,直到模式串结束。这种算法思想叫BF算法。简单易懂,代码如下:

    int BF( char* str,  char* sub)//str模式串,sub子串
    {
    	int lenstr = strlen(str);
    	int lensub = strlen(sub);
    	int i = 0, j = 0;	//i记录主串下标,j记录模式串下标。
    	int k = i;
    	while (i < lenstr && j < lensub)
    	{
    		if (str[i] == sub[j])
    		{
    			++i;
    			++j;
    		}
    		else	//匹配失败
    		{
    			j = 0;
    			i = ++k;	//i从主串的下一位置开始,k中记录上一次的起始位置
    		}
    	}
    	if (j < lensub)
    	{
    		return -1;	//匹配失败
    	}
    	else
    	{
    		return k;
    	}
    }
    

    我们假设主串长度为m,模式串长度为n,则简单模式匹配算法的时间复杂度是O(n*m)。是不是很简单?很符合我们的想法?但是,在D.E.Knuth、J.H.Morris和V.R.Pratt这三位大牛看来,BF算法由于不断地回溯,大大增多了操作的步骤,对于数据量庞大的样本,这种算法的弊端更能体现出来。所以怎么办呢?他们发明了一种新的算法KMP算法,KMP算法可以充分利用模式串的部分匹配信息,保持主串i指针不变(不需要回溯主串),通过修改模式串j指针(变动的是模式串下标),让模式串尽量移动到有效的位置,以减少比较次数。可以实现算法时间复杂度为O(m+n)。是不是一下子觉得厉害了许多?但是,大牛的思想我们这样的凡人怎么可能一下子就明白,刚开始看完课件的时候,感觉自己的小脑阔不太够用,一脸懵,经过长时间的消化,终于理解了该算法的精妙之处。现在我们要开始KMP了。

    首先我们在这里引入一个数组next[]。该数组存储的是模式串位置与主串位置i失配时,滑动模式串使其模式串位置为next[j]的字符与主串位置i的字符继续匹配。
    比如模式串p="abaabcac"的各个位置失配时的next[]值怎么求:

    • 步骤如下:
      (1)先将模式串标序号(从0,1开始都可以,我们从0开始)。
      (2)我们要时刻记住next的值表示的是什么,是指失配是子串从哪一个位置开始遍历。以上面的模式串为例子。第一个位置就失配的话,就要移动子串,所以next此时等于-1,如果第二个位置失配,那么观察现在已经有了的子串,发现之前没有出现过这样的字母,所以下一次从第一个位置开始比较,next值为0。如果第三个位置失配,观察子串,发现没有公共的子串,所以next值为0.如果第四个位置失配,看之前的子串,明显“aba”,第一个a和最后一个a一样,所以下一次匹配就不需要从头开始。直接到a的下一个位置即可,所以,next等于1。
    //由模式串t求出next值:
    void GetNext(SqString t,int next[])	 
    {  int j,k;
       j=0;k=-1;next[0]=-1;
       while (j<t.length-1)
       {	if (k==-1 || t.data[j]==t.data[k])
    	{   j++;k++;
    	    next[j]=k;
    	}
    	else k=next[k];
       }
    }
    

    所以KMP算法代码如下:

    KMP算法:
    int KMPIndex(SqString s,SqString t) 
    {  int next[MaxSize],i=0,j=0;
       GetNext(t,next);
       while (i<s.length && j<t.length) 
       {  if (j==-1 || s.data[i]==t.data[j]) 
    	{  i++;
    	   j++;			//i,j各增1
    	}
    	else j=next[j]; 		//i不变,j后退
       }
       if (j>=t.length)
    	return(i-t.length);	//匹配模式串首字符下标
       else
    	return(-1);			//返回不匹配标志
    }
    

    这样看来是不是就完美了?不,虽然next数组有很多的好处,但是下面这一种情况时:

    你会发现,当我失配的时候,前面位置的字母都一样,按道理来说,直接把子串移走,从不匹配的下一个位置开始比较就可以,利用next数组反而增加了比较次数。所以next数组是有缺陷的,所以需要进化。nextval就来了。举个例子:模式串仍然为abaabcac;

    • j=2时,next=0,比较t[0]所对应的元素与t[2]对应的元素是否相同,显然相同。所以next值进行修正,等于t[0]对应的next值,记为nextval。当然如果不相同的话那就不需要进行修正了。
      听了这么多,不如做一道题。
    设目标串为s=“abcaabbcaaabababbca”,模式串p=“babab”。
    1.计算模式p的nextval函数值   
    2.不写算法,画出利用KMP算法进行模式匹配时每一趟的匹配过程。
    
      j            0  1  2  3  4
      t            b  a  b  a  b
    next[j]        -1 0  0  1  2 
    nextval[j]     -1 0 -1  0 -1
    
    第一趟匹配:i=0,j=0,匹配失败,修正j=-1,执行i=i+1=1, j=j+1=0;
    第二趟匹配:i=2,j=1,匹配失败,修正j=0,执行i=i+1=2, j=j+1=0;
    第三趟匹配:i=2,j=0,匹配失败,修正j=-1,执行i=i+1=3, j=j+1=0;
    第四趟匹配:i=3,j=0,匹配失败,修正j=-1,执行i=i+1=4, j=j+1=0;
    第五趟匹配:i=4,j=0,匹配失败,修正j=-1,执行i=i+1=5, j=j+1=0;
    第六趟匹配:i=6,j=0,匹配失败,修正j=0,执行i=i+1=6,j=0;
    第七趟匹配:i=7,j=1,匹配失败,修正j=0,执行i=i+1=7, j=0;
    第八趟匹配:i=7,j=0,匹配失败,修正j=-1,执行i=i+1=8, j=j+1=0;
    第九趟匹配:i=8,j=0,匹配失败,修正j=-1,执行i=i+1=9, j=j+1=0;
    第十趟匹配:i=9,j=0,匹配失败,修正j=-1,执行i=i+1=10, j=j+1=0;
    第十一趟匹配:i=10,j=0,匹配失败,修正j=-1,执行i=i+1=11, j=j+1=0;
    第十二趟匹配:i=16,j=5,匹配成功返回i-p.length=11;
    
    //修改后的KMP算法:
    int KMPIndex1(SqString s,SqString t)
    {  int nextval[MaxSize],i=0,j=0;
       GetNextval(t,nextval);
       while (i<s.length && j<t.length) 
       {  if (j==-1 || s.data[i]==t.data[j]) 
    	{  i++;
    	   j++;
    	}
    	else
    	   j=nextval[j];
       }
       if (j>=t.length)
    	return(i-t.length);
       else
    	return(-1);
    }
    

    1.1.2二叉树

    之前学的链表,栈,队列都属于线性结构,但是生活并不只是一条线,他是许许多多个个体组成的;一对一的关系并不是绝对,一对多,多对多,世界才精彩。扯的有点远了。我们要开始进入非线性结构的世界了。首先,了解基本术语。

    • 结点:指树的数据元素;
    • 结点的度:指结点接挂的子树数,分支数目;
    • 根:即根节点,没有前驱;
    • 叶子:指的是终端结点(没有后继,度为0的结点);
    • 森林:指m棵不相交树的集合;
    • 树的深度:指所有结点中最大的层数;
    • 双亲:指上层结点;
    • 孩子:指下层结点的子树的根;
    • 兄弟:指同一双亲下的同层结点;

    (1)二叉树的定义;

    二叉树是每个结点最多有两个子树的树结构。它有五种基本形态:二叉树可以是空集;根可以有空的左子树或右子树;或者左、右子树皆为空。

    (2)二叉树的性质;

    • 性质1:二叉树第i层上的结点数目最多为2i-1(i>=1)
    • 性质2:深度为k的二叉树至多有2k-1个结点(k>=1)
    • 性质3:包含n个结点的二叉树的高度至少为(log2n)+1
    • 性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1
    • 注:性质4的证明:
      证明:因为二叉树中所有结点的度数均不大于2,不妨设n0表示度为0的结点个数,n1表示度为1的结点个数,n2表示度为2的结点个数。三类结点加起来为总结点个数,于是便可得到:n=n0+n1+n2  (1)
      由度之间的关系可得第二个等式:n=n00+n11+n2*2+1即n=n1+2n2+1  (2)
      将(1)(2)组合在一起可得到n0=n2+1

    (3)满二叉树、完全二叉树;

    • 满二叉树:高度为h,并且由2h-1个结点组成的二叉树,称为满二叉树
    • 完全二叉树:一棵二叉树中,只有最下面两层结点的度可以小于2,并且最下层的叶结点集中在靠左的若干位置上,这样的二叉树称为完全二叉树。
      特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。

    (4)二叉树的存储结构;

    • 1.顺序存储结构
      二叉树的顺序存储,就是用一组连续的存储单元存放二叉树中的结点。因此,必须把二叉树的所有结点安排成为一个恰当的序列,结点在这个序列中的相互位置能反映出结点之间的逻辑关系,用编号的方法从树根起,自上层至下层,每层自左至右地给所有结点编号,缺点是有可能对存储空间造成极大的浪费,在最坏的情况下,一个深度为k且只有k个结点的右单支树需要2k-1个结点存储空间。依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一地反映出结点之间的逻辑关系,这样既能够最大可能地节省存储空间,又可以利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。
    #define Maxsize 100     //假设一维数组最多存放100个元素
    typedef char Datatype;  //假设二叉树元素的数据类型为字符
    typedef struct
    { Datatype bt[Maxsize];
        int btnum;
      }Btseq;
    
    • 2.链式存储结构
      二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。其结点结构为:

      其中,data域存放某结点的数据信息;lchild与rchild分别存放指向左孩子和右孩子的指针,当左孩子或右孩子不存在时,相应指针域值为空(用符号∧或NULL表示)。利用这样的结点结构表示的二叉树的链式存储结构被称为二叉链表。尽管在二叉链表中无法由结点直接找到其双亲,但由于二叉链表结构灵活,操作方便,对于一般情况的二叉树,甚至比顺序存储结构还节省空间。因此,二叉链表是最常用的二叉树存储方式。
    #define datatype char  //定义二叉树元素的数据类型为字符
    typedef struct  node   //定义结点由数据域,左右指针组成
    { 
        Datatype data;
        struct node *lchild,*rchild;
    }Bitree;
    

    (5)二叉树的创建与遍历;

    • 二叉树的建立;
      树形结构要多利用递归来求解,递归的关键就是想清楚所有的基准情形,然后扩展到一般情况,写代码的时候最好把基准情况放在前面,把一般情况放在后面!
    //定义二叉树结构体:
    typedef struct BinaryTreeNode
    {
        TelemType data;
        struct BinaryTreeNode *Left;
        struct BinaryTreeNode *Right;
    }Node;
    //二叉树的建立;
    Node* createBinaryTree()
    {
        Node *p;
        TelemType ch;
        cin>>ch;
        if(ch == 0)     //如果到了叶子节点,接下来的左、右子树分别赋值为0
        {
            p = NULL;
        }
        else
        {
            p = new Node;
            p->data = ch;
            p->Left  = createBinaryTree();  //递归创建左子树
            p->Right = createBinaryTree();  //递归创建右子树
        }
        return p;
    
    • 二叉树的遍历;
    //先序遍历
    void Preorder (BiTree T)
    {
       if (T) {
          printf(" %c",T->data);   // 访问根结点
     
          Preorder(T->lch); // 遍历左子树
          Preorder(T->rch);// 遍历右子树
       }
    }
     
    //中序遍历
    void Inorder (BiTree T)
    {
         if(T) {
           Inorder(T->lch);
     
           printf(" %c",T->data);
     
           Inorder(T->rch);
           }
    }
     
    //后序遍历
    void Postorder (BiTree T)
    {
         if(T) {
           Postorder(T->lch);
           Postorder(T->rch);
     
           printf(" %c",T->data);
         }
    }
    
    

    1.1.3树

    了解了二叉树后,我们就开始学习树了,二叉树只是树的一种;

    (1)树的存储结构;

    说到存储结构,就会想到我们前面章节讲过的顺序存储和链式存储两种结构。先来看看顺序存储结构,用一段地址连续的存储单元依次存储线性表的数据元素。这对于线性表来说是很自然的,对于树这样一多对的结构呢?树中某个结点的孩子可以有多个,这就意味着,无论按何种顺序将树中所有结点存储到数组中,结点的存储位置都无法直接反映逻辑关系,你想想看,数据元素挨个的存储,谁是谁的双亲,谁是谁的孩子呢?简单的顺序存储结构是不能满足树的实现要求的。不过充分利用顺序存储和链式存储结构的特点,完全可以实现对树的存储结构的表示。我们这里要介绍三种不同的表示法:双亲表示法、孩子表示法、孩子兄弟表示法。

    • 双亲表示法
      我们人可能因为种种原因,没有孩子,但无论是谁都不可能是从石头里蹦出来的所以是人一定会有父母。树这种结构也不例外,除了根结点外,其余每个结点,它不一定有孩子,但是一定有且仅有一个双亲。
      我们假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。也就是说,每个结点除了知道自己是谁以外,还知道它的双亲在哪里。
    typedef struct 
    {  ElemType data;	//结点的值
        int parent;		//指向双亲的位置
    } PTree[MaxSize];
    
    

    但是这样的存储结构找双亲容易,找孩子不容易;

    • 孩子表示法
      换一种完全不同的考虑方法 . 由于树中每个结点可能有多棵子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法。不过,树的每个结点的度,也就是它的孩子个数是不同的。所以我们可以根据这棵树的最大度,设计一个数组,存储它的所有子树。不过,这样做的话,当每个结点度相差过大的时候会造成指针的极大的浪费。并且不容易找到父亲结点;
    typedef struct node
    {      ElemType data;		  //结点的值
            struct node *sons[MaxSons];	      //指向孩子结点
    }  TSonNode;
    
    • 孩子兄弟表示法
      刚才我们分别从双亲的角度和从孩子的角度研究树的存储结构,如果我们从树结点的兄弟的角度又会如何呢? 当然,对于树这样的层级结构来说,只研究结点的兄弟是不行的,我们观察后发现,任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。 因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
      兄弟链存储结构中结点的类型声明如下:
    typedef struct tnode 
    {      ElemType data;	//结点的值
            struct tnode *son;  	//指向兄弟
            struct tnode *brother;  2//指向孩子结点
    } TSBNode;
    

    这种表示法,给查找某个结点的某个孩子带来了方便,只需要通过son找到此结点的长子,然后再通过长子结点的brother找到它的二弟,接着一直下去,直到找到具体的孩子。这个表示法的最大好处是它把一棵复杂的树变成了一棵二叉树。但是类似二叉树,这种做法找父亲仍然不容易。

    (2)树的遍历;

    树的遍历运算是指按某种方式访问树中的每一个结点且每一个结点只被访问一次。与二叉树的遍历相同,树的遍历同样分为前序遍历,中序遍历,后序遍历,还有层次遍历;

    • 先根遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树。
    • 后根遍历:若树不空,则先依次后根遍历各棵子树,然后访问根结点。
    • 层次遍历:若树不空,则自上而下、自左至右访问树中每个结点。
      比如下面这一棵树:
    • 先序遍历:FCADBEHGM
    • 后序遍历:ABDCHMGEF
    • 中序遍历:ACBDFHEMG
    • 层序遍历:FCEADHGBM,层序遍历一般很少用。

    (3)树的建立;

    同一棵二叉树(假设每个结点值唯一)具有唯一先序序列、中序序列和后序序列。但不同的二叉树可能具有相同的先序序列、中序序列或后序序列。(序列中不包括空节点)。所以,仅由先序、中序或后序序列中的一种,无法唯一构造出该二叉树。

    • 同时给定一棵二叉树的先序序列和中序序列就能唯一确定这棵二叉树。
    • 同时给定一棵二叉树的中序序列和后序序列就能唯一确定这棵二叉树。
    • but 同时给定一棵二叉树的先序序列和后序序列不能唯一确定这棵二叉树。 因为无法确定左右子树;
    (一)由先序和中序序列构造二叉树示例;
    • 算法:
      1、通过先序遍历找到根结点A,再通过A在中序遍历的位置找出左子树,右子树
      2、在A的左子树中,找左子树的根结点(在先序中找),转步骤1
      3、在A的右子树中,找右子树的根结点(在先序中找),转步骤1
    • 实现代码:
    node* CreateBT1(char* pre, char* in, int n)
    {
        node* b;
        char* p;
        int k;
     
        if (n <= 0 || pre == nullptr || in==nullptr)    
            return nullptr;
     
        b = (node*)malloc(sizeof(node));
        b->data = *pre;
        b->mparent = tmpParent;
     
        for (p = in; p < in + n; ++p)
            if (*p == *pre)
                break;
     
        k = p - in;
        b->lchild = CreateBT1(pre + 1, in, k);
        b->rchild = CreateBT1(pre + k + 1, p + 1, n - k - 1);
        
        return b;
    }
    
    (二)由后序和中序序列构造二叉树示例;
    • 实现代码:
    node* CreateBT2(char* post/*指向后序序列开头的指针*/, char* in/*指向中序序列开头的指针*/, int n)
    {
     
        char r, *p;
     
        int k;
      
        if (n <= 0 || post== nullptr || in==nullptr)     
            return nullptr;
     
        r = *(post + n - 1);
        node* b = (node*)malloc(sizeof(node));
        b->data = r;      //我们要创建的树根节点建立好了
     
        for (p = in; p < in + n; ++p)
            if (*p == r) break;
     
        k = p - in;      //k是左子树节点数
     
        b->lchild = CreateBT2(post, in, k);          
        b->rchild = CreateBT2(post + k, p + 1, n - k - 1);
     
        return b;
    }
    

    1.1.4线索二叉树

    (1)线索二叉树的原理

    通过考察各种二叉链表,不管儿叉树的形态如何,空链域的个数总是多过非空链域的个数。准确的说,n各结点的二叉链表共有2n个链域,非空链域为n-1个,但其中的空链域却有n+1个。因此,提出了一种方法,利用原来的空链域存放指针,指向树中其他结点。这种指针称为线索。

    • 记ptr指向二叉链表中的一个结点,以下是建立线索的规则:
      (1)如果ptr->lchild为空,则存放指向中序遍历序列中该结点的前驱结点。这个结点称为ptr的中序前驱;
      (2)如果ptr->rchild为空,则存放指向中序遍历序列中该结点的后继结点。这个结点称为ptr的中序后继;
      简单的说,如果你使用的是中序遍历,左子树指的是该结点在中序遍历中的前驱结点,右子树指的是该结点在中序遍历中的后继结点; 显然,在决定lchild是指向左孩子还是前驱,rchild是指向右孩子还是后继,需要一个区分标志的。因此,我们在每个结点再增设两个标志域LTag和LTag,LTag=0时,ptr->lchild指向左孩子,为1时指向前驱。
      因为二叉树可以进行先序遍历,中序遍历,后序遍历,所以也有先序二叉树,中序二叉树,后序二叉树。

    (2)遍历线索化二叉树

    遍历线索化二叉树步骤:

    • 1.找中序遍历的第一个结点:左子树上处于“最左下”(没有左子树)的结点。
    • 2.找中序线索化链表中结点的后继 :若无右子树,则为后继线索所指结点; 否则为对其右子树进行中序遍历时访问的第一个结点。
    void ThInOrder(TBTNode *tb)
    {      TBTNode *p=tb->lchild;		//p指向根结点
           while (p!=tb)//tb头结点
           {     
                  while (p->rtag==0)   p=p->lchild;		//找开始结点
    	printf("%c",p->data);			//访问开始结点
    	while (p->rtag==1 && p->rchild!=tb)
    	{     p=p->rchild;
    	       printf("%c",p->data);
    	}
    	p=p->rchild;
          }
    } 
    

    1.1.5哈夫曼树、并查集

    1. 什么是哈夫曼树?他和其他的树有什么不同呢?

    如果给你n个权值作为n的叶子结点,让你构造一棵二叉树,并且要求带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树。

    2. 如何构建哈夫曼树呢?

    要记住,哈夫曼树是对于数据来说是最优的二叉树。即如果每个叶子结点有权值的话,要使得建成的整棵树的权值总和最小。哈夫曼树是带权路径长度最短的树。所以构建树的时候要记住,权值较大的结点离根较近一点,权值较小的结点离根远一点。
    构造哈夫曼树的过程:

    • (1)根据给定的n个权值{w1,w2,……wn},构造n棵只有根结点的二叉树。F={T1,T2,…,Tn}。
    • (2)在F中选取根结点的权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,这棵新的二叉树根结点的权值为其左、右子树根结点权值之和。
    • (3)在集合F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到集合F中。
    • (4)重复(2)、(3)两步,当F中只剩下一棵二叉树时,这棵二叉树便是所要建立的哈夫曼树。
      举个例子,建立例如:已知权值 W={ 5, 6, 2, 9, 7 },建立一颗哈夫曼树;要记住,最小的每次要最小的两个结点作为左右子树进行构建成一颗新的二叉树;当然,由于每个人对左右子树的摆放顺序不一致,得到的哈夫曼树也是不一样的,但是如何检验一棵树是否为哈夫曼树呢?它的WPL即带权路径长度是最小的;

      最后建起来的树是这个样子的;
    /*建立哈夫曼树的代码*/
    typedef struct
    {	char data;		//节点值
    	float weight;	//权重
    	int parent;		//双亲节点
    	int lchild;		//左孩子节点
    	int rchild;		//右孩子节点
    } HTNode;
    
    void CreateHT(HTNode ht[],int n)
    {  int i,j,k,lnode,rnode; float min1,min2;
       //此处补充叶子节点相关设置
       for (i=0;i<2*n-1;i++)	  	//所有节点的相关域置初值-1
          ht[i].parent=ht[i].lchild=ht[i].rchild=-1;
       for (i=n;i<2*n-1;i++)		//构造哈夫曼树
       {  min1=min2=32767; lnode=rnode=-1;
    	for (k=0;k<=i-1;k++)
    	  if (ht[k].parent==-1)		//未构造二叉树的节点中查找
    	  {  if (ht[k].weight<min1)
    	     {  min2=min1;rnode=lnode;
    		  min1=ht[k].weight;lnode=k;  }
    	     else if (ht[k].weight<min2)
    	     {  min2=ht[k].weight;rnode=k;  }   
            } //if
    	  ht[lnode].parent=i;ht[rnode].parent=i;
            ht[i].weight=ht[lnode].weight+ht[rnode].weight;
    	  ht[i].lchild=lnode;ht[i].rchild=rnode;
       }
    } 
    

    3.并查集;

    • 查找一个元素所属的集合及合并2个元素各自专属的集合等运算。
    • 应用:亲戚关系、朋友圈应用。
      在并查集中,每个分离集合对应的一棵树,称为分离集合树。整个并查集也就是一棵分离集合森林。
    • 集合查找:在一棵高度较低的树中查找根结点的编号,所花的时间较少。同一个集合标志就是根(parent)是一样。
    • 集合合并:两棵分离集合树A和B,高度分别为hA和hB,则若hA>hB,应将B树作为A树的子树;否则,将A树作为B树的子树。总之,总是高度较小的分离集合树作为子树。得到的新的分离集合树C的高度hC =MAX{hA,hB+1}。
    1)并查集树的初始化
    void MAKE_SET(UFSTree t[],int n)  //初始化并查集树
    {      int i;
            for (i=1;i<=n;i++)
            {	t[i].data=i;		//数据为该人的编号
    	t[i].rank=0;		//秩初始化为0
    	t[i].parent=i;		//双亲初始化指向自已
             }
    }
    
    2)查找一个元素所属的集合
    int FIND_SET(UFSTree t[],int x)    //在x所在子树中查找集合编号
    {      if (x!=t[x].parent)		                    //双亲不是自已
    	return(FIND_SET(t,t[x].parent));   //递归在双亲中找x
           else
    	return(x);			      //双亲是自已,返回x
    }
    
    3)两个元素各自所属的集合的合并
    void UNION(UFSTree t[],int x,int y)       //将x和y所在的子树合并
    {        x=FIND_SET(t,x);	        //查找x所在分离集合树的编号
              y=FIND_SET(t,y);	        //查找y所在分离集合树的编号
              if (t[x].rank>t[y].rank)	        //y结点的秩小于x结点的秩
    	t[y].parent=x;		        //将y连到x结点上,x作为y的双亲结点
              else			        //y结点的秩大于等于x结点的秩
              {	    t[x].parent=y;		        //将x连到y结点上,y作为x的双亲结点
    	    if (t[x].rank==t[y].rank)      //x和y结点的秩相同
    	          t[y].rank++;	        //y结点的秩增1
              }
    }
    

    1.2.谈谈你对树的认识及学习体会。

    树这一部分的知识比较抽象,所以学习的时候比较困难。刚开始网上所给的的时候学KMP算法,看了很长时间都没有搞清楚next 和nextval该如何求,他们代表了什么。甚至已经上完了课。还是不甚理解。所以我查找了网上的有关KMP算法的资料。发现网上所给的讲解也是五花八门。经过了好几天的揣摩,终于看懂了它的原理。所以呢,有时候外界的资料只能对你有一定的辅助效果,不能完全依靠他。他会给予你一些灵感,但是最后还是要靠自己理解。后来学到了树,树的结构比较好理解,但是建立啊创建什么的与之前学的知识又不一样。所以一直在啃老底也是行不通的,只有不断地充电吸取新的知识,才能触类旁通。苟日新,日日新,日又新。

    2.阅读代码(0--5分)

    2.1 题目及解题代码

    //解题代码:
    class Solution {
    public:
        bool isSameTree(TreeNode* p, TreeNode* q) {
            if(!p && !q) return true;
            if(!p || !q) return false;
            return (p->val == q->val) 
                    && (isSameTree(p->left,q->left) 
                    && isSameTree(p->right,q->right));
        }
    };
    

    2.1.1 该题的设计思路

    利用迭代的方法,从根开始,每次迭代将当前结点从双向队列中弹出。然后,进行判断:p 和 q 不是 None,p.val 等于 q.val,若以上均满足,则压入子结点。

    • 时间复杂度 : O(N),其中 N 是树的结点数,因为每个结点都访问一次。
    • 空间复杂度 : O(N)。

    2.1.2 该题的伪代码

     bool isSameTree(TreeNode* p, TreeNode* q) {
            if (p和q都时为NULL时) 返回 true;
            if (有一个不是空) 返回 false;
            return (p->val == q->val)&& (isSameTree(p的左子树, q的左子树)&& isSameTree(p的右子树, q的右子树);
        }
    

    2.1.3 运行结果

    2.1.4分析该题目解题优势及难点。

    • 优势:简单易懂,只要有一种结果不相同就返回flase;
    • 难点:利用迭代法写代码的时候比较麻烦。

    2.2 题目及解题代码

     unordered_map<TreeNode*,int> mp;
        int dfs(TreeNode* root,int flag)
        {
            if(root==NULL) return 0;
            else
            {
                if(flag==1)
                    return dfs(root->left,0)+dfs(root->right,0);
                else
                {
                    if(mp.find(root)!=mp.end())
                        return max(mp[root],dfs(root->left,0)+dfs(root->right,0));
                    int tp=root->val+dfs(root->left,1)+dfs(root->right,1);
                    mp[root]=tp;
                    return max(mp[root],dfs(root->left,0)+dfs(root->right,0));
                }
            }
        }
        int rob(TreeNode* root) {
            return dfs(root,0);
        }
    

    2.2.1 该题的设计思路

    两种遍历方式:

    • 1、选择当前节点,那么左右孩子节点不能选择

    • 2、不选当前节点,左右俩孩子节点可以选
      减枝:对于选择当前节点的值的情况进行记录即可~

    • 时间复杂度O(n);

    • 空间复杂度O(n);

    2.2.2 该题的伪代码

    int dfs(TreeNode* root, int flag)
    {
        if (根结点root为空) return 0;
        else
        {
            if (flag 等于 1) return dfs(左子树, 0) + dfs(右子树, 0);
            else
            {
                if (mp.find(root) != mp.end())   return max(mp[root], dfs(左子树, 0) + dfs(右子树, 0));
                定义 tp表示所得资金总数 tp = 根节点资金 + 左子树资金 + 右子树资金;
                mp[root] = tp;
                return 最大的资金数;
            }
        }
    }
    

    2.2.3 运行结果

    2.2.4分析该题目解题优势及难点。

    • 优势:刚开始看题目的时候有点头绪,不过还是不好理清,但是要理解:任何一个节点能偷到的最大钱的状态可以定义为
      当前节点选择不偷:当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱
      当前节点选择偷:当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数;这样就能更好的找到递归式子,代码也就好写了。
    • 难点:难点也是如何找见这样的关系,理清头绪;

    2.3 题目及解题代码

     bool isSymmetric(TreeNode* root) {
            bool res = true;
            if (root!=NULL)
            {
                res = helper(root->left,root->right);
            }
            return res;
        }
    
     bool helper(TreeNode*A, TreeNode* B)
        {
            if (A==NULL && B==NULL)
                return true;
            if (A==NULL || B==NULL)
                return false;
            if (A->val != B->val)
                return false;
            return helper(A->left,B->right) && helper(A->right,B->left);
        }
    

    2.3.1 该题的设计思路

    找到递归点:左树与右树对称与否,与其跟两树的子树的对称情况有关系。
    递归结束条件:

    • 都为空指针则返回 true

    • 只有一个为空则返回 false

    • 两个指针当前节点值不相等 返回false
      递归过程:

    • 判断 A 的右子树与 B 的左子树是否对称

    • 判断 A 的左子树与 B 的右子树是否对称

    • 时间复杂度:O(n),因为我们遍历整个输入树一次,所以总的运行时间为O(n),其中n是树中结点的总数。

    • 空间复杂度:递归调用的次数受树的高度限制。在最糟糕情况下,树是线性的,其高度为O(n)。因此,在最糟糕的情况下,由栈上的递归调用造成的空间复杂度为O(n)。

    2.3.2 该题的伪代码

    bool helper(TreeNode* A, TreeNode* B)
    {
        if (A与B都是空的) return true;  递归终止条件
        if (A与B中仅有一个是空的)return false;  如果其中之一为空,也不是对称的
        if (A与B所对应的值不相同)   return false;
        return helper(A左子树, B右子树) && helper(A右子树, B左子树);前序遍历
    }
    

    2.3.3 运行结果

    2.3.4分析该题目解题优势及难点。

    • 优势:利用递归的方法,代码量少。比较简单。
    • 难点:写递归方程的时候容易忽略某些判断条件导致无法正确的找到出口。

    2.4 题目及解题代码

    vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
            vector<vector<int>> result;
            if (!root) return result;
            stack<TreeNode*> left, right;
            left.push(root);
            while (!left.empty() || !right.empty()) {
                if (!left.empty()) {
                    result.push_back(vector<int>());
                    while (!left.empty()) {
                        TreeNode* curr = left.top();
                        left.pop();
                        result.back().push_back(curr->val);
                        if (curr->left) right.push(curr->left);
                        if (curr->right) right.push(curr->right);
                    }
                }
                if (!right.empty()) {
                    result.push_back(vector<int>());
                    while (!right.empty()) {
                        TreeNode* curr = right.top();
                        right.pop();
                        result.back().push_back(curr->val);
                        if (curr->right) left.push(curr->right);
                        if (curr->left) left.push(curr->left);
                    }
                }
            }
            return result;
        }
    

    2.4.1 该题的设计思路

    这道题类似于之前的层次遍历,但是它每遍历一层的之后需要改变方向进行下一层的遍历。所以用了两个栈,先将一层入到一个栈中,然后这个栈的节点出栈,在输出之后,把它的孩子存入另外一个栈中,由于栈先进后出的特点,所以每一层的顺序转换就可以运用了。

    • 时间复杂度:树节点为n,分别进栈一次出栈一次,所以O(2n);
    • 空间复杂度:O(n);

    2.4.2 该题的伪代码

    定义两个栈s1,s2
    if(树是否为空)为空直接返回
    将根节点压入s1栈
    while (s1或s2中有一个不为空)
    {
       while (s1不为空)
    	{
    	   取s1栈顶,出栈栈顶元素,并将其输出
    	   if(栈顶左子树不空) 将左子树入s2栈
    	   if(栈顶右子树不空) 将右子树入s2栈
        }
       while (s2不为空) 存入栈s1中,其余与上述操作一样
       
    }
    

    2.4.3 运行结果

    2.4.4分析该题目解题优势及难点

    • 优势:利用我们学过的栈与队列的知识可以很好的解决问题。
    • 难点:每一层方向的改变是本题的难点。
  • 相关阅读:
    vue 富文本编译器 vue-quill-editor
    vue-拖拽的使用awe-dnd
    Dapper是什么?
    如何运用领域驱动设计
    面试官问我MySQL索引,我
    MySQL:你知道什么是覆盖索引吗?
    mysql覆盖索引与回表
    C#.NET 字符串转数组,数组转字符串
    MYSQL如何让主键使用BTREE索引
    MySQL大表优化方案 Mysql的row_format(fixed与dynamic)
  • 原文地址:https://www.cnblogs.com/shenchao123/p/12669906.html
Copyright © 2011-2022 走看看