DS博客作业03--树

0.PTA提交截图

1.本周学习总结

1.1 总结树及串内容

🌈

  • 串即字符串
    • 一般记为 str ="a1a2a3...an"
    • 可以是空串 直接用双引号表示""中间无空格,空格也是一个字符
  • 子串和主串
    • 例如:"BCD"是"ABCD"的子串;主串长度一般大于或等于子串长度;
      值得注意的是字符串比较大小的时候 是比较大小即ASCII码的大小而不是比较长短,这样子的比较往往是没有意义的,所以在字符串的应用上,我们更加重视字符串是否相等或者是否匹配

🌟BF算法:Brute Force

  • 效率低,很朴素

  • 其核心思想:查找主串S中字串T的位置 如果S[0]等于T[0]则两两右移 如果S[1]不等于T[1] 那么又开始判断S[1]是否等于T[0] 不等于则S又下移 等于则重复以上步骤 非常的暴力

    • 具体如图所示:例如主串是s="ABCD" 字串是t="CD" 其查找过程如下
  • 具体实现代码

int index(SqString s,SqString t)
{     int i=0,  j=0;
      while (i<s.length && j<t.length) 
      {       if (s.data[i]==t.data[j])	//继续匹配下一个字符
	{     i++;			//主串和子串依次匹配下一个字符
	      j++;
	}
	else			//主串、子串指针回溯重新开始下一次匹配
	{    i=i-j+1;		//主串从下一个位置开始匹配
	     j=0; 			//子串从头开始匹配
	}
      }
      if (j>=t.length)
	return(i-t.length);	//返回匹配的第一个字符的下标
      else
	return(-1);		//模式匹配不成功
}

为什么说BF算法的效率低?

  • 回溯
    • 👼举个栗子: 主串s="ABCDEFG" 子串t="ABCDF"

🌟KMP算法
由于BF算法效率十分低下 这时候引入了KMP算法 ps:这个算法我也不是很懂,主要是去网上查了一些资料结合老师课件自学的..可能会写错欢迎评论指正

  • KMP算法的核心就是避免不必要的回溯,问题由模式串决定,而不由目标串决定
  • 重要的是模式串中的前缀和后缀匹配的个数
    • 例如:
      ssssd 假设当匹配到d时发生失配 其前缀和后缀相等的个数就为 (是不是很抽象) 画个图来理解一下

      三个相等
  • 此时引用k即next数组
    • 👼我又来举个栗子啦:

      这个匹配过程我就不画了我觉得我描述的挺清楚的哈,电脑画画确实难💡
  • 具体实现代码
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);			//返回不匹配标志
}

虽然说KMP算法效率高,但是还是存在缺陷,比如模式串失配位置的元素和前缀元素相等的时候,这时候就出现了nextval数组
🍒next数组改进:nextval数组

  • 匹配时修正j即可

  • 获取nextval数组的函数

void GetNextval(SqString t,int nextval[])
{
   int j=0,k=-1;
   nextval[0]=-1;
   while (j<t.length)
   { 
   if (k==-1 || t.data[j]==t.data[k])
	{ 
       j++;k++;
	   if (t.data[j]!=t.data[k]) nextval[j]=k;
	   else nextval[j]=nextval[k];
	}
	else
	   k=nextval[k];
   }
}
  • 优化后的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);
}

🌈

🍨二叉树

🌳二叉树的定义和性质

  • 二叉树的定义:是n(n>=0)个结点的有限集合,它或为空树(n=0),或由一个根结点和两棵称为根的左子树和右子树的,互不相交的二叉树组成。
    这定义一看就是递归过程,很晕!注意:二叉树的度一定不超过2,而且具有左右子树之分!!次序不能颠倒,如果只有一颗树一定要区分它是左子树还是右子树哟
  • 二叉树的性质:
    • 二叉树第i层上的结点数目最多为2i-1(i≥1)。
    • 深度为k的二叉树至多有2k-1个结点(k≥1)。
    • 在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。

🎋满二叉树

  • 在一棵二叉树中,如果所有分支节点都存在左子树和右子树,并且所有叶子都在同一层,这样的二叉树称为满二叉树。
  • 特点:
    • 叶子只能出现在最下一层
    • 非叶子结点的度一定是2
    • 在同样深度的二叉树中,满二叉树的结点个数一定最多,同时叶子也是最多的

