• DS博客作业03--树


    0.PTA得分截图

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

    1.1 二叉树结构

    1.1.1 二叉树的2种存储结构

    顺序存储
    在数组中依次存储二叉树中的元素。其结点的父子关系如下:
    1.非根结点序号为i(i>1),则其父结点的序号为[i/2],[]的意思是取整数。
    2.序号为i的左孩子结点的序号为2i(若存在左孩子),右孩子的序号为2i+1(若存在右孩子)。
    这种存储方式的缺点是:如果存储的树不是完全二叉树,则要造成空间的浪费,因为数组中有的地方会没有元素。
    链式存储
    对于一般二叉树比较节省空间
    访问一个结点的孩子很方便,访问双亲不方便
    增加指向双亲的指针域parent可以解决
    一个结点包括三个部分:存储的数据自身、指向左孩子的指针和指向右孩子的指针。如图所示:

    1.1.2 二叉树的构造

    顺序结构创建二叉树

    将顺序存储改为链式储存,利用链递归建树

    层次建树(利用队列)

    一层一层的建立,则需要储存每层数据,就需要队列与之搭配完成建树

    void creatbintree(BTree& bt, string s)
    {
    	int i = 1;
    	BTree p;
    	bt = new BTNode;
    	if (s[i] == '#') {//如果第一个节点为空,就直接返回空树
    		bt = NULL;
    		return;
    	}
    	else {//创建根节点
    		bt->data = s[i];
    		bt->lchild = bt->rchild = NULL;
    		q.push(bt);  //根节点入队
    	}
    	while (!q.empty()) {   //当队列不为空
    		p = q.front();
    		q.pop();
    		i++;
    		p->lchild =new BTNode;//创建左孩子
    		if (s[i] == '#')  p->lchild = NULL; //左孩子为空	
    		else {
    			p->lchild->data = s[i];
    			p->lchild->lchild = p->lchild->rchild = NULL;
    			q.push(p->lchild);  //左孩子入队
    		}
    		p->rchild = new BTNode;//创建右孩子
    		i++;
    		if (s[i] == '#')  p->rchild = NULL; //右孩子为空	
    		else {
    			p->rchild->data = s[i];
    			p->rchild->lchild = p->rchild->rchild = NULL;
    			q.push(p->rchild);   //右孩子入队
    		}
    	}
    }
    

    先序遍历递归建树

    先建立根节点,再先序建立左子树,最后先序建立右子树。

    BinTree CreatBinTree(string str, int& i)
    {
        BinTree bt;
        if (i > str.size() - 1) return NULL;
        if (str[i] == '#') return NULL;
        bt = new TNode;
        bt->Data = str[i];
        bt->Left = CreatBinTree(str, ++i);
        bt->Right = CreatBinTree(str, ++i);
        return bt;
    }
    

    1.1.3 二叉树的遍历

    首先来看前序遍历,所谓的前序遍历就是先访问根节点,再访问左节点,最后访问右节点,

    如上图所示,前序遍历结果为:ABDFECGHI
    代码:

    void PreOrder(BTree bt)
    {
    	if (bt != NULL) {
    		cout << " " << bt->data; //访问根结点
    		PreOrder(bt->lchild);//遍历左支
    		PreOrder(bt->rchild);//遍历右支
    	}
    }
    

    再者就是中序遍历,所谓的中序遍历就是先访问左节点,再访问根节点,最后访问右节点,

    如上图所示,中序遍历结果为:DBEFAGHCI
    实现代码如下:

    void MidOrder(BTree bt)
    {
        if (bt != NULL){
            MidOrder(bt->lchild);
            cout << bt->data<<" ";
            MidOrder(bt->rchild);
        }
    }
    

    最后就是后序遍历,所谓的后序遍历就是先访问左节点,再访问右节点,最后访问根节点。

    如上图所示,后序遍历结果为:DEFBHGICA
    实现代码如下:

    void PostOrder(BTree bt)
    {
    	if (bt != NULL) {
    		PostOrder(bt->lchild);//左
    		PostOrder(bt->rchild);//右
    		cout << " " << bt->data;//访问根结点
    	}
    }
    

    层次遍历(一层一层,从左向右)
    层次遍历借助了队列,将该层的元素从左到右储存,再输出

    void Level(BTree BT)
    {
        BTree b;
        q.push(BT);
        while (!q.empty()) {
            b=q.front();
            q.pop();
            cout << " " << b->Data;
            if (b->lchild)q.push(b->lchild);
            if (b->rchild)q.push(b->rchild);
        }
    }
    

    1.1.4 线索二叉树


    为了区分左、右指针和线索,需要对每个结点增加两个标志位ltag和rtag,定义如下,

    中序线索二叉树

    构造线索二叉树,即是非递归遍历一遍二叉树,这就需要一个栈结构,用来保存遍历过程中需要回溯的结点的指针。下面算法中,p指向正在访问的结点,pre指向它的中序前驱,即上一次刚访问过的结点,这里的“访问”是指把当前结点的左指针和中序前驱结点的右指针改为左线索和右线索。中序线索二叉树如下,

    构造中序线索二叉树的意义在于,可以方便地从中找到指定结点在中序序列中的前驱和后继,而不必周游二叉树。而且,非递归地遍历二叉树时,不需要借用栈

    #include <iostream>
    #include <stack>
    #include <cstring>
    #include <cstdio>
    #define MAXN 100
    #define DataType char
     
    using namespace std;
     
    typedef struct ThrTreeNode* pThrTreeNode;
    typedef struct ThrTreeNode* pThrTree;
    struct ThrTreeNode{
        DataType info;
        pThrTreeNode llink,rlink;
        int ltag,rtag;
    };
     
    void Thread(pThrTree t)
    {
        stack<pThrTree> s;
        pThrTree p = t, pre = NULL;
        if(t == NULL)   return;
        do{
            while(p){                   //遇到结点压入栈,然后进入其左子树
                s.push(p);  p=p->llink;
            }
            p=s.top();  s.pop();
            if(pre){
                if(pre->rlink == NULL){ //修改前驱结点的右指针
                    pre->rlink = p;
                    pre->rtag = 1;
                }
                if(p->llink == NULL){   //修改该节点的左指针
                    p->llink = pre;
                    p->ltag = 1;
                }
            }
            pre = p; p=p->rlink;
        }while(!s.empty() || p);
    }
     
    void ThreadInOrder(pThrTree t)            //按中序遍历周游中序线索二叉树
    {
        pThrTree p =t;
        if(t == NULL)   return;
        while(p->llink !=NULL && p->ltag == 0)  p=p->llink;
                                        //顺左子树一直向下
        while(p != NULL){
            cout<<p->info;              //访问
            if(p->rlink !=NULL && p->rtag == 0){
                                        //右子树不是线索时
                p=p->rlink;
                while(p->llink !=NULL && p->ltag == 0)  p=p->llink;
                                        //顺右子树的左子树一直向下
            }
            else    p=p->rlink;         //顺线索向下
        }
    }
     
    pThrTree CreateBinTree(char seq[],int &i,int k){
                                        //创建普通二叉树
       if(i>k||seq[i]=='#')
           return NULL;
       pThrTreeNode p=new struct ThrTreeNode;
       if(p!=NULL){
           p->info=seq[i];
           p->ltag=p->rtag=0;           //初始化标志量
           i++;
           p->llink=CreateBinTree(seq,i,k);
           i++;
           p->rlink=CreateBinTree(seq,i,k);
           return p;
        }
        return NULL;
    }
    int main()
    {
        char seq[MAXN];
        scanf("%s",seq);
        int k=0;
        pThrTree t = CreateBinTree(seq, k, strlen(seq)-1);
        Thread(t);
        ThreadInOrder(t);
        return 0;
    }
    

    1.1.5 二叉树的应用--表达式树

    void InitExpTree(BTree &T,string str)
    {
    	遍历字符串str{
    		如果当前字符串为数字{
    			T->data=当前数字 
    			入栈Q1 
    		 } 
    		 如果当前字符串为运算符{
    		 	与Q2栈顶比较优先级
    			 优先级低入栈Q2
    			 优先级相同Q2出栈
    			 优先级高 T->data=Q2栈顶元素 
    		 } 
    	} 
    }
    double EvaluateExTree(BTree T)
    {
    	double x,y;
    	x=递归调用左子树的值
    	y=递归调用右子树的值
    	根据data的运算符分别运算并返回结果 
    }
    
    

    1.2 多叉树结构

    1.2.1 多叉树结构

    1.双亲存储结构
    父亲容易找到,孩子不容易找到

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

    2.孩子链存储结构

    空指针太多,造成空间浪费
    typedef struct node
    {
       ElemType data;   
       struct tnode *sons[MaxSons];  
    }TSonNode;
    

    3.孩子兄弟链存储结构

    结构体定义:

    typedef struct tnode
    {
       ElemType data;    //结点的值
       struct tnode *son;   //指向兄弟
       struct tnode *brother;   //指向孩子结点
    }TSBNode;
    

    1.2.2 多叉树遍历

    // 层次优先遍历多叉树结构(利用队列实现),按层,深度输出节点名称。 如果逆向输出,需要借助栈
    void travel(OriTree * tree)
    {
    	if (tree == nullptr)
    		return;
     
    	QQueue<OriTree*> qTree;
    	qTree.append(tree);
    	
    	OriTree* p = nullptr;
    	while (!qTree.isEmpty())
    	{
    		p = qTree.front();
    		qTree.pop_front();
    		if (p != nullptr)
    		{
    			for (int i = 0; i < p->childrenList.count(); i++)
    			{
    				qDebug() << p->childrenList.values().at(i)->nameValue;
    				qTree.append(p->childrenList.values().at(i));
    			}
    		}
    	}
    }
    

    1.3 哈夫曼树

    1.3.1 哈夫曼树定义

    给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

    1.3.2 哈夫曼树的结构体

    typedef struct
    {
        char data;//结点值
        int weight;//权重
        int parent;//双亲结点
        int lchild;//左孩子结点
        int rchild;//右孩子结点
    }HTNode, * HTree;
    

    1.3.3 哈夫曼树构建及哈夫曼编码

    由给定结点构造哈夫曼树
    8个结点的权值大小如下:


    选出的两个数字都不是已经构造好的二叉树里面的结点,所以要另外开一棵二叉树;或者说,如果两个数的和正好是下一步的两个最小数的其中的一个,那么这个树直接往上生长就可以了,如果这两个数的和比较大,不是下一步的两个最小数的其中一个,那么就并列生长。

    void CreatHFT(HTNode hf[], int n)//建立哈夫曼树
    {
        int min1, min2, lnode, rnode;
        for (int i = 0; i < 2 * n - 1; i++)//初始化
            hf[i].parent = hf[i].lchild = hf[i].rchild = -1;
        for (int i = n; i < 2 * n - 1; i++) {
            min1 = min2 = MAX;//找到最小和次小
            lnode = rnode = -1;//最小和次小对应下标
            for (int j = 0; j <= i - 1; j++) {
                if (hf[j].parent == -1) {
                    if (hf[j].weight < min1) {
                        min2 = min1;
                        min1 = hf[j].weight;
                        lnode = j;
                    }
                    else if (hf[j].weight < min2) {
                        min2 = hf[j].weight;
                        rnode = j;
                    }
                }
                hf[lnode].parent = hf[rnode].parent = i;
                hf[i].weight = hf[lnode].weight + hf[rnode].weight;
                hf[i].lchild = lnode; hf[i].rchild = rnode;
            }
        }
    }
    

    哈夫曼编码

    首先我们来看这棵构造好的哈夫曼树:(经过左边路径为0,经过右边路径为1)
    则可直接写出编码,例如:
    A:11 B:001 C:011 D E:0000 F:0001 G:0100 H:0101 I:1000 J:1001

    因此我们可以借助一个叫做优先队列的数据结构,而优先队列的实现往往是借助于二叉堆的结构实现,在这里我们要实现的是小根堆的数据结构。一开始的时候,我们可以将所有的节点一个一个的压入队列中,每次有节点入队,队列都会进行自调整,使其保持一个小根堆的状态。当所有的节点全部入队之后,这时候我们根据以上推导出来的结论,每次取两个权值最小的节点,将其值计算之后,然后再将两个节点权值之和的节点压入队列中,直到队列中只剩下一个节点(即根节点),跳出循环体,输出最后的答案。即整棵哈夫曼树的带权路径WPL。

    1.4 并查集

    并查集是用来处理“有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中”的问题的。通常我们使用的数据结构空间复杂度还可以但时间复杂度远远超出我们所需求的。而并查集这种树状数据结构正好可以处理这种一些不相交集合的合并及查询问题,大大减少了我们的时间复杂度。
    简单的来说就是一个把自己的根列出来,然后再查找自己的根的在哪的问题。

    并查集的结构体、查找、合并操作

    初始化father数组,但我们通常还会初始化另外一个辅助的数据结构 height[N], 来记录每个结点的深度,以使得我们的并查集的效率不会太差,不会生成一个非常长高的单链。
    初始化

    #define N 1001
    int father[N]; //记录父节点
    int height[N]; //记录每个节点的高度
    
    void Initial(int n)
    {
        for (int i = 0; i <= n; i++)
        {
            father[i] = i; // 初始化,父节点是自己
            height[i] = 0; // 高度为0
        }
    }
    

    查找

    由于同一个集合只存在一个根结点,所以查找操作就是对给定的结点寻找其根结点的过程。
    // 递归
    int Find(int x)
    {
        if (x != father[x])
        {
            x = Find(father[x]);
        }
        return x;
    }
    
    // 非递归
    int Find(int x)
    {
        while (x != father[x])
        {
            x = father[x];
        }
        return x;
    }
    

    合并
    把两个集合合并成一个,思路:

    首先判断两个元素是否是同一个集合,就是看二者是否有相同的根结点
    如果不是同一个集合,那么把一个集合的根结点的父亲指向另一个集合的根结点
    我们一般会将比较矮的集合(树)指向比较高的树
    void Union(int x, int y)
    {
        x = Find(x);
        y = Find(y);
        if (x != y)
        {
            if (height[x] < height[y])
            { // 矮树添加到高树上
                father[x] = y;
            }
            else if (height[x] > height[y])
            {
                father[y] = x;
            }
            else
            {
                father[y] = x;
                height[x]++;
            }
        }
    }
    

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

    1.树结构是一对多的,相对于之前的链表、栈、队列有更多种遍历方式
    2.用到了递归,运用递归可以减少很多代码量,但是如何设计递归也是比较难的
    3.学习一些算法优化的方法,需要多了解一些STL库中的东西,sort,堆排等等

    2.PTA实验作业(4分)

    2.1 二叉树

    7-2 jmu-ds-二叉树叶子结点带权路径长度和


    2.1.1 解题思路及伪代码

    解题思路
    先建立二叉树,再进行遍历找到末端结点,计算左右结点的wpl值
    伪代码

    void Wpl(BTree T, int h, int& wpl)
        if p为空
            return ;
        if 不为空
            if (p->lchild == NULL && p->rchild == NULL)//找到末端结点
                wpl = wpl + (p->data - '0') * h;//计算wpl
            Wpl(p->lchild, h + 1, wpl);//计算左子树
            Wpl(p->rchild, h + 1, wpl);//计算右子树
    

    2.1.2 总结解题所用的知识点

    1.建立二叉树
    2.对函数的调用

    2.2 目录树


    2.2.1 解题思路及伪代码

    解题思路:

    2.2.2 总结解题所用的知识点

    1.结构体中增加isfile判断一个结点是目录还是文件
    3.运用孩子兄弟存储结构建树
    4.注意根目录与子目录的插入位置关系
    4.对孩子兄弟链的使用

    3.阅读代码(0--1分)

    3.1 题目及解题代码

    代码

    3.2 该题的设计思路及伪代码

    思路

    对各种情况进行判断,重复调用函数

    伪代码

    if 都为空
       return ture
    if 一空一不空
       return false
    if p,q的左子树不为都为空
          if p,q左子树不同
              return false
          else 
              if p,q右子树相同
                  return ture
              else
                  return false
    

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

    优点:思路清晰,代码易懂
    难点:判断情况复杂,可以进行优化

  • 相关阅读:
    [NOI2009]管道取珠
    Rebalance再均衡
    生产者分区写入策略
    Kafka事务
    幂等性
    消费者组
    Kafka中的重要概念
    偏移量offset
    服务注册和发现的意思,Spring cloud如何实现?
    负载平衡的意义
  • 原文地址:https://www.cnblogs.com/letmee/p/14729721.html
Copyright © 2020-2023  润新知