• DS博客作业03--树


    0.PTA得分截图


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

    1.1串

    1.串的存储结构

    (1)定长顺序存储:类似线性表顺序存储,由一组地址连续的存储单元存储
    (2)堆分配存储:也由一组地址连续的存储单元,存储空间是在程序执行过程中动态分配new char[length]
    (3)链式存储:一个结点可以存储一个或多个字符(通常将链串中每个结点所存储的字符个数称为结点大小),但有时操作不便

    2.串的模式匹配算法

    (较多应用于文本查重)
    主串:目标串
    子串:模式串

    ①BF算法(需回溯)

    若主串长度为n,子串长度为m
    因为其主串和子串位置指针都是需要回溯,则:
    最好情况时间复杂度为O(m)而最坏情况则可达到O(n*m)

    主串s
    子串t//string型
    while (i<s.size()&& j<t.size()) 
    {   
        if (s.data[i]==t.data[j])//若当前字符匹配,则继续匹配下一个字符
        {    
              i++;//主串和子串都需要向后移动
    	  j++;
        }
        else//主串、子串指针回溯重新开始下一次匹配
        {
              i=i-j+1;//主串从上一次匹配开始位置的下一个位置开始匹配
    	  j=0;//子串从头开始匹配
        }
    }
    

    ②kmp算法(不需回溯)
    next数组
    当 t[j] == t[k] 时,t[0]…t[k-1]" == " t[j-k]…t[j-1],且 t[j] == t[k],所以next[j+1]=k+1
    当 t[j] == t[k] 时,t[j+1] 的最大子串的长度为 k,
    当 t[j] != t[k] 时, next[j+1] < k,
    那么求 next[j+1] = 求 t[j-k+1] ~ t[j] 与 t[0] ~ t[k-1] 的符合 k 的子串 = 求next[k]

    int j=0,k=-1;
    next[0]=-1;
    while(j<t.length-1)
    {
       if(k == -1 || t[j] == t[k])
       {
          j++;k++;
          next[j] = k;
       }
       else k = next[k];//t[j] != t[k]
    }
    


    nextval数组(当两个字符相同时,就跳过)next优化

    int j=0,k=-1;
    nextval[0]=-1;
    while(j<t.length-1)
    {
       if(k == -1 || t[j] == t[k])
       {
          j++;k++;
          if(t[j]==t[k])
             nextval[j] = nextval[k];
          else
             nextval[j] = k;
       }
       else 
          k = nextval[k];
    }
    

    3.cin,getline,cin.getline输入字符串

    ①cin

    cin>>
    1.cin读取数据时,从第一个非空白字符(空格、制表符或换行符)开始阅读,当它读取到下一个空白字符时停止读取;
    2.cin会把换行符留在输入队列中;
    

    ②getline

    getline函数:
    getline(cin(正在读取的输入流), inputLine(接收输入字符串的 string 变量的名称))
    
    1.可读取整行,包括空格(若开头结尾有空格也读取),并将其存储在字符串对象中;
    2.读取换行符并且将换行符替换成'',但是会丢弃换行符(即输入队列不存在换行符);
    

    ③cin.getline

    cin.getlin函数:
    cin.getline(line,100)
    
    1.类似字符数组char line[100];
    2.cin.getline读取换行符并替换成'',并且不会主动丢弃换行符,会把它留在输入队列中
    

    1.2树

    (非线性结构:一对多

    基本术语:

    例:

    根结点:没有前驱(1)
    终端(叶子)结点:没有后继(4,8,9,6,10,11)
    双亲(父亲):即上层的那个结点(直接前驱)(4的父亲为2,7的父亲为3)
    孩子:直接连接的下层结点(直接后继)(2的孩子为4,5)
    兄弟:同双亲且同层结点(4的兄弟为5,6的兄弟为7)
    祖先:从根到该结点所经分支的所有结点(8的祖先为5,2,1)
    子孙:该结点任意子树(6,7,10,11都为3的子孙)
    度:分支数目(子树个数)
    

    1.3二叉树

    结点特点
    ①非空二叉树上叶节点数等于双分支结点数加1

    n0:度为0(叶子结点),n1:度为1(单分支),n2:度为2(双分支)
    总结点数=n0+n1+n2 = 分支数 + 1
    分支数=n1+2*n2
    所以:n0+n1+n2 = n1+2*n2+1
          n0 = n2+1
    

    ②二叉树的第 i 层上至多有2(i-1)个结点
    ③高度为h的二叉树至多有2h-1个结点,至少有h个结点

    1、特殊的二叉树

    (1)满二叉树

    所有分支结点都有双分结点,并且叶结点都集中在二叉树的最下一层
    高度为h的满二叉树有2h-1个结点

    (2)完全二叉树

    没有单独右分支结点
    结点数:2(h-1)-1< n <=2h-1
    满二叉树是完全二叉树的一个特例

    特点:

    ①度为1节点,只可能有一个,且该结点只有左孩子无右孩子
    ②除树根结点外,若一个结点的编号为i,则它的双亲结点的编号为i/2
    ③若编号为i的结点有左或右孩子结点,左孩子结点的编号为2i右孩子结点的编号为2i+1
    ④若i≤n/2(取下限),则编号为i的结点为分支结点,否则为叶结点
    

    2、二叉树存储结构

    ①顺序存储结构(数组存储:从上至下、从左到右)

    1、非根结点i的父结点序号为i/2(取下限)
    2、结点i的左孩子序号为2i
    3、结点i的右孩子序号为2i+1
    

    完全二叉树来说,其顺序存储是十分合适
    数据较大后,空间利用率较低
    数组缺点,查找、插入删除不方便

    ②链式存储结构

    结点包含:数据域和左、右指针域

    typedef struct TNode
    {
        ElemType data;
        struct node *lchild,*rchild;
    }TNode;
    

    若一棵树n个结点
      2n个指针域
      非空指针域有n-1个
      空指针域2n-(n-1) = n+1
    

    3、二叉树建立

    顺序存储建立二叉树

    (顺序存储结构转成二叉链)

    i从1开始

    if (i > len - 1) return NULL;
    if (str[i] == '#') return NULL;
    BT = new TNode;
    BT->data = str[i];
    BT->lchild = CreateBtree(str, 2 * i);//左子树
    BT->rchild = CreateBtree(str, 2 * i + 1);//右子树
    return BT;
    

    先序存储建立二叉树

    if (i > len - 1)return NULL;
    if (str[i] == '#')return NULL;
    BT = new TNode;
    BT->Data = str[i];
    BT->lchild = CreatBinTree(str, ++i);
    BT->rchild = CreatBinTree(str, ++i);
    return BT;
    

    先序中序建立二叉树

    if (count <= 0)
      return NULL;
    BT = new TNode;
    BT->data = *preLine;
    for (ch = inLine; ch < inLine + count; ch++)//查找先序序列当前元素在中序序列中的位置
    {
        if (*ch == *preLine)
    	 break;
    }
    k = ch - inLine;//左子树剩余结点个数
    BT->lchild = CreatTree(preLine + 1, inLine, k);
    BT->rchild = CreatTree(preLine + k + 1, ch + 1, count - k - 1);
    return BT;
    

    后序中序建立二叉树

    if (count <= 0)
      return NULL;
    BT = new TNode;
    BT->data = *(postLine + count - 1);
    for (ch = inLine; ch < inLine + count; ch++)//查找先序序列当前元素在中序序列中的位置
    {
        if (*ch == *preLine)
    	 break;
    }
    k = ch - inLine;//左子树剩余结点个数
    BT->lchild = CreatTree(postLine, inLine, k);
    BT->rchild = CreatTree(postLine + k, num + 1, count - k - 1);
    return BT;
    

    4、二叉树遍历

    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);
    	 cout<<bt->data; 	//访问根结点
    	 InOrder(bt->rchild);
           }
    }
    

    ③后序遍历

    void PostOrder(BTree bt) 
    {      if (bt!=NULL)  
            {      PostOrder(bt->lchild);
    	 PostOrder(bt->rchild);
    	 printf("%c ",bt->data); 	//访问根结点
           }
    }
    
    

    1.非递归遍历法

    ①层次遍历(利用,先进先出)

    void PrintTree(BTree BT)//层次遍历二叉树
    {
    	BTree ptr;//遍历二叉树
    	queue<BTree>qu;
    	qu.push(BT);//根结点进栈
    	while (!qu.empty())//若队列不为空
    	{
    	   ptr = qu.front();//第一个元素出栈
    	   qu.pop();
    	   cout << ptr->data;
    	   if (ptr->lchild != NULL)//若出栈元素有左右子结点,进栈
    		 qu.push(ptr->lchild);
    	   if (ptr->rchild != NULL)
    		 qu.push(ptr->rchild);
    	}
    }
    

    5、哈夫曼树

    (具有最小带权路径长度的二叉树)

    二叉树的带权路径长度wpl:(若有n个带权值的叶子结点)从根结点到各个叶子结点的路径长度与相应结点权值的乘积的和
    原则:
    权值越大的叶结点越靠近根结点;权值越小的叶结点越远离根结点

    哈夫曼树总节点数

    结点总数n = n0+n1+n2
    因为,哈夫曼树结点度为0或2
     所以,n=n0+n2
    因为,n0=n2+1
     所以,总结点n = 2n0-1
    

    哈夫曼树建树

    结构体定义
    typedef struct HTNode
    {	char data; //结点数据
    	float weight;	//权值
    	int parent;//双亲结点
    	int lchild;//左孩子结点
    	int rchild;//右孩子结点
    } HTNode;
    

    for i=0 to 2*n-1 do 
        初始化每个结点的值ht[i].parent=ht[i].lchild=ht[i].rchild=-1;
    end for
    //构造哈夫曼树
    给定一组权值W
    1、在W中选出权值最小ht[lnode]和次小ht[rnode]的两棵二叉树作为左、右子树构造一棵的二叉树
       (新二叉树根结点的权值为其左、右子树根结点权值之和:ht[i].weight= ht[lnode].weight+ht[rnode].weight)
    2、新二叉树根结点的权值放入W中,重复操作直到W为空 
    for (i = n; i < 2 * n - 1; i++)
    {
       smallest = SecSmallest = MAXNUM;
       lnode = rnode = -1;
       for (j = 0; j < i; j++)
       {
         if (HTNodes[j].parent == -1)
         {
            if (HTNodes[j].weight < smallest)//遍历寻找最小权值
            {
    	   SecSmallest = smallest;
    	   rnode = lnode;
    	   smallest = HTNodes[j].weight;
    	   lnode = j;
    	}
    	else if (HTNodes[j].weight < SecSmallest)//遍历寻找次小权值
    	{
    	   SecSmallest = HTNodes[j].weight;
    	   rnode = j;
    	}
         }
        }
    HTNodes[lnode].parent = i;
    HTNodes[rnode].parent = i;
    HTNodes[i].weight = HTNodes[lnode].weight + HTNodes[rnode].weight;//新二叉树根结点的权值
    HTNodes[i].lchild = lnode;
    HTNodes[i].rchild = rnode;
    }
    

    应用:哈夫曼编码

    左数为0,右树为1
    

    6、并查集

    (查找某个元素所属的集合、合并2个元素各自专属的集合等)
    应用:朋友圈

    初始化!!!

    for (i = 1; i <= stuNum; i++)
    {
       fd[i].data = i;	//集合元素
       fd[i].rank = 0;
       fd[i].parent = i;	//双亲初始化指向自已
    }
    

    查找所在集合编号(根节点编号)

    int FIND_SET(UFSTree& fd, int x)
    {
       if (x != fd[x].parent)//双亲不是自已
    	 return(FIND_SET(fd, fd[x].parent));//递归在双亲中找x
       else
    	 return(x);//双亲是自已,返回x(找到根节点)
    }
    

    集合合并

    x = FIND_SET(fd,x);	        //查找x所在朋友圈的编号
    y = FIND_SET(fd,y);	        //查找y所在朋友圈的编号
    if (x == y)
    {
    	return;
    }
    if (fd[x].rank > fd[y].rank)	        //y结点的秩小于x结点的秩
    {
    	fd[y].parent = x;		        //x作为y的双亲结点
    	fd[x].num = fd[y].num + fd[x].num;              //两朋友圈人数相加
    }
    else			        
    {
    	fd[x].parent = y;		        //y作为x的双亲结点
    	if (fd[x].rank == fd[y].rank)      
    		fd[y].rank++;	        //y结点的秩增1
    	fd[y].num = fd[y].num + fd[x].num;          //两朋友圈人数相加
    }
    

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

    1、关于串:kmp算法还是有点绕,移动规律和数组有点对不上
    2、关于树:大致对于建树遍历树和树的应用(哈夫曼树等等)都基本了解,但是中序和前序、后序建树的字符查找还没有很熟练掌握
    注意点:
    ①是顺序存储结构建树(左孩子lchild:2i,右孩子rchild:2i+1),还是采用先序建树(i++)
    ②递归时,正确设置返回条件,避免返回到一半中断或溢出
    (经常和栈,队列结合使用,例如层次遍历)


    2.阅读代码

    2.1 题目及解题代码

    二叉树的锯齿形层次遍历


    2.1.1 该题的设计思路

    利用双端队列deque
    先进先出,则将新元素添加到队列尾部,后插入的元素就可以排在后面;(偶数层)
    先进后出,则将新元素添加到队列首部,后插入的元素就可以排在前面;(奇数层)
    

    2.1.2 该题的伪代码

    vector<vector<int>> res定义res
    定义队列q辅助层次遍历树
    从level=0开始
    while(层次遍历队列不为空)
        deque<int> temp定义双端队列存储每层的节点值
        for  i = 0 to 当前队列元素个数 do
             出队对头元素q.front()
             若为偶数层,输出顺序从左往右(从后添加push_back)
             若为奇数层,输出顺序从右往左(从前添加push_front)
             若对头元素有左右孩子,入队列q
        end for
    将双端队列中的元素顺序添加到res中
    res.push_back(vector<int>(temp.begin(), temp.end()))
    

    2.1.3 运行结果

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

    1、利用双端队列:队列的两端分别称为前端和后端,两端都可以入队和出队(仍是线性结构)
    2、时间复杂度较小,利用双端队列运行速度较快
    3、c++vector的用法不是很了解,为了这个题目还特地去看了stl的vector用法(定义,存储方式和输出方式)

    vector<vector<int>> res(相当于动态二维数组)
    for (i = 0; i < res.size(); i++)
    {
       cout << "[ ";
       for (j = 0; j < res[i].size(); j++)
       {
          cout << res[i][j]<<"," ;
       }
       cout <<" ]" <<endl;
    }
    

    2.2 题目及解题代码

    二叉树剪枝



    2.2.1 该题的设计思路

    先遍历左数,再右树,最后再根,所以后序遍历才能完全剪枝

    2.2.2 该题的伪代码

    后序遍历递归
    if (!root) return root;
    后序遍历树
      root->left = pruneTree(root->left);
      root->right = pruneTree(root->right);
      若该结点为叶子结点且val为0,剪枝(释放结点且return NULL)
    return root;
    

    2.2.3 运行结果

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

    1、递归算法,代码更简洁,可读性强
    2、要考虑是哪种顺序的遍历,例如先序遍历就可能会导致不完全剪枝
    3、注意:并不是只要剪掉所有值为0的叶子结点,叶子结点剪完之后,非叶子结点也可能变成叶子结点
    4、递归算法其实算是一个难点,有时候跟着跟着就会乱掉,画图还是比较有用的,借图会更好理解


    2.3 题目及解题代码

    二叉搜索树中的搜索

    方法一(迭代)

    方法二(递归)

    2.3.1 该题的设计思路

    2.3.2 该题的伪代码

    二叉搜索树每个节点都有的特点:
    大于左子树上任意一个节点的值;
    小于右子树上任意一个节点的值;
    迭代法

    struct TreeNode* searchBST(struct TreeNode* root, int val)
    {
      while(根节点不空 root != null )
        如果根节点是目的节点
               return root
        如果 val < root.val
               查找根节点的左子树 root = root.left
        如果 val > root.val
               查找根节点的右子树 root = root.right
        return 0;
    }
    
    

    递归

    struct TreeNode* searchBST(struct TreeNode* root, int val)
    {
        如果根节点为空或根节点是目的结点 val == root.val
          返回root
        如果 val < root.val,进入根节点的左子树查找
              searchBST(root.left, val)
        如果 val > root.val,进入根节点的右子树查找
              searchBST(root.right, val)
        返回root
    }
    
    

    2.3.3 运行结果

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

    1、利用迭代法降低了空间复杂度
    2、可读性较强
    3、遍历特殊二叉树不一定要用递归法,递归所占空间较大,但是胜在代码比较简洁


    2.4 题目及解题代码

    二叉树最大宽度


    2.4.1 该题的设计思路

    每个节点一个 position 值
    如果向左子树,那么 position -> position * 2
    如果向右子树,那么 position -> positon * 2 + 1
    遍历每个节点时,记录节点的 position 信息
    第一个遇到的节点是最左边的节点,最后一个到达的节点是最右边的节点。
    

    2.4.2 该题的伪代码

    int widthOfBinaryTree(TreeNode* root) 
    {
       定义队列存结点及结点位置
       queue<pair<TreeNode*, unsigned long long>> q;
       入队根结点q.push({root, 1});
       while 队列不为空
           求出最大宽度ans = max(int(q.back().second - q.front().second + 1), ans);
           队列中保存每个节点的指针以及pos值
           若有左右孩子,入队
             if (node->left) q.push({node->left , 2 * pos});
             if (node->right) q.push({node->right, 2 * pos + 1});
           每层的宽度为队尾pos-队首pos+1
       return ans;
    }
    

    2.4.3 运行结果

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

    1、利用队列存储两个变量:结点和结点位置,记录位置后更方便计算每层宽度(队尾pos-队首pos+1)
    2、max函数方便比较大小
    3、解题思路很新颖,这种队列的存储方法也是看了这题后才知道

  • 相关阅读:
    Zookeeper java API
    Zookeeper 安装与配置
    Zookeeper 架构
    HBase 优化
    HBase 与 MapReduce 整合
    Hbase protobuf Java API
    HBase数据库设计
    Fork/Join并发处理框架
    并发容器Map
    Redis缓存穿透、缓存击穿和雪崩
  • 原文地址:https://www.cnblogs.com/sixiDL000/p/12606672.html
Copyright © 2020-2023  润新知