🎋完全二叉树

  • 定义:对于一颗具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点位置完全相同,则这棵二叉树称为完全二叉树。
    如何理解?:实际上就是满二叉树删除最下面一层右边若干个结点得来的,满二叉树是满的,但是完全二叉树在称为满二叉树之前不会有下一层
  • 特点:
    • 叶子结点只能出现在最下面两层
    • 最下层的叶子结点一定是集中在左部连续位置
    • 倒数第二层如果有叶子节点,一定是集中在右部连续位置
    • 如果结点的度为1,那么该节点只有左孩子
    • 同样节点的二叉树,完全二叉树的深度最小
    • 具有n个结点的完全二叉树的深度为log2n+1 log取下限!
    • 如果对一颗有n个结点的完全二叉树(其深度为log2n取下限+1)的结点按层序编号,对于任意一节点i(1<=i<=n)有以下性质
      ①如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲结点是i/2取整
      ②如果2i>n,则结点i无左孩子(结点i为叶结点),否则其左孩子是结点2i
      ③如果2i+1>n,则结点i无右孩子,否则其右孩子结点为2i+1
      注意:满二叉树一定是完全二叉树,但是完全二叉树不一定是满二叉树

🎋二叉树排序数

  • 一棵空树,或者是具有下列性质的二叉树:
    • 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
    • 若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
    • 左、右子树也分别为二叉排序树;
    • 没有键值相等的结点。

🌳二叉树存储结构、建法、遍历及应用

🎋二叉树的顺序存储结构

  • 顺序存储可以用数组来进行存储,只要将二叉树空的部分用空结点或者#补全成一颗完全二叉树即可,数组第一个可以存放#或者-1
    • 定义typedef ElemType BiTree[Max];
    • 操作过程如下:

      这样的操作极易浪费内存,因为当树结点为空的很多的时候,就会填进去很多个#导致内存浪费,所以这时候引入链式存储结构

🎋二叉树的链式存储结构

  • 定义:
typedef struct node
{
    int data;//数据域
    struct node *lchild;//指向左孩子
    struct node *rchild;//指向右孩子
}BTree;
  • 图示:

🐾二叉树的创建方法总结

🎋二叉树的顺序存储结构转成二叉链

  • 给出一颗二叉树,把它填进数组里然后用#补成满二叉树
    • 具体操作如下:
void CreateBTree(BTree &bt,string str, int i)
{
        int len;
	len = str.size();
	bt = new TNode;
	if (i > len || i < 1)
	{
		bt=NULL;return;//停止递归
	}
	if (str[i] == '#') 
	{
		bt=NULL;  return;//填进去
	}
	bt->data = str[i];//作为新的根节点
	CreateBTree(bt->lchild,str, 2 * i);//创建左子树
	CreateBTree(bt->rchild,str, 2 * i + 1);//创建右子树
}
  • 这种方法比较简单

🎋先序遍历递归建树

  • 首先先了解有哪些遍历方式遍历?
    💎先序遍历 :一棵树的读入顺序为:根节点->左子树->右子树
  • 先序遍历递归算法
 void PreOrder(BTree bt)
{
      if (bt!=NULL)  
      {      printf("%c ",bt->data); 	//访问根结点
             PreOrder(bt->lchild);
             PreOrder(bt->rchild);
      }
}

💎中序遍历:一棵树的读入顺序为:左子树->根节点->右子树

  • 中序遍历递归算法
void InOrder(BTree bt)
{ 
        if (bt!=NULL)  
        {      InOrder(bt->lchild);
	 printf("%c ",bt->data); 	//访问根结点
	 InOrder(bt->rchild);
       }
}

💎后序遍历 :一棵树的读入顺序为:左子树->右子树->根节点

  • 后序遍历递归算法
void PostOrder(BTree bt) 
{
       if (bt!=NULL)  
        {      PostOrder(bt->lchild);
	 PostOrder(bt->rchild);
	 printf("%c ",bt->data); 	//访问根结点
       }
}

💎层序遍历:有点像顺序存储那样子遍历,先依次访问某一层的每个根节点(按顺序),再按照访问顺序分别访问它的左孩子和右孩子

  • 此种遍历方式比较,复杂,但是容易读,容易按顺序输出
  • 需要引用队列注意:环形队列不可用
  • 层序遍历过程:
void LayerOrder(BTreeNode *t)
{
    if(t == NULL)return;
    queue<BTreeNode*> q;
    BTreeNode *temp;
    q.push(t);
    while(!q.empty())
    {
        temp = q.front();
        q.pop();
        cout<<temp->data<<' ';//访问它
        if(temp->lchild != NULL)
            q.push(temp->lchild);
        if(temp->rchild != NULL)
            q.push(temp->rchild);
    }
 }

