• 20182301 2019-2020-1 《数据结构与面向对象程序设计》第十周学习总结


    20182301 2019-2020-1 《数据结构与面向对象程序设计》第十周学习总结

    教材学习内容总结

    图的结构构成
    • 顶点(vertex):图中的数据元素,如图一

    • 边(edge):图中连接这些顶点的线,如图一

    • G=(V,E) 或者 G=(V(G),E(G))

      • 其中 V(G)表示图结构所有顶点的集合,顶点可以用不同的数字或者字母来表示。E(G)是图结构中所有边的集合,每条边由所连接的两个顶点来表示。
      • 图结构中顶点集合V(G)不能为空,必须包含一个顶点,而图结构边集合可以为空,表示没有边。
    图的基本概念
    • 无向图

    • 如果一个图结构中,所有的边都没有方向性,那么这种图便称为无向图。典型的无向图,如图二所示。由于无向图中的边没有方向性,这样我们在表示边的时候对两个顶点的顺序没有要求。例如顶点VI和顶点V5之间的边,可以表示为(V2, V6),也可以表示为(V6,V2)。

    • 有向图

    • 一个图结构中,边是有方向性的,那么这种图就称为有向图,如图三所示。由于图的边有方向性,我们在表示边的时候对两个顶点的顺序就有要求。我们采用尖括号表示有向边,例如<V2,V6>表示从顶点V2到顶点V6,而<V6,V2>表示顶点V6到顶点V2。

    • 顶点的度

    • 连接顶点的边的数量称为该顶点的度。顶点的度在有向图和无向图中具有不同的表示。对于无向图,一个顶点V的度比较简单,其是连接该顶点的边的数量,记为D(V)。

    • 对于有向图要稍复杂些,根据连接顶点V的边的方向性,一个顶点的度有入度和出度之分。

      • 入度是以该顶点为端点的入边数量, 记为ID(V)。
      • 出度是以该顶点为端点的出边数量, 记为OD(V)。
    • 邻接矩阵

    • 邻接顶点是指图结构中一条边的两个顶点。邻接顶点在有向图和无向图中具有不同的表示。对于无向图,邻接顶点比较简单。

    • 对于有向图要稍复杂些,根据连接顶点V的边的方向性,两个顶点分别称为起始顶点(起点或始点)和结束顶点(终点)。有向图的邻接顶点分为两类:

      • 入边邻接顶点:连接该顶点的边中的起始顶点。例如,对于组成<V2,V6>这条边的两个顶点,V2是V6的入边邻接顶点。
      • 出边邻接顶点:连接该顶点的边中的结束顶点。例如,对于组成<V2,V6>这条边的两个顶点,V6是V2的出边邻接顶点。
    • 无向完全图

    • 如果在一个无向图中, 每两个顶点之间都存在条边,那么这种图结构称为无向完全图。典型的无向完全图,如图四所示。

    • 理论上可以证明,对于一个包含M个顶点的无向完全图,其总边数为M(M-1)/2。比如图四总边数就是5(5-1)/ 2 = 10。

    • 有向完全图

    • 如果在一个有向图中,每两个顶点之间都存在方向相反的两条边,那么这种图结构称为有向完全图。典型的有向完全图,如图五所示。

    • 理论上可以证明,对于一个包含N的顶点的有向完全图,其总的边数为N(N-1)。这是无向完全图的两倍,这个也很好理解,因为每两个顶点之间需要两条边。

    • 有向无环图(DAG图)

    • 如果一个有向图无法从某个顶点出发经过若干条边回到该点,则这个图是一个有向无环图。

    • 有向无环图可以利用在区块链技术中。

    • 无权图和有权图

    • 这里的权可以理解成一个数值,就是说节点与节点之间这个边是否有一个数值与它对应,对于无权图来说这个边不需要具体的值。对于有权图节点与节点之间的关系可能需要某个值来表示,比如这个数值能代表两个顶点间的距离,或者从一个顶点到另一个顶点的时间,所以这时候这个边的值就是代表着两个节点之间的关系,这种图被称为有权图;

    • 图的连通性

    • 图的每个节点不一定每个节点都会被边连接起来,所以这就涉及到图的连通性,如下图:

    • 可以发现上面这个图不是完全连通的。

    • 简单图 ( Simple Graph)

    • 对于节点与节点之间存在两种边,这两种边相对比较特殊

      • 自环边(self-loop):节点自身的边,自己指向自己。
      • 平行边(parallel-edges):两个节点之间存在多个边相连接。
    • 这两种边都是有意义的,比如从A城市到B城市可能不仅仅有一条路,比如有三条路,这样平行边就可以用到这种情况。不过这两种边在算法设计上会加大实现的难度。而简单图就是不考虑这两种边。

    常见图的算法
    • 广度优先遍历

      • 遍历方法:从一个顶点开始,辐射状地优先遍历其周围较广的区域
      • 实现方法:需要一个队列来保存遍历过的定点顺序,以便按出队的顺序再去访问这些顶点的邻接顶点
      • 顶部伪代码:
        • 访问初始结点v并标记结点v为已访问。
        • 结点v入队列
        • 当队列非空时,继续执行,否则算法结束。
        • 出队列,取得队头结点u。
        • 查找结点u的第一个邻接结点w。
        • 若结点u的邻接结点w不存在,则转到步骤3;否则循环执行以下三个步骤:
          1. 若结点w尚未被访问,则访问结点w并标记为已访问。
          2. 结点w入队列
          3. 查找结点u的继w邻接结点后的下一个邻接结点w,转到步骤6。
      • 图片解释:其广度优先算法的遍历顺序为:1->2->3->4->5->6->7->8
    • 深度优先遍历

      • 遍历方法:从一个顶点开始,沿边去探寻每一个顶点。(通俗一点:一条道走到黑!)
      • 实现方法:通过栈来保存遍历过的定点顺序,遇到一个顶点,只是先获得它,并不出栈(因为要多次利用它),然后把它的第一个未被访问的节点入队列。如果没有相邻的未被访问的顶点,才把这个顶点出栈。
      • 顶部伪代码:
        • 访问初始结点v,并标记结点v为已访问。
        • 查找结点v的第一个邻接结点w。
        • 若w存在,则继续执行4,否则算法结束。
        • 若w未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行步骤123)。
        • 查找结点v的w邻接结点的下一个邻接结点,转到步骤3。
      • 图片解释:其深度优先遍历顺序为1->2->4->8->5->3->6->7
    最小生成树
    • 在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。
    • Kruskal算法
    • 此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
      • 把图中的所有边按代价从小到大排序;
      • 把图中的n个顶点看成独立的n棵树组成的森林;
      • 按权值从小到大选择边,所选的边连接的两个顶点ui,viui,vi,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。
      • 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
    • Prim算法
    • 此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
      • 图的所有顶点集合为VV;初始令集合u={s},v=V−uu={s},v=V−u;
      • 在两个集合u,vu,v能够组成的边中,选择一条代价最小的边(u0,v0)(u0,v0),加入到最小生成树中,并把v0v0并入到集合u中。
      • 重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。
    最短路径问题
    • 无权图的单源最短路径

    • 首先,我们将起点的路径长设为0,其他顶点路径长设为负数(也可以是其他不可能的值,图例中用?表示),下例以v1作为起点

    • 接着我们将起点所指向的顶点的路径长设为1,可以肯定的是,只有被路径长为0的起点所指向的顶点的路径长为1,本例中即v3和v4:

    • 接下来,我们将路径长为1的顶点(v3和v4)所指向的顶点的路径长设为2,同样可以肯定,只有被路径长为1的顶点所指向的顶点的路径长为2。不过此时会遇到一个问题:v3是v4所指向的顶点,但v3的路径长显然不应该被设为2。所以我们需要对已知路径长的顶点设一个“已知”标记,已知的顶点不再更改其路径长,具体做法在给出代码时将写明。本例中,路径长要被设为2的顶点是v2、v5、v6

    • 规律是:将路径长为i的顶点所指向的未知顶点的路径长设为i+1,i从0开始,结束条件即:当前路径长为i的顶点没有指向其它顶点,或所指向的顶点均为已知。

    • 需要注意的是结束条件的说法,我们并没有要求所有顶点都变为已知,因为确定某顶点为起点后,是有可能存在某个顶点无法由起点出发然后到达的,比如我们的例子中的v0,不存在从v1到v0的路径。

    • 将最短路径的计算结果存于一个线性表中,其结构如下:

    • 其中“一行”为线性表中的一个元素,每一行的四个单元格就是一个元素中的四个域:顶点、是否已知、与起点最短路径长、最短路径中自身的前一个顶点。

    • 那么之前计算最短路径的过程用这个表来表示的话,就是下面这样:

    • 简单伪代码

    //无权最短路径计算,图存于邻接表graph,结果存入pathTable,起点即start
    void unweightedPath(Node* graph,struct pathNode* pathTable,size_t  start)
    {
        pathTable[start].known=true;
        pathTable[start].distance=0; //若pathTable[x].distance为0,则其preV是无用的,我们不予理睬
    
        //初始化pathTable中的其他元素
    
        //curDis即当前距离,我们要做的是令distance==curDis的顶点所指的未知顶点的distance=curDis+1
        for(int curDis=0;curDis<numVertex;++curDis)
        {
            for(int i=0;i<numVertex;++i)
            {
                if(!pathTable[i].known&&pathTable[i].distance==curDis)
                {
                    pathTable[i].known=true;
                    //遍历pathTable[i]所指向的顶点X
                    {
                        if(!pathTable[X].known)
                        {
                            pathTable[X].preV=i;
                            pathTable[X].distance=curDis+1;
                        }
                    }
                }
            }
        }
    }
    
    • 令已知顶点所指未知顶点的distance=curDis+weight

    • 解决的思路是:我们罗列出所有已知顶点指向的所有未知顶点,看这些未知顶点中谁的distance被修改后会是最小的,最小的那个我们就修改其distance,并认为它已知。

    • 首先是正常的初始化(我们将边的权重也标识出来),假设起点为v0:

    • 接着我们罗列出所有已知顶点(只有v0)指向的所有未知顶点:v1、v2、v3。然后发现若修改它们的distance,则v1.distance=v0.distance+1=1,v2.distance=v0.distance+3=3,v3.distance=v0.distance+5=5。显然v1被修改后的distance是未知顶点中最小的,所以我们只修改v1的distance,并将v1设为已知,v2、v3不动:

    • 接着我们继续罗列出所有已知顶点(v0、v1)指向的所有未知顶点:v2、v3、v4。然后发现若修改它们的distance,则v2.distance=v0.distance+3=3,v4.distance=v1.distance+1=2,v3.distance=v1.distance+1=2(虽然v0也指向v3,但是通过v0到v3的路径长大于从v1到v3,所以v3的distance取其小者),其中v3和v4的新distance并列最小,我们任选其一比如v4,然后只修改v4的distance,并将v4设为已知,其它不动:

    • 继续,我们罗列出所有已知顶点(v0、v1、v4)指向的所有未知顶点:v2、v3、v6,发现若修改,则v2.distance=3,v3.distance=2,v6.distance=3,所以我们只修改v3的distance,并将v3设为已知:

    • 继续,我们罗列出所有已知顶点(v0、v1、v3、v4)指向的所有未知顶点:v2、v5、v6,发现若修改,则v2.distance=3,v5.distance=10,v6.distance=3,我们在v2和v6中任选一个如v2,只修改v2.distance,并将v2设为已知:

    • 继续,我们罗列出所有已知顶点指向的所有未知顶点:v5、v6,发现若修改,则v5.distance=5,v6.distance=3,所以我们只修改v6:

    • 最后,罗列出的未知顶点只有v5,若修改,其distance=5,我们将其修改并设为已知,算法结束:

    • 有权图伪代码:

    //有权最短路径计算,图存于邻接表graph,结果存入pathTable,起点即start
    void weightedPath(Node* graph,struct pathNode* pathTable,size_t  start)
    {
        //初始化pathNode数组
        
        size_t curV;
        while(true)
        {
            //找到可确定distance的未知顶点中新distance最小的那个,存入curV,若没有则跳出循环
            //令pathNode[curV].distance和pathNode[curV].prev修改为正确的值
            pathNode[curV].known=true;
        }
    }
    

    教材学习中的问题和解决过程

    • 问题1:了解拓扑排序

    • 问题1解决方案:(详见链接5)

    • 拓扑排序就是对图中顶点进行的排序,其要求是:若存在从vx到vy的路径,那么排序结果中vx必须在vy之前。

    • 进行拓扑排序的图必须是有向无圈图。

      • 在无向图中,若存在边(vx,vy)则必存在边(vy,vx),那么依拓扑排序的要求,vx就必须在vy的前面,同时vy又必须在vx前面,这显然是矛盾的,所以拓扑排序只能用于有向图。
      • 在有向有圈图中,比如上图,其中的圈v0-v1-v4-v3-v0就暗含着两条子路径:v0-v1-v4和v4-v3-v0,依前一条路径而言,排序结果中v0必须在v4前面,而依后一条路径而言,v4又必须在v0前面,这显然也是矛盾的,所以拓扑排序只能用于有向无圈图。
    • 有向无圈图的两个特点:

      • 若图有向无圈,则必然存在一个入度为0的顶点。
      • 若图有向无圈,则去掉其入度为0的顶点及相连边(必为以该顶点为起点的有向边)后,图依然是有向无圈图。
    • 伪代码:

    void topSort(graph* g,size_t numVertex,size_t topResult)
    {
        //两个表示顶点的变量,后面用
        size_t tempV,adjV;
        //存储各顶点入度的数组,顶点x的入度为indegree[x]
        size_t indegree[numVertex];
        伪:根据图g初始化indegree数组
    
        for(int i=0;i<numVertex;++i)
        {
            伪:从indegree中找到一个入度为0的顶点,存入tempV
            if(伪:没找到入度为0的顶点)
                伪:报错、返回
            
            topResult[i]=tempV;
    
            伪:通过g[tempV]遍历tempV为起点的边的终点,存入adjV
                indegree[adjV]--;
        }
    }
    
    • 问题2:图的广度优先遍历和深度优先遍历最根本的区别是什么?
    • 问题2解决方案:
    • 深度优先遍历的非递归做法时采用栈;广度优先遍历的非递归做法时采用队列
    • 深度优先遍历是把每个分支深入到不能深入为止。具体的有先序遍历、中序遍历、后序遍历;广度优先遍历又称层序遍历,从上往下一层一层遍历
    • 问题3:关于深度优先遍历的网络爬虫,深入了解一下
    • 问题3解决方案:
    • 网络爬虫(又被称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。
    • 访问频率限制;
      • Header头部信息校验;
      • 采用动态页面生成;
      • 采用动态页面生成;
      • 登录限制;
      • 验证码限制等。
    • 爬虫实例
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.net.MalformedURLException;
    import java.net.URL;
    
    import org.jsoup.Jsoup;
    import org.jsoup.nodes.Document;
    import org.jsoup.nodes.Element;
    import org.jsoup.select.Elements;
    
    public class Reptile {
        
        public static void main(String[] args) {
            // 传入你所要爬取的页面地址
            String url1 = "http://www.xxxx.com.cn/";
            // 创建输入流用于读取流
            InputStream is = null;
            // 包装流, 加快读取速度
            BufferedReader br = null;
            // 用来保存读取页面的数据.
            StringBuffer html = new StringBuffer();
            // 创建临时字符串用于保存每一次读的一行数据,然后 html 调用 append 方法写入 temp;
            String temp = "";
            try {
                // 获取 URL;
                URL url2 = new URL(url1);
                // 打开流,准备开始读取数据;
                is = url2.openStream();
                // 将流包装成字符流,调用 br.readLine() 可以提高读取效率,每次读取一行;
                br = new BufferedReader(new InputStreamReader(is));
                // 读取数据, 调用 br.readLine() 方法每次读取一行数据, 并赋值给 temp, 如果没数据则值 ==null,
                // 跳出循环;
                while ((temp = br.readLine()) != null) {
                    // 将 temp 的值追加给 html, 这里注意的时 String 跟 StringBuffer
                    // 的区别前者不是可变的后者是可变的;
                    html.append(temp);
                }
                // 接下来是关闭流, 防止资源的浪费;
                if (is != null) {
                    is.close();
                    is = null;
                }
                // 通过 Jsoup 解析页面, 生成一个 document 对象;
                Document doc = Jsoup.parse(html.toString());
                // 通过 class 的名字得到(即 XX), 一个数组对象 Elements 里面有我们想要的数据, 至于这个 div的值,打开浏览器按下 F12 就知道了;
                Elements elements = doc.getElementsByClass("xx");
                for (Element element : elements) {
                    // 打印出每一个节点的信息; 选择性的保留想要的数据, 一般都是获取个固定的索引;
                    System.out.println(element.text());
                }
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    代码学习中的问题和解决过程

    • 问题1:计算出度和入度时,0总是多出很多,为什么?
    • 问题1解决方案:
    • 因为该并未初始化,所以没有的点都默认为0,于是
            for(i=0;i<5;i++){
                for(j=0;j<5;j++){
                    a[i]=0;
                    b[i]=0;
                    dig[i][j]=-1;
                }
            }
    
    
    • 问题2:输入点不能溢出,如下图:

    • 问题2解决方案:

    for(i=0;i<5;i++){
                System.out.println("请输入第一个: ");
                int input0 = scan.nextInt();
                dig[i][j] = input0;
                for(j=1;;j++){
                System.out.println("请问是否有下一个接点 ");
                    String kong = scan.nextLine();
                    char yn = scan.next().charAt(0);
                    if(('Y' == yn)||(yn == 'y')){
                        String kong2 = scan.nextLine();
                        int input = scan.nextInt();
                        dig[i][j] = input;
                        a[i]++;
                    }
                    else{
                        j=0;
                        break;
                    }
    
                }
            }
            for(int k=0;k<5;k++) {
                for(j=0;j<5;j++){
                    for(i=0;dig[j][i]!=-1;i++){
                        if(dig[j][i]==k)
                            b[k]++;
                    }
                }
            }
    
    • 问题3:
    • 问题3解决方案:
    //java语言
    
    public class PrintBinaryTree {
    
        public static class Node {
            public int value;
            public Node left;
            public Node right;
    
            public Node(int data) {
                this.value = data;
            }
        }
    
        public static void printTree(Node head) {
            System.out.println("Binary Tree:");
            printInOrder(head, 0, "H", 17);
            System.out.println();
        }
    
        public static void printInOrder(Node head, int height, String to, int len) {
            if (head == null) {
                return;
            }
            printInOrder(head.right, height + 1, "v", len);
            String val = to + head.value + to;
            int lenM = val.length();
            int lenL = (len - lenM) / 2;
            int lenR = len - lenM - lenL;
            val = getSpace(lenL) + val + getSpace(lenR);
            System.out.println(getSpace(height * len) + val);
            printInOrder(head.left, height + 1, "^", len);
        }
    
        public static String getSpace(int num) {
            String space = " ";
            StringBuffer buf = new StringBuffer("");
            for (int i = 0; i < num; i++) {
                buf.append(space);
            }
            return buf.toString();
        }
    
        public static void main(String[] args) {
            Node head = new Node(1);
            head.left = new Node(-222222222);
            head.right = new Node(3);
            head.left.left = new Node(Integer.MIN_VALUE);
            head.right.left = new Node(55555555);
            head.right.right = new Node(66);
            head.left.left.right = new Node(777);
            printTree(head);
    
            head = new Node(1);
            head.left = new Node(2);
            head.right = new Node(3);
            head.left.left = new Node(4);
            head.right.left = new Node(5);
            head.right.right = new Node(6);
            head.left.left.right = new Node(7);
            printTree(head);
    
            head = new Node(1);
            head.left = new Node(1);
            head.right = new Node(1);
            head.left.left = new Node(1);
            head.right.left = new Node(1);
            head.right.right = new Node(1);
            head.left.left.right = new Node(1);
            printTree(head);
    
        }
    
    }
    

    总代码

    代码托管第十九章

    书本代码第十九章

    (statistics.sh脚本的运行结果截图)

    上周考试错题总结

    最近无检测,故无错题

    点评过的同学博客和代码

    • 本周结对学习情况

    - 结对学习内容
        - 学习图的定义
        - 学习图的遍历
    

    其他(感悟、思考等,可选)

    团队是重要,合作起来我们可以攻克所有难关。

    学习进度条

    代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
    目标 10000行 30篇 400小时
    第一周 69/69 2/2 30/30 Scanner
    第二、三周 529/598 3/5 25/55 部分常用类
    第四周 300/1300 2/7 25/80 junit测试和编写类
    第五周 2665/3563 2/9 30/110 接口与远程
    第六周 1108/4671 1/10 25/135 多态与异常
    第七周 1946/6617 3/13 25/160 栈、队列
    第八周 831/7448 1/14 25/185 查找、排序
    第九周 6059/13507 3/17 35/220 二叉查找树
    第十周 1354/14861 3/20 45/265

    尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。
    耗时估计的公式:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。

    参考:软件工程软件的估计为什么这么难软件工程 估计方法

    • 计划学习时间:30小时

    • 实际学习时间:45小时

    • 改进情况:
      这周没有太多别的事情,专心学习Java,攻读数据结构。

    参考资料

  • 相关阅读:
    Spring 发生 has not been refreshed yet 异常
    rsyslog config
    grok
    阿里云态势
    Unity的asm笔记
    Unity2020或Unity2019安装后无法启动
    rider代码折叠
    使用rider调试lua
    MacType更好的字体渲染
    Unity字体和画面花屏处理
  • 原文地址:https://www.cnblogs.com/zhaopeining/p/11931443.html
Copyright © 2020-2023  润新知