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

    0.PTA总分

    1.本周学习总结

    1.1 总结树及串内容

    • 串的BF算法
      BF算法通俗来讲就是暴力算法。
      其暴力的手法:每当匹配失败时,母串从本次匹配开始位置的后一位开始匹配,子串从头开始继续匹配,直到子串匹配完成才算成功。
      当母串长度为m,子串长度为n时,其最坏情况下(母串中无匹配子串),时间复杂度达O(m*n),最好情况下(母串第一位开始就是子串)只有O(1)。
    int  BF(char s[],char t[])
    {
    	int i=0;
    	int j=0;
    	while((i<strlen(s))&&(j<strlen(t)))    //当下标超过数组长度时,查找完成
    	{
    		if(s[i]==p[j])
    		{
    			i++;
    			j++;
    		}
    		else
    		{
    			i=i-j+1;    //母串回到前一次匹配后一位
    			j=0;        //子串回0
    		}
    	}
    	if(j==strlen(p))        //若子串遍历完成,说明匹配成功
    	{
    	return i-j+1;        //返回子串在母串第一次出现的位置
    	}
    	else
    	{
    		return -1;
    	}
    }
    
    • 串的KMP算法
      KMP算法,其实我也只是知道如何去实现,但其中具体的思想还是没整太明白。
      区别于BF的地方:每次匹配失败后,主串的下标i并不会像BF算法一样回退,而是移动子串到母串相应位置即可。
      其中重要的是引入next数组,该数组保存子串第j位前的字符串的最长匹配的前缀后缀
      母串长度m,子串长度n时,时间复杂度O(m+n)。

      next数组:对于abab来说,第4个字符b之前的字符串aba中,第一个a跟最后一个a匹配且长度为1,所以next[3]值为1.由如abcabd,第6个字符d前字符串:abcab,含有 ‘前ab’==‘后ab’,长度为2,所以next[5]=2。

    求next数组

    void GetNext(SqString t,int next[])
    { 
    	int j, k;
    	j=0; k=-1;
            next[0|=-l; 
    	while (j<t.length-l)
    	{
    		if (k=-l || t.data[j]=t.data[kl)
    		{	
    			j++; k++;
    			next[j]=k;
    		}
    		else k=next[k];
    	}
    }
    

    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==-l || 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;//返回不匹配标志
    }
    
    

    二叉树

    二叉树,顾名思义,树的每个结点最多有两个分支,当然也可以只有一个或没有。两个分支根据位置被称作“左子树”和“右子树”。
    满二叉树:可以说是二叉树的理想情况。通俗来说,就是每个结点都有两个子树;转化为数学而言:一棵深度为k的满二叉树,具有的结点数为(2^k)-1

    完全二叉树:满二叉树则是完全二叉树的一种特殊情况。完全二叉树当从上往下,从左往右遍历时,不能有空结点。在最后一层可以不满

    • 存储结构
    1. 顺序存储

      从下标1开始存储具有性质:
      1.非根结点i的父结点序号为[i/2]
      2.结点i的左孩子序号为2i
      3.结点i的右孩子序号为2i+1
      不足之处:当树的数据较为极端时,采用顺序存储会造成空间的浪费。数组通病:不便于插入,删除

    2. 链式结构
      二叉树结点由一个数据元素和分别指向其左、右子树和两个分支构成
      表示二叉树的链表中的结点至少包含3个域:数据域和左、右指针域(lchild,rchild)
      typedef struct BTNode
      {
      ElemType data;
      struct BTNode *lchild, rchild;
      }BTNode,
      BTree;

    • 二叉树建法
    1. 顺序转二叉链
    len=str.length();
    BTree CreateBTree(string str,int i)//下标从1开始
    {
       int len;
       BTree t;
       t=new BTNode;
       if(i>len || i<=0||str[i]=='#') return NULL;
       t->data =str[i];
       t->lchild =CreateBTree(str,2*i); //左子树
       t->rchild =CreateBTree(str,2*i+1); //右子树
       return t;
    }
    
    1. 先序串转二叉链
    void CreateBiTree(BiTree& T,string s)
    {
    	char ch;
    	ch=s[i++];
    	if (ch == '#')  T = NULL;
    	else 
            {
    		T = new BiTNode;
    		T->data = ch;
    		CreateBiTree(T->lchild,s);
    		CreateBiTree(T->rchild,s);
    	}
    }
    

    3.层次法建树

    void CreateBiTree(BTree& BT, string s)//以#开头的字符串
    {
    	int i = 1;
    	BTree T;
    	queue<BTree>Q;
    	if (s[1] != '#')          
    	{
    		BT = new BtNode;
    		BT->data = s[1];
    		BT->lchild = BT->rchild = NULL;
    		Q.push(BT);
    	}
    	else BT = NULL;      //首个字符非空时入队列,否则为空链
    	while (!Q.empty() && i < len-1)
    	{
    		T = Q.front();
    		Q.pop();        
    		i++;
    		if (s[i] == '#')
    		{
    			T->lchild = NULL;
    		}
    		else            //左孩子非空时入队
    		{
    			T->lchild = new BtNode;
    			T->lchild->data = s[i];
    			T->lchild->lchild = T->lchild->rchild = NULL;
    			Q.push(T->lchild);
    		}
    		i++;
    		if (s[i] == '#')
    		{
    			T->rchild = NULL;
    		}
    		else        //右孩子非空时入队
    		{
    			T->rchild = new BtNode;
    			T->rchild->data = s[i];
    			T->rchild->lchild = T->rchild->rchild = NULL;
    			Q.push(T->rchild);
    		}
    	}
    }
    
    • 二叉树遍历
    1. 先序遍历
    void PreorderPrintLeaves( BinTree BT )
    {
        if(BT)                      //节点非空时进入
        {
            if(BT->Left==NULL&&BT->Right==NULL)//节点左右子树均空时进入
            {
                printf(" ");
                printf("%c",BT->Data);
            }
            PreorderPrintLeaves( BT->Left );    //先遍历左子树
            PreorderPrintLeaves( BT->Right );   //遍历右子树
        }
    }
    
    1. 中序遍历
    void InorderPrintNodes( BiTree T)
    {
        if(T)
        {
            InorderPrintNodes(T->lchild); 
            printf(" %c",T->data);
            InorderPrintNodes(T->rchild);
        }
    }
    
    
    1. 后续遍历
    void PostOrder(BTree T) 
    {      
            if (T)  
            {      
                 PostOrder(T->lchild);
    	     PostOrder(T->rchild);
    	     printf("%c ",T->data);
            }
    }
    
    1. 层次遍历
    void levelOrder(BTree root)//层序遍历
    {
            int flag=1;                             //空格输出控制
    	queue<BTree> que;
    	if (root == NULL || len == 1)           //由于是以'#'开始,长度为1 时也为空
    	{
    		cout << "NULL";
    		return;
    	}
    	que.push(root);                         //入队根节点
    	while (!que.empty())                        //队列非空时
    	{
    		BtNode* front = que.front();
    		que.pop();
            if(flag==1)
            {
                cout << front->data;
                flag=0;
            }
            else cout << " " << front->data;        //输出队头元素且出队
    		if (front->lchild) que.push(front->lchild);
    		if (front->rchild) que.push(front->rchild); //左孩子或右孩子非空时入队
    	}
    }
    

    • 树的存储结构

    1.双亲表示法
    结构中至少包含data,parent(指向父亲)
    优势:找父亲容易;不足:找孩子不容易。

    typedef struct 
    {  
        ElemType data;
       int parent;	
    } Tree[MaxSize];
    

    1. 孩子表示法
      结构中包含data,child
      较明显的劣势:当某一个特殊结点孩子较多,其他结点也必须带有一样数目的空节点。显然浪费空间。
    typedef struct TSnode
    {      
            ElemType data;		  
            struct TSnode *child[MaxSons];
    } TNode;
    

    1. 孩子兄弟链存储结构
      优势:可以表示结构较复杂的树,且很好的利用空间
      缺陷:找父亲不容易。可添加parent指针解决。
    typedef struct Tnode 
    {      
            ElemType data;	        //结点的值
            struct Tnode *son;  	//指向兄弟
            struct Tnode *brother;  //指向孩子结点
    } TSBNode;
    

    • 树的操作
      操作中最普遍的还为:遍历、插入、删除。
    1. 遍历
    • 先序遍历

    • 后续遍历

    • 层次遍历

    1. 插入、删除(跟链表操作大同小异)
    • 树的应用
      典型的属哈夫曼树及哈夫曼树编码、线索二叉树、并查集等。

    • 线索二叉树
      普通二叉树中,会有不少的空节点,如何将这些空节点利用起来,就是线索二叉树的作用。
      其结构体中增加了ltag跟rtag两项,作为是否有孩子的标值
      其中:

    • ltag为0时lchild指向该结点的左孩子,为1时lchild指向该结点的前驱;

    • rtag为0时rchlid指向该结点的右孩子,为1时rchlid指向该结点的后继;

    typedef struct TNode
    {
           ElemType data;                                   //结点数据
           struct BTNode *lchild, *rchild;                  //左右孩子指针
           int  ltag;                                       //左右标志
           int  rtal;
    }BTNode, *BTree;
    

    以下图为例,展示3种线索二叉树:

    1. 先序线索二叉树

    2. 中序线索二叉树

    3. 后序线索二叉树

    • 哈夫曼树、哈夫曼编码
      哈夫曼树又称最优树,是一类带权路径长度最短的树。
      树的带权路径长度指的是树中所有叶子结点的带权路径长之和。(wpl值)

    哈夫曼树构建
    简单来讲就是每次取两个最小值进行建树,树的结点为两个孩子之和。

    typedef struct
    {
        int data;
        double weight;
        int parent;
        int lchild;
        int rchild;
    }HTNode;   
    
    void CreateHt(HTNode ht[], int n)
    {
        int i, j, k;
        int lnode, rnode;
        int min1, min2;
        for (i = n; i < 2 * n - 1; i++)
        {
            min1 = min2 = 100000000;          //初始化min值,防止样例值过大
    	//min1最小值,min2次小值
            lnode = rnode = -1;
            ht[i].parent = -1;                  //初始化结点
            for (k = 0; k < i ; k++)
            {
                if (ht[k].parent == -1)         		//当结点无parent时进行,有parent说明结点已是某一结点的左(右)子树
                {
                    if (ht[k].data < min1)      		//找最小值min1,使得左子树为min1
                    {
                        min2 = min1;
                        rnode = lnode;
                        min1 = ht[k].data;
                        lnode = k;
                    }
                    else if(ht[k].data < min2)    		//找次小值min2,使得右子树为min2
                    {
                        min2 = ht[k].data;
                        rnode = k;
                    }
                }
            }
            ht[lnode].parent = i;
            ht[rnode].parent = i;
            ht[i].data = ht[lnode].data + ht[rnode].data;
            ht[i].lchild = lnode;
            ht[i].rchild = rnode;       //构建parent结点,并指向对应的左右子树。
        }
    }
    
    • 哈夫曼树编码

    哈夫曼树编码简单来看是在原有的哈夫曼树基础上,将每个结点左孩子的权值改为0,右孩子权值改为1。
    其主要用于通信方面传输问题

    对于一段文字如:BADCADFEED;用二进制传输。

    传输的数据为 “001000011010000011101100100011”
    当传输一篇文章时,可想长度的可怕。
    由于文本中每个字符的出现频率相加必为1,所以我们用频率作为权值进行建树。之后再更改权值为0或1。

    我们得到新的字符表

    以及新的数据报:1001010010101001000111100
    相对于旧的数据:001000011010000011101100100011
    可见数据被压缩了,在传输大量文本时可节约不少的传输成本。

    • 并查集

    之前网上冲浪时发现了一个跟并查集相关且较有趣的故事,这里进行摘抄转载。原文链接

    江湖上散落着各式各样的大侠,有上千个之多。他们没有什么正当职业,整天背着剑在外面走来走去,碰到和自己不是一路人的,就免不了要打一架。但大侠们有一个优点就是讲义气,绝对不打自己的朋友。而且他们信奉“朋友的朋友就是我的朋友”,只要是能通过朋友关系串联起来的,不管拐了多少个弯,都认为是自己人。这样一来,江湖上就形成了一个一个的帮派,通过两两之间的朋友关系串联起来。而不在同一个帮派的人,无论如何都无法通过朋友关系连起来,于是就可以放心往死了打。但是两个原本互不相识的人,如何判断是否属于一个朋友圈呢?

    我们可以在每个朋友圈内推举出一个比较有名望的人,作为该圈子的代表人物。这样,每个圈子就可以这样命名“中国同胞队”美国同胞队”……两人只要互相对一下自己的队长是不是同一个人,就可以确定敌友关系了。

    但是还有问题啊,大侠们只知道自己直接的朋友是谁,很多人压根就不认识队长抓狂要判断自己的队长是谁,只能漫无目的的通过朋友的朋友关系问下去:“你是不是队长?你是不是队长?”这样,想打一架得先问个几十年,饿都饿死了,受不了。这样一来,队长面子上也挂不住了,不仅效率太低,还有可能陷入无限循环中。于是队长下令,重新组队。队内所有人实行分等级制度,形成树状结构,我队长就是根节点,下面分别是二级队员、三级队员。每个人只要记住自己的上级是谁就行了。遇到判断敌友的时候,只要一层层向上问,直到最高层,就可以在短时间内确定队长是谁了。由于我们关心的只是两个人之间是否是一个帮派的,至于他们是如何通过朋友关系相关联的,以及每个圈子内部的结构是怎样的,甚至队长是谁,都不重要了。所以我们可以放任队长随意重新组队,只要不搞错敌友关系就好了。于是,门派产生了。

    结构体

    typedef struct TREE
    {
            Elemtype data;    //数据
    	int parent;        //双亲下标
    	int rank;        //结点秩
    }UNode;
    

    初始化

    void init(UNode T[], int n)
    {
    	int i = 1;
    	for (i = 1; i <= n; i++)
    	{
                    T[i].data=i;
    		T[i].parent = i;
    		T[i].rank = 0;
    	}
    }
    

    查找元素所属的集合

    int find(UNode T[], int x)
    {
    	if (x != T[x].parent)
    	{
    		return find(T, T[x].parent); //递归找双亲,直到根(boss)。
    	}
    	else return x;
    }
    

    合并集合

    void Union(UNode T[], int x, int y)
    {
    	int a, b;
    	Ux = find(T, x);        //Ux为x的"boss";Uy为y的"boss"
    	Uy = find(T, y);
    	if (T[Ux].rank>=T[b].rank)        //秩小的合并到秩大的"下属"
    	{
    		
    		T[Uy].parent = Ux;
    	}
    	else 
    	{
    		T[Ux].parent = Uy;
    	}
    	
    }
    

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

    整个学下来,感觉有复杂有简单,但总体而言,个人还是觉得较难。尤其是每个题目中如何定义正确的结构体,如何使用结构体,以及如何创建对应结构体的树,这些我觉得是最为需要攻克的难题。一道题的难易、编写代码复杂与否等往往与结构体相关。在写PTA中一些题目的时候,往往是想思路最难,编写过程以及调试最花时间,所以今后要熟悉结构体对数据的掌握。


    2.阅读代码

    2.1 1379. 找出克隆二叉树中的相同节点

    代码

    2.1.1 该题的设计思路

    思路不难,两棵树同时进行层次遍历,然后原树匹配成功的时候,克隆树也就成功了,返回克隆树当前所在结点。

    2.1.2 该题的伪代码

    
    TreeNode* getTargetCopy(TreeNode* original, TreeNode* cloned, TreeNode* target)
    {
        定义ori,clo队列;
        两棵树同步操作:
        入栈根节点push;
        while(两队列均不空时)
        {
            if(ori队头元素值跟目标值相等)return 克隆树队头结点;
            左、右边孩子非空时 左、右孩子入队push;
            队头出队pop
        }end while
        循环结束仍没返回结点说明匹配失败,return NULL;
    }
    

    2.1.3 运行结果

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

    优势:提供了同步操作的思路,我刚开始想的时候是想记录ori树寻找目标值的路径,再将路径放到clo树上。对比之下显得有些麻烦。
    难点:若是直接匹配元素值是否相等的话,若树中本身就有相同元素会造成匹配错误。

    2.2 面试题32 - III. 从上到下打印二叉树 III

    代码

    2.2.1 该题的设计思路

    采用双端队列,层次遍历的变形。树的奇数层采取前取后放,偶数层采取后取前放。

    2.2.2 该题的伪代码

    while(队列非空时)
    {
        while(遍历当前层)
        {
            if(层数为奇数)
            {
                取队头元素;
                队头出队;
                左、右孩子非空时,左、右孩子入队队尾;
            }
            else
            {
                取队尾元素;
                队尾出队;
                右、左孩子非空时,右、左孩子入队队尾;//注意先右后左;
            }
        }
    }
    

    2.2.3 运行结果

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

    优势:采用双端队列可以较好的简化题目,只需将平常的层序遍历反向编写即可。
    难点:直接判断层数的奇偶并不容易,所以用flag变量来记录是正序入队还是反序入队。另外反序入队时,是先右孩子再左孩子。

    2.3 1145. 二叉树着色游戏

    代码

    2.3.1 该题的设计思路

    求出x所在结点的左分支,右分支以及连结父亲的分支各有多少节点数,只要任一结点数大于(n+1)/2,则存在。

    2.3.2 该题的伪代码

    先序遍历树,直到x结点;
    left=GetSum(x->lchild);//遍历x结点左子树
    right=GetSum(x->rchild);//遍历x结点右子树
    连接父亲分支结点数father即为总数-left-right;
    if(三者任一者大等于(n+1)/2)retrun true;
    

    2.3.3 运行结果

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

    优势:将看似难处理的题目,通过数学理解转化为简单的分支结点求和。
    难点:难处就在于能不能看出这个数学问题,当然我刚开始是没有想到的。

    2.3 450. 删除二叉搜索树中的节点

    代码

    2.3.1 该题的设计思路

    所要删除的结点有三种情况:

    1. 所删除结点为叶子结点,可以直接删除。
    2. 所删除结点拥有右节点,则该节点可以由该节点的后继节点进行替代。然后可以从后继节点的位置递归向下操作以删除后继节点。
    3. 所删除的节点没有右节点但是有左节点。用它的前驱节点进行替代,然后再递归的向下删除前驱节点。

    2.3.2 该题的伪代码

    if(key > root.val)//说明要删除的节点在右子树
        root等于rchild;
    if(key < root.val)//说明要删除的节点在左子树
        root等于lchild;
    if(key == root.val)//则该节点就是我们要删除的节点
        if(该节点是叶子节点)//直接删除
            root = null;
        else if(该节点有右节点)
            root.val = successor.val;//用后继节点的值替代
            删除后继节点;
        else if(只有左节点)
            root.val = predecessor.val;//它的前驱节点的值替代
            删除前驱节点;
    

    2.3.3 运行结果

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

    优势:让我真正接触到了二叉树中数据的删除操作,算是增长了见识。
    难点:对于被删除的结点,有多种情况,而每种情况删除的操作并不一样,以及如何找到对应替代结点也是难处。

  • 相关阅读:
    vue--父组件向子组件传参--父组件定义v-bind:参数名--子组件接收props----子组件调用父组件的方法(子组件向父组件传参)父组件@事件名称--子组件接收this.$emit
    vue--组件切换--带动画效果---component
    js string类型转换成数组对象类型---eval
    Vue--创建组件-template---定义私有组件
    vue--自定义标签属性--用于多个事件共同引用一个组件--但是两个事件要实现的功能不同-避免冲突
    vue--组件动画效果--淡入淡出--位移
    vue---向后台添加数据--删除数据--事件方法传参---在单页面配置url请求地址--暂时没有用到webpack
    vscode安装Bootstrap插件--方便补全代码段
    vue-resource--ajax请求数据
    生命周期演示--从加载--渲染--销毁
  • 原文地址:https://www.cnblogs.com/gdlkblue/p/12683219.html
Copyright © 2011-2022 走看看