回到先序遍历建树是不是觉得瞬间眉目舒展
就是那么一回事

BTree CreatTree(string str, int &i)
{
	BTree bt;
	if (i > len - 1) return NULL;//结束递归
	if (str[i] == '#') return NULL;//填空
	bt = new BTNode;//申请内存
	bt->data = str[i];//访问根节点
	bt->lchild = CreatTree(str, ++i);//创建左子树
	bt->rchild = CreatTree(str, ++i);//创建右子树
	return bt;//递归返回根节点
}

🎋层序遍历建树

  • 按层序遍历给定一串树结点例如 A, B, C, D, F, G, I, 0, 0, 0, E, 0, 0, 0
  • 空的用来补齐,而且不需要补满,只需要补齐每个结点的左孩子和右孩子即可
  • 具体算法
void CreateBTree(BTree &BT,string str)
{
      BTree  T;int i=0;
      queue<BTree> Q;//队列 	
	if( str[0]!='0' ){
		BT =new BTNode;
		BT->data = str[0];
		BT->lchild=BT->rchild=NULL;
		Q.push(BT);
	}
	else BT=NULL; //返回空树
	while( !Q.empty())
	{
		T = Q.front();//从队列中取出一结点
		Q.pop();
		i++;
		if(str[i]=='0' ) T->lchild = NULL;
		else //如果有左孩子,则生成新的节点,入队
		{
			T->lchild = new BTNode;
			T->lchild->data = str[i];
			T->lchild->lchild=T->lchild->rchild=NULL;
			Q.push(T->lchild);
		}
		i++; 
		if(str[i]=='0') T->rchild = NULL;
		else //如果有右孩子同理
		{ 
			T->rchild = new BTNode;;
			T->rchild->data = str[i];
		        T->rchild->lchild=T->rchild->rchild=NULL;
			Q.push(T->rchild);
		}
	} 

🎋括号法字符串构造二叉树

  • 碰到左括号表示左孩子,逗号表示右孩子,右括号则出栈
  • 具体算法
void CreateBTNode(BTNode * &b,char *str)
{      //由str  二叉链b
        BTNode *St[MaxSize], *p;
        int top=-1,  k , j=0;  
        char ch;
        b=NULL;		//建立的二叉链初始时为空
        ch=str[j];
        while (ch!='\0')  	//str未扫描完时循环
        {     switch(ch) 
               {
	 case '(': top++; St[top]=p; k=1; break;	//可能有左孩子结点,进栈
	 case ')': top--;  break;
	 case ',': k=2;  break; 			//后面为右孩子结点
        default:        		 //遇到结点值
                p=(BTNode *)malloc(sizeof(BTNode));
	  p->data=ch;  p->lchild=p->rchild=NULL;
	  if  (b==NULL)    	//p为二叉树的根结点
	       b=p;
	  else    			//已建立二叉树根结点
                {     switch(k) 
                       {
	         case 1:  St[top]->lchild=p;  break;
	         case 2:  St[top]->rchild=p;  break;
	         }
                }
         }
         j++;  ch=str[j];		//继续扫描str
     }
}

🐾二叉树的应用
表达式树

  • 具体思路是:
void InitExpTree(BTree &T, string str)
建立两个栈,一个运算符栈一个树根栈
stack<BTree> num;stack<char> op;
然后在运算符栈里存入一个‘#’ 作用是为了控制第一个运算符直接存入以及表达式结束后 建树的操作控制循环条件
while(str[i])
if 是操作数 就直接创建一个树节点 令T->data=操作数 左右子树置空
i++;
else 是运算符
if str[i]的优先级<栈顶运算符
直接让运算符入栈 i++;
else if str[i]的优先级>栈顶运算符
开始建树节点 让运算符栈顶=T->data;
左右子树分别等于从数根栈中弹出的两个树节点
i++;
else if 两个符号优先级相等 
运算符栈出栈;i++;
end if
end if
end while //表达式遍历完后
//开始利用两个栈进行建树操作
while(op.top()!=’#’)//‘#’为建树结束标志
建树操作跟str[i]的优先级>栈顶运算符的操作一样
end while
double EvaluateExTree(BTree T)//计算
后序遍历这棵树的左右节点用左右节点进行+-*/ 
注意除法的时候要注意除数为0的情况
还有判断数是否是空树以及字符数转化数字的操作

注意:控制第一个字符以及下面表达式二叉树的建立我用的是 判断运算符栈是否为空和两个栈是否为空,但是怎么都不行,跟同学讨论了一下知道了使用‘#’来控制的办法,把这个万恶的错误给改了

🍨普通树
🎋树结构

  • 双亲存储结构
    • 根据双亲表示法节点结构定义
typedef struct BTNode
{	
    ElemType data;        //存放树中结点的数据	
    int parent;           //存放双亲下标	
}BTNode;
typedef struct
{	
    CTBox nodes[MAX_TREE_SIZE];	
    int r;                //根的位置
    int n;                //结点的总数
}BTree;
  • 其存放

    存放顺序为先序遍历。
  • 这样的存储结构,我们可以通过结点的parent指针找到他的双亲结构,时间复杂度为O(1),当parent=-1时就是树结点的根
    • 我们可以通过改变BTNode的结构,增加,然后可以增加孩子1或者孩子2;可以关注到它的孩子。
    • 也可以给它的结构,给结构加个部分就是兄弟啥的,就可以指向兄弟
      个人觉得很灵活
      则引入孩子兄弟表示法
      🎋孩子兄弟表示
typedef struct tnode
{
   ElemType data;//结点的值
   struct tnode *son;
   struct tnode *brother;
}TSBNode;
  • 二叉树是指向左子树右子树,而这里的两个结点一个指向兄弟一个指向孩子

🐾树的创建
🎋双亲表示法的创建

PTree InitPNode(PTree tree)
{
    int i,j;
    char ch;
    //printf("请输出节点个数:\n");
    scanf("%d",&(tree.n));

    //printf("请输入结点的值其双亲位于数组中的位置下标:\n");
    for(i=0; i<tree.n; i++)
    {
        fflush(stdin);
        scanf("%c %d",&ch,&j);
        tree.tnode[i].data = ch;
        tree.tnode[i].parent = j;
    }
    return tree;
}

🎋孩子表示法的创建

CTree initTree(CTree tree)
{
    printf("输入节点数量:\n");
    scanf("%d",&(tree.n));
    for(int i=0;i<tree.n;i++)
    {
        printf("输入第 %d 个节点的值:\n",i+1);
        fflush(stdin);
        scanf("%c",&(tree.nodes[i].data));
        tree.nodes[i].firstchild=(ChildPtr*)malloc(sizeof(ChildPtr));
        tree.nodes[i].firstchild->next=NULL;

        printf("输入节点 %c 的孩子节点数量:\n",tree.nodes[i].data);
        int Num;
        scanf("%d",&Num);
        if(Num!=0)
        {
            ChildPtr * p = tree.nodes[i].firstchild;
            for(int j = 0 ;j<Num;j++)
            {
                ChildPtr * newEle=(ChildPtr*)malloc(sizeof(ChildPtr));
                newEle->next=NULL;
                printf("输入第 %d 个孩子节点在顺序表中的位置",j+1);
                scanf("%d",&(newEle->child));
                p->next= newEle;
                p=p->next;
            }
        }
    }
    return tree;
}

🎋双亲孩子表示法的创建

void InitCtree(CTree &t)  //初始化树 
{
    int i;
    printf("请输入树的结点个数:\n");
    scanf("\n%d",&t.n);
    printf("依次输入各个结点:\n"); 
    for(i=0; i<t.n; i++)
    {
      fflush(stdin);
      t.tree[i].data = getchar();
      t.tree[i].r = 0;
      t.tree[i].firstchid = NULL; 
    }
}

void AddChild(CTree &t) //添加孩子
{
    int i,j,k;
    printf("添加孩子\n");           
    for(k=0; k<t.n-1; k++)
    {
      fflush(stdin); 
      printf("请输入孩子结点及其双亲结点的序号:\n");
      scanf("%d,%d",&i,&j); 
      fflush(stdin);
      CNode *p = (CNode *)malloc(sizeof(CNode));
      p->childnode = i;
      p->nextchild = NULL;

      t.tree[i].r = j;                   //找到双亲 

      if(!t.tree[j].firstchid)
          t.tree[j].firstchid = p;
      else
       {
         CNode *temp = t.tree[j].firstchid;
         while(temp->nextchild)
          temp = temp->nextchild;
        temp->nextchild = p;
       }
    }
}

🎋孩子兄弟表示法的创建

void creat_cstree(CSTree &T)
{

    FILE *fin=fopen("树的孩子兄弟表示法.txt","r");  
    char fa=' ',ch=' ';
    for( fscanf(fin,"%c%c",&fa,&ch); ch!='#'; fscanf(fin,"%c%c",&fa,&ch) ) 
    {      
        CSTree p=(CSTree)malloc(sizeof(CSTree)); 
        init_cstree(p);
        p->data=ch;
        q[++count]=p;

        if('#' == fa)
            T=p;
        else
        {
            CSTree s = (CSTree)malloc(sizeof(CSTree));
            int i;
            for(i=1;i<=MAXSIZE;i++)
            {
                if(q[i]->data == fa)    
                {
                    s=q[i];
                    break;
                }
            }
             if(! (s->firstchild) )        //如果该双亲结点还没有接孩子节点
                s->firstchild=p;
            else        //如果该双亲结点已经接了孩子节点
            {
                CSTree temp=s->firstchild;
                while(NULL != temp->nextsibling)
                {
                    temp=temp->nextsibling;
                }
                temp->nextsibling=p;
            }
        }
    } 
    fclose(fin);
}

🐾树的遍历

  • 先根遍历:类似先序遍历,都是先访问根节点,然后按照次序一步一步往右移动,会先序遍历的人应该很容易理解先根遍历,这里就不绘图啦
  • 后根遍历:类似后序遍历,按照顺序树的下面开始遍历 根都是最后访问的,总根在最后一个,注意一定是树不空的时候
  • 层次遍历:哈哈这个是最有次序的与二叉树的层次遍历同理从上到下,然后从左到右

🍨线索二叉树和双向线索二叉树

🐾线索二叉树

  • 我们的二叉树链不是每个结点都会用到,有的结点会造成浪费,而且由于二叉树条条路都走到黑,导致没有我们不知道它前驱后继,我们可以引入利用这个空的结点来记录前驱和后继
    这时候就出现了线索二叉树
    • 这时候我们要考虑一下线索二叉树用什么遍历方式最好呢?只是说中序遍历线索二叉树更好,但是先序后序遍历也是欧克的啦。
      答案:中序遍历 理由如下

      思路很简单:
  • 如果左子树为空,则存放指向中序遍历序列中该结点的前驱结点。这个结点称为该结点的中序前驱;
  • 如果右子树为空,则存放指向中序遍历序列中该结点的后继结点。这个结点称为该结点的中序后继;

🐾双向线索二叉树

  • 这些空结点可以指向它的前驱和后继,当然也不是所有遇到的中序遍历每隔一个都有两个空的结点啦。
    • 于是就有了双向线索二叉树,我们只需要对这个定义进行扩容,加上ltag和rtag即可
    • 定义:
typedef struct node 
  {
          ElemType data;		//结点数据域
          int ltag,rtag;      		//增加的线索标记
          struct node *lchild;		//左孩子或线索指针
          struct node *rchild;		//右孩子或线索指针
  }TBTNode;

ltag,rtag的作用,当它们为0的时候分别指向左孩子和右孩子,当ltag为1时指向前驱,rtag为1的时候指向其后继

  • 我这里拿中序遍历做个示范,其它同理,线索用虚线表示。
  • 中序线索二叉树创建
TBTNode *pre;		   		//全局变量
TBTNode *CreatThread(TBTNode *b)     //中序线索化二叉树
{    TBTNode *root;
     root=(TBTNode *)malloc(sizeof(TBTNode));  //创建头结点
     root->ltag=0; root->rtag=1;  root->rchild=b;
     if (b==NULL) root->lchild=root;	//空二叉树
     else
     {        root->lchild=b;
	pre=root;             	//pre是*p的前驱结点,供加线索用
	Thread(b);   		//中序遍历线索化二叉树
	pre->rchild=root;    	//最后处理,加入指向头结点的线索
	pre->rtag=1;
	root->rchild=pre;    	//头结点右线索化
     }
     return root;
} 
void  Thread(TBTNode *&p)    		//对二叉树b进行中序线索化
{    if (p!=NULL)	
     {  
             Thread(p->lchild);           		//左子树线索化
             if (p->lchild==NULL)          	//前驱线索化
             {     p->lchild=pre; p->ltag=1;  }	//建立当前结点的前驱线索
             else  p->ltag=0;
             if  (pre->rchild==NULL)	     	//后继线索化
            {     pre->rchild=p;pre->rtag=1;}	//建立前驱结点的后继线索
            else  pre->rtag=0;
            pre=p;
           Thread(p->rchild);  		//递归调用右子树线索化
     }
} 

🍨哈夫曼树和哈夫曼编码
🐾哈夫曼树

  • 二叉树的带权路径长度
    • 就是每个结点的值(即权值)和根结点到它的路径(这个路径就是层数-1)相乘的和
    • wpl=wili求和(i>=1)
  • 哈夫曼树就是最优解,即带权路径长度最短的树
    🐾哈夫曼树的构建
  • 原则:
    • 权值越大的叶结点越靠近根结点。
    • 权值越小的叶结点越远离根结点。
      -方法:
    • 把需要建的东西都放到一个集合里,每次都取最小的两个权值建成一棵树,这棵树的根节点的权值是这两个最小的权值相加得到的,然后把这两个最小的值在原来的集合里删去,再把根节点的这个权值放进集合里,再重新找两个最小的来建树,到最后集合里只剩下一棵树,这棵树即是哈夫曼树啦



      🐾哈夫曼编码
  • 哈夫曼编码就是为了使得编码长度最短而出现的
    • 哈夫曼编码有个性质就是一个编码不可能等于另一个编码的前缀。
    • 哈夫曼编码依靠给出的字母出现的的频率,利用这个频率来进行建哈夫曼树的。
    • 然后根据往左走和往右走可以分别在路径上写上0和1 得出来的就是哈夫曼编码
    • 平均码长等于其wpl/出现总次数
  • 哈夫曼编码求法
void CreateHCode(HTNode ht[],HCode hcd[],int n)
{  int i,f,c; HCode hc;
   for (i=0;i<n;i++)		//根据哈夫曼树求哈夫曼编码
   {  hc.start=n;c=i; f=ht[i].parent;
	while (f!=-1)   //循环直到无双亲节点即到达树根节点
	{  if (ht[f].lchild==c)	//当前节点是左孩子节点
	      hc.cd[hc.start--]='0';
	   else	  		//当前节点是双亲节点的右孩子节点
	      hc.cd[hc.start--]='1';
	   c=f;f=ht[f].parent; //再对双亲节点进行同样的操作
 	 }
	 hc.start++;		//start指向哈夫曼编码最开始字符
       hcd[i]=hc;
   }

🍨并查集

  • 并查集特别适合找关系,等于就是把一堆人或者一堆东西,分成一个个集合,每一个集合代表一棵树
    • 要判断某个人和某个人或者某个东西和某个东西是否有关系,那就层层递归,分别找他们的根节点,如果根节点一样,这两个东西或者两个人就有关系
    • 根节点就意味着是老大
  • 并查集从另一个意义上来讲就是一个集合森林
  • 存储结构:
typedef struct node
{
        int data;		//结点对应人的编号
        int rank;  //结点秩:子树的高度,合并用
        int parent;		//结点对应双亲下标
} UFSTree;		//并查集树的结点类型
  • 说明:
  • 一开始的双亲都指向自己,秩都初始化为0

🤞要形成一个集合,肯定是要先合并,形成一个关系网才能进行查找根,判断关系~
那么合并操作怎么做呢!
🍨两个元素各自所属的集合的合并

  • 基本思路在于,谁的轶比较大谁就当另一个的老大,啊呸是双亲
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
          }
}

🍨查找一个元素所属的集合

  • 基本思路:这一个找老大的过程,找到最上一层的那一个根结点,返回它~
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
}

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

难啊,递归搞不懂啊,而且概念说不上复杂就是很多,容易搞混,比如什么线索二叉树双向线索二叉树,晕乎乎的,刷题刷了也不一定会写呀,我翻阅了比较多的资料,就感觉自己的知识比较浅薄,而且我觉得这个对于数学思维要求还是挺高的,就是数学学得好,理解起来会更简单,但是我是属于,图会画,大致上也知道是个什么东西,可是敲代码来我就是个废物。唉,还是要多练把,可以在力扣上找一些简单题写写看,或者看看别人的代码是怎么写的。每次看到别人写递归我觉得我是个弟弟。

2.阅读代码

2.1 树的子结构

🍓题目

输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)

B是A的子结构, 即 A中有出现和B相同的结构和节点值。

例如:
给定的树 A:

 3
/ \

4 5
/
1 2
给定的树 B:

4
/
1
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。

示例 1:

输入:A = [1,2,3], B = [3,1]
输出:false
示例 2:

输入:A = [3,4,5,1,2], B = [4,1]
输出:true

🍓解题代码

class Solution {
public:
    bool helper(TreeNode* A, TreeNode* B) {
        if (A == NULL || B == NULL) {
            return B == NULL ? true : false;
        }
        if (A->val != B->val) {
            return false;
        }
        return helper(A->left, B->left) && helper(A->right, B->right);
    }
    bool isSubStructure(TreeNode* A, TreeNode* B) {
        if (A == NULL || B == NULL) {
            return false;
        }
        return helper(A, B) || isSubStructure(A->left, B) || isSubStructure(A->right, B);
    }
};

2.1.1 该题的设计思路

使用了递归的方法在A中寻找和B根节点数据域相同的节点,当A和B都为空的时候返回false,因为题目约定空树不是任意一个树的子结构。当A,B都不为空的时候,一边进行A左右孩子的遍历,一边比较A与B的数据域。在比较数据域时,若两者指针都为空则代表两者在此处都没有数据存放,返回true,当数据域不相等时,则不符合题意,返回false,当相等时则使用递归继续比较各自左右孩子的数据域。将A与B比较的结果返回到isSubStructure函数中,若有一处返回true则证明B是A的子结构

  • 时间复杂度:O(n^2);n为A的节点数
  • 空间复杂度:O(h^2);h为A树的高度

2.1.2 该题的伪代码

bool isSubStructure(TreeNode* A, TreeNode* B){
 if(A为空或者B为空)返回false;
 返回helper(A,B)或isSubStructure( A->left,B)或isSubStructure( A->right,B);
}
bool helper(TreeNode* A,TreeNode* B){
 if(A为空且B也为空)返回true;
 if(A不为空或者B不为空)返回false;
 返回(A->val==B->val)&&(isSameTree(A->left,B->left)&&isSameTree(A->right,B->right));
}

2.1.3 运行结果

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

  • 该题目的解题使用了递归,缩短了代码长度,每一步的目的都很清晰,能让阅读者对代码易懂。
  • 但是也在郧西过程当中会有许多多余的步骤。
  • 题目难点在于需要先在A中找到与B根节点相同的节点,然后两者再进行结构数值上的比较,对于递归要求比较灵活的运用,并且需要对树空的情况加以区分。

2.2 验证二叉搜索树

🍓题目

给定一个二叉树,判断其是否是一个有效的二叉搜索树。

假设一个二叉搜索树具有如下特征:

节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。

示例 1:

输入:
2
/
1 3
输出: true

示例 2:

输入:
5
/
1 4
/
3 6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
根节点的值为 5 ,但是其右子节点值为 4 。

🍓解题代码

bool fun(struct TreeNode* root, long low, long high) {
    if (root == NULL) return true;
    long num = root->val;
    if (num <= low || num >= high) return false;
    return fun(root->left, low, num) && fun(root->right, num, high);
}
bool isValidBST(struct TreeNode* root){
    return fun(root, LONG_MIN, LONG_MAX);
}

2.2.1 该题的设计思路

  • 对于每个节点值val,设其最低和最高的边界为low , high,用 long 长整型来防止INT_MAX溢出
  • 判断根结点时,条件是val必须大于low,而且小于high ,否则返回 false
  • 判断左右孩子的时候
    • 左孩子的话是low要变化,因为左孩子只能比根节点要小,都比每次的根节点要小,新的low为high与val更小的那个数。但是因为是二叉搜索树所以val必小于high,所以新的low=val
    • 右孩子的话是high要变化,因为右孩子只能比根节点大,比每次的根节点也都要大,新的high为low与val更大的那个数,但是同理,val肯定大于low,所以新的high=val
    • 时间复杂度:O(n)
    • 空间复杂度:O(n)

2.2.2 该题的伪代码

bool fun(struct TreeNode* root, long low, long high)
{
   if 根节点是空 return 真
   end if
   if 根的值小于low或者根的值大于high return 假
   end if
   然后递归
   return fun(左子树作为新的根传进去,又开始比较)&&(比较左子树又比较右子树)
}

2.2.3 运行结果

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

  • 这个代码是我对比其他的代码更加的简洁易懂,就觉得挺强的,思路也很简洁,因为二叉搜索树肯定是越往下根节点要么越大要么越小,不然就直接返回false,这个递归就可以做到这点,low和high运用十分巧妙,代码量也少
  • 此题目的难点就在于,因为每次low和high都是要进行变化的,可是怎么变化,一定要有一个新的思路,不然就无法进行递归,这里传参直接传进num即结点val的值,就是已经在思路理由了,根据二叉搜索树的性质来进行判断新的low和high,我觉得一般人难有这种做法,所以写这个代码的人不是一般人。

2.3 重建二叉树

🍓题目

输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

例如,给出

前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]

返回如下的二叉树:

3

/
9 20
/
15 7

🍓解题代码

 TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) 
{
        if(pre.empty())return nullptr;
        if(vin.empty())return nullptr;
        int idx=0;
        vector<int>vin_left;
        vector<int>vin_right;
        vector<int>pre_left;
        vector<int>pre_right;
        for(int i=0;i<vin.size();i++)
        {
            if(idx)
            {
                vin_right.push_back(vin[i]);
                pre_right.push_back((pre[i]));
            }
            else{
                if(pre[0]==vin[i])idx=1;
                else{
                    vin_left.push_back(vin[i]);
                    pre_left.push_back(pre[i+1]);
                    }
            }
        }
        TreeNode* res = new TreeNode(pre[0]);
        res->left = reConstructBinaryTree(pre_left, vin_left);
        res->right = reConstructBinaryTree(pre_right, vin_right);
        return res;
}

2.3.1 该题的设计思路

  • 基本思路
    • 前序遍历:根结点左子树右子树;中序遍历:左子树根节点右子树
    • 每次都从前序遍历中获得每次的根节点,然后再中序遍历中找到该位置,得到前序遍历和中序遍历的左右子树,然后再分别对左右子树进行同样的操作。
    • 一直递归直到没有左右子树的时候
    • 时间复杂度为O(n);
    • 空间复杂度为O(n);

2.3.2 该题的伪代码

TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin)
{
   if 先序或中序为空 return null
   end if
   int idx=0;//
   分别建立几个存放先序和中序遍历的左右子树的容器
   for i=0 to 中序长度-1
      if (idx)//已经找到根节点
      继续把中序遍历和先序遍历剩下的分别放进属于他们的右子树中
      else if 找到根节点 idx=1;
           else 把中序继续放进它的左子树容器中
                把先序序列则下移
            end if
      end if
   建立一个新的根节点
   然后进行左右子树递归,把容器里的左子树右子树进行递归建树
   一直到没有左右子树的时候

2.3.3 运行结果

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

  • 该题的优势在于它使用了vector容器,相对比上课讲的方法,上课讲的是指针数组来进行建树,来说就更加的不容易出错,因为下标容易越界或者是段错误,而且善用idx,就是提前先弄好的容器里面的东东,最后用容器里的东东来建就好了,就更加的方便,大致思路都和课上差不多,就是觉得这个代码更加巧妙一点,更容易看懂。
  • 难点在于找根的时候先序序列该怎么做,这个是要画图出来看会更加明确,更好理解的。

2.4 相同的树

🍓题目

给定两个二叉树,编写一个函数来检验它们是否相同。

如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例 1:

输入: 1 1
/ \ /
2 3 2 3

    [1,2,3],   [1,2,3]

输出: true

示例 2:

输入: 1 1
/
2 2

    [1,2],     [1,null,2]

输出: false

示例 3:

输入: 1 1
/ \ /
2 1 1 2

    [1,2,1],   [1,1,2]

输出: false

🍓解题代码

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.4.1 该题的设计思路

  • 基本思路:

利用递归的方法,当两指针都不为空的时候检查两者的数据域是否相等,若当前数据域相等则继续比较两者的左孩子,右孩子,若不相等返回false或者。当两指针都指向空的时候代表两者该点没有储存数值,返回true。当两者指针只有一方为空的时候,代表出现了另一方没有的节点,返回false

  • 时间复杂度:O(n),n为节点数
  • 空间复杂度:O(h),h为树的高度

2.4.2 该题的伪代码

bool isSameTree(TreeNode* p,TreeNode* q)
{
 if(q为空且p也为空)返回true;
 end if
 if(p不为空或者q不为空)返回false;
end if
 返回(p->val==q->val)&&(isSameTree(p->left,q->left)&&isSameTree(p->right,q->right));
}

2.4.3 运行结果

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

  • 代码简洁明了,利用递归减少代码的长度,三种情况容易看得清楚,易于理解
  • 这题也没什么特别难的地方,主要是递归比较难写吧
  • 值得注意的是一定要小心空树的情况,不然容易崩溃
  • 使用bool类型,更加方便简洁,不需要设置什么flag之类的东西,直接返回输出即可~
posted @ 2020-04-13 08:43  雪梨wink  阅读(229)  评论(0编辑  收藏  举报