20172305 2018-2019-1 《Java软件结构与数据结构》第九周学习总结
教材学习内容总结
本周内容主要为书第十五章内容:
-
图(结点和结点之间的连接构成)
- 顶点:结点
- 边:结点之间的连接
- 邻接:两个结点之间有一条连通边,则两个结点是邻接的,有时邻接顶点称为邻居。
- 自循环(环):连通一个顶点及其自身的边
- 环路:一种首顶点和末顶点相同且没有重边的路径,没有环路的图称为无环的。
-
无向图:一种边为无序结点对的图
- 如果无向图拥有最大数目的连通顶点的边,则认为这个无向图是完全的。
- 对有n个顶点的无向图,要使图完全就要求有n(n-1)/2条边。
- 如果无向图中的任意两个顶点之间都存在一条路径,则认为这个无向图是连通的。
- 无向图的路径是双向的。
- 路径:图中一系列边,每条边连通两个顶点。
- 无向树是一种连通的无环无向图,其中一个元素被指定为树根。
-
有向图(双向图):边为有序顶点对的图,eg:边(A,B)允许从A向B游历,但不允许反方向的游历。
- 如果有向图的每两个顶点之间都有两条方向相反的边连接,则认为这个有向图是完全的。
- 如果有向图的任意两个顶点之间都存在一条路径,且连接两个顶点的路径中所有的边都必须同向,则认为这个有向图是连通的。
- 拓扑序(排列得到的顶点次序):如果有向图中没有环路,且有一条从A到B的边,则可以把顶点A安排在顶点B之前。
- 有向图的路径是单向的。
- 路径:图中连通两个顶点的有向边序列。
- 有向树是一种指定了一个元素作为树根的有向图。
- 不存在其他顶点到树根的连接
- 每个非树根元素恰好有一个连接
- 树根到每个其他顶点都有一条路径
-
网络(加权图):一种每条边都带有权重或代价的图
- 三元组来表示每条边:起始顶点、终止顶点、权重
- 无向网络的起始顶点与终止顶点可以互换
- 有向图必须包含每个有向连接的三元组
-
图算法--遍历
- 广度优先遍历(类似树的层序遍历)
- 首先从一个未走到过的顶点作为起始顶点,比如元素0顶点作为起点。
- 沿0顶点的边去尝试访问其它未走到过的顶点,首先发现2顶点还没有走到过,于是来到了2顶点。
- 返回到0顶点,再以0顶点作为出发点继续尝试访问其它未走到过的顶点,这样来到了1顶点。
- 返回到0顶点,再以0顶点作为出发点继续尝试访问其它未走到过的顶点,这样来到了5顶点。
- 但是,此时沿0顶点的边,已经不能访问到其它未走到过的顶点了,所以需要返回到2顶点。
- 沿2顶点的边去尝试访问其它未走到过的顶点,(因为1顶点已经走过了)首先发现3顶点还没有走到过,于是来到了3顶点。
- 返回到2顶点,再以2顶点作为出发点继续尝试访问其它未走到过的顶点,这样来到了4顶点。
- 但是,此时沿4顶点的边,已经不能访问到其它未走到过的顶点了,至此,所有顶点我们都走到过了,遍历结束。
- 深度优先遍历(类似树的前序遍历)
- 首先从一个未走到过的顶点作为起始顶点,比如元素0顶点作为起点。
- 沿0顶点的边去尝试访问其它未走到过的顶点,首先发现2顶点还没有走到过,于是来到了2顶点。
- 再以2顶点作为出发点继续尝试访问其它未走到过的顶点,这样又来到了1顶点。
- 再以1号顶点作为出发点继续尝试访问其它未走到过的顶点。
- 但是,此时沿1顶点的边,已经不能访问到其它未走到过的顶点了,所以需要返回到2顶点。
- 返回到2号顶点后,以2顶点作为出发点继续尝试访问其它未走到过的顶点,此时又会来到3顶点,再以3号顶点作为出发点继续访问其它未走到过的顶点,于是又来到了5号顶点。
- 但是,此时沿5顶点的边,已经不能访问到其它未走到过的顶点了,所以需要返回到3顶点。
- 返回到3顶点后,以3顶点作为出发点继续尝试访问其它未走到过的顶点,此时会来到4顶点,再以4顶点作为出发点继续访问其它未走到过的顶点。
- 但是,此时沿4顶点的边,已经不能访问到其它未走到过的顶点了,至此,所有顶点我们都走到过了,遍历结束。
- 图的遍历可以从任一顶点开始
- 深度优先遍历和广度优先遍历的位移不同是使用到了栈而不是队列来管理遍历。
-
图算法--测试连通性
- 在一个图中,无论哪个为起始顶点,当且仅当广度优先遍历中的顶点数等于图中的顶点数目时,则该图是连通的。
-
图算法--最小生成树
- 生成树是一棵含有图中所有顶点和部分边(可能不是所有边)的树。
- 最小生成树是边的权重总和和小于或等于同一个图中其他任何一棵生成树的权重总和。
- 非连通的无向图,不存在最小生成树
- 权重不一定和距离成正比
- 权重可能是0或负数
- 若存在相等的权重,那么最小生成树可能不唯一
- Prim算法:
-
图算法--判断最短路径
- 方法一:判定起始顶点与目标顶点之间的字面意义上的最短路径,即两个顶点之间的最小边数。
- 方法二:Dijkstra算法
- 以0顶点为开始位置,0顶点到1顶点的权重为3,到2顶点的权重为max,到3顶点的权重为4,到4顶点的权重为5。
- 选取权重最小的顶点为1顶点,因为所有的权重都为正,那么0顶点到1顶点的最短距离就是3,再以1顶点开始,1顶点可以到4顶点和2顶点,到4顶点的位置是3+1=4<5,即0顶点到4顶点的最短距离是通过1顶点的,再看到2顶点的位置是3+10=13<max,即0顶点直接到2顶点的距离为max,但是通过1顶点可以缩短为13。
- 扣除0顶点和1顶点已经确定好了最短距离之后,选取最短的权重为0顶点到3顶点为4。
- 以3顶点开始,3顶点可以到2顶点,到2顶点的位置是4+7=11<13,即0顶点到2顶点的最短距离,从max到通过1顶点再到通过3顶点到2顶点。
- 扣除0顶点、1顶点和3顶点,已经确定好了最短距离之后,选取最小的权重为0顶点到1顶点再到4顶点为4,4顶点可以到1顶点、3顶点和2顶点,但是3顶点和1顶点已经确定好了,只剩2顶点,那么到2顶点的位置为3+1+6=10<11,即到2顶点的最短距离改为通过1顶点、4顶点最后到2顶点。
- 最后扣除0顶点、1顶点、3顶点和4顶点,只剩下2顶点,距离只剩10,所以到2顶点的最短距离为10,结束算法。
-
图的实现策略
- 邻接列表
- 在邻接表的表示中,无向图的同一条边在邻接表中存储的两次。如果想要知道顶点的读,只需要求出所对应链表的结点个数即可。
- 有向图中每条边在邻接表中只出现一此,求顶点的出度只需要遍历所对应链表即可。求出度则需要遍历其他顶点的链表。
无向图的邻接列表:
有向图的邻接列表:
- 邻接矩阵
- 在邻接矩阵表示中,无向图的邻接矩阵是对称的。矩阵中第 i 行或 第 i 列有效元素个数之和就是顶点的度。
- 在有向图中 第 i 行有效元素个数之和是顶点的出度,第 i 列有效元素个数之和是顶点的入度。
无向图的邻接矩阵:
有向图的邻接矩阵:
- 邻接矩阵与邻接表优缺点:邻接矩阵的优点是可以快速判断两个顶点之间是否存在边,可以快速添加边或者删除边。而其缺点是如果顶点之间的边比较少,会比较浪费空间。因为是一个 n∗nn∗n 的矩阵。而邻接表的优点是节省空间,只存储实际存在的边。其缺点是关注顶点的度时,就可能需要遍历一个链表。还有一个缺点是,对于无向图,如果需要删除一条边,就需要在两个链表上查找并删除。
- 边集数组
- 边集数组由两个一维数组构成:一个存储顶点信息; 一个存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)、和权(weight)组成。
- 边集数组关注的是边的集合,在边集数组中要查找一个顶点的度需要扫描整个边数组,效率并不高。因此它更适合对边依次进行处理的操作,而不适合对顶点相关的操作。
无向图的边集数组:
有向图的边集数组:
- 十字链表
- 十字链表是为了便于求得图中顶点的度(出度和入度)而提出来的。用十字链表来存储有向图,可以达到高效的存取效果。它是综合邻接表和逆邻接表形式的一种链式存储结构。
绿色链表表示以结点A为弧头的弧组成的链表。黄色链表表示以结点A为弧尾的弧组成的链表。
- 邻接多重表
- 邻接多重表主要用于存储无向图。如果用邻接表存储无向图,每条边的两个边结点分别在以该边所依附的两个顶点为头结点的链表中,这给图的某些操作带来不便。因此,在进行这一类操作的无向图的问题中采用邻接多重表作存储结构更为适宜。
教材学习中的问题和解决过程
-
问题1:如何测试有向图和无向图的连通性?
-
问题1解决方案:无向图的连通性在书上已经给出,字面理解就是任意两点之间都存在一条路径,而从编写的角度就是一个顶点开始深度优先遍历或是广度优先遍历得出的顶点数。而有向图的是具有方向的,如果这样的话我们如何判定有向图的连通性?在查询的过程中,发现有向图的连通性可以分成强连通图、弱连通图以及单向连通图。书上的图15.4的连通图就是一个弱连通图,如果是强连通图则需要在此基础上每两点之间都添加一条与之方向相反的线。针对有向图的测试连通性的方法可以用到Tarjan算法
- 在有向图中, 若对于任意两个两点之间, 都存在互逆的路径,则称此图是强连通图。即有向图中,若对于任意两个不同的顶点x和y,都存在从x到y以及从y到x的路径,则称G是强连通图。
- 有向图的所有的有向边替换为无向边,所得到的图称为原图的基图。如果一个有向图的基图是连通图,则有向图是弱连通图。
- 如果有向图中,对于任意节点v1和v2,至少存在从v1到v2和从v2到v1的路径中的一条,则原图为单向连通图。
- 强连通图、连通图、单向连通图三者之间的关系是,强连通图必然是单向连通的,单向连通图必然是弱连通图。
- 在Tarjan算法中为每个节点i维护了以下几个变量:
DFN[i]:深度优先搜索遍历时节点i被搜索的次序。
low[i]:节点i能够回溯到的最早位于栈中的节点。
flag[i]:标记几点i是否在栈中。 - Tarjan算法的运行过程:
(1)首先就是按照深度优先搜索算法搜索的次序对图中所有的节点进行搜索。
(2)在搜索过程中,对于任意节点u和与其相连的节点v,根据节点v是否在栈中来进行不同的操作:
a.节点v不在栈中,即节点v还没有被访问过,则继续对v进行深度搜索。
b.节点v已经在栈中,即已经被访问过,则判断节点v的DFN值和节点u的low值的大小来更新节点u的low值。如果节点v的 DFN值要小于节点u的low值,根据low值的定义(能够回溯到的最早的已经在栈中的节点),我们需要用DFN值来更新u 的low值。
(3)在回溯过程中,对于任意节点u用其子节点v(其实不能算是子节点,只是在深度遍历的过程中,v是在u之后紧挨着u的节点)的 low值来更新节点u的low值。因为节点v能够回溯到的已经在栈中的节点,节点u也一定能够回溯到。因为存在从u到v的直接路径,所以v能够到的节点u也一定能够到。
(4)对于一个连通图,我们很容易想到,在该连通图中有且仅有一个节点u的DFN值和low值相等。该节点一定是在深度遍历的过程中,该连通图中第一个被访问过的节点,因为它的DFN值和low值最小,不会被该连通图中的其他节点所影响。
代码学习中的问题和解决过程
- 问题1:如何理解书中深度和广度的遍历代码?递归形式如何表示?
- 问题1的解决方案:广度优先遍历和深度优先遍历的区别是在于广度优先遍历用的是队列,深度优先遍历用的是栈,一个是先进先出另一个后进先出。
- 遍历是创建了一个无序列表和栈或是队列,队列或栈用来存放的是索引值,无序列表存储的是结果,先进行判断如果你想查找的的索引值是超出用来存放内容的数组的长度的话,就会直接迭代无序列表,注意此时的无序列表是没有内容的,所以迭代出的内容也什么也没有。如果没有超过则创建一个布尔的一维数组用来记录每个索引值对应的内容是否被查过,先记录每个索引值都未被查到,再将开始查找元素的索引值存入栈或队列中,并将该索引值对应的布尔型数组内存放的false改为true来确定是该元素已经查过。进入while循环变量x是刚入栈或是队列的索引值,广度优先遍历是将该索引值对应的内容添加到无序列表的尾部,再进入for循环不断将为未存元素存入队列中,通过判断存入队列的元素是否与x元素有边,如果有边还未被查找的元素就会被存入队列中,然后跳出for循环在while循环中此时更换出队的索引值,再进入for循环通过判断条件来确定继续存放在队列中的索引值,这样通过不同的索引值来实现图上的每一个元素都会连着下一个元素,类似层序遍历的思路。而深度优先遍历循环体条件和判断条件都相同,但是判断条件内部不同,需要辅助一个布尔型的变量来判断是否与查找元素有联系,深度优先遍历与广度优先遍历不同的是深度有可能会遇到该元素没有与之相连的边的元素未被查到,而与它相连的边的被查元素还有边和未查元素,这样就需要返回到上一个,在从上一个开始进行循环,而多出的那个判断条件就是确定该元素是否符合上述可能情况如果符合的话就会弹出,到上一个元素位置进行查找,这是广度优先遍历不会遇到的情况,也是改用栈的原因,深度优先遍历类似与前序遍历的思路。根据书上所写可以用递归的方法编写代码,附递归版代码(广度优先遍历的代码在网上查找的)
public void DFSRecursion() {
boolean[] visited = new boolean[numVertices];
for (int i = 0; i < numVertices; i++)
visited[i] = false;
for(int i=0;i<numVertices;i++) {
if (!visited[i]) {
depthFirstSearch(visited,i);
}
}
}
private void depthFirstSearch(boolean[] isVisited,int i) {
System.out.print(vertices[i]+" ");
isVisited[i]=true;
int w=getFirstNeighbor(i);
while (w!=-1) {
if (!isVisited[w]) {
depthFirstSearch(isVisited,w);
}
w=getNextNeighbor(i, w);
}
}
public void broadFirstSearch() {
boolean[] visited = new boolean[numVertices];
for (int i = 0; i < numVertices; i++)
visited[i] = false;
for(int i=0;i< numVertices;i++) {
if(!visited[i]) {
broadFirstSearch(visited, i);
}
}
}
private void broadFirstSearch(boolean[] isVisited,int i) {
int u,w;
LinkedList queue=new LinkedList();
System.out.print(vertices[i]+" ");
isVisited[i]=true;
queue.addLast(vertices[i]);
while (!queue.isEmpty()) {
u=((Integer)queue.removeFirst()).intValue();
w=getFirstNeighbor(u);
while(w!=-1) {
if(!isVisited[w]) {
System.out.print(vertices[w]+" ");
isVisited[w]=true;
queue.addLast(w);
}
w=getNextNeighbor(u, w);
}
}
}
- 问题2:PP15.7如何确定最短路径和最便宜路径?
- 问题2的解决方案:根据题目要求允许客户输入两个城市以及两个城市之间的价格,这就类似加权图,根据GraphADT接口以及实现的添加顶点和添加边的方法都没有涉及到权重(这里的权重为两个城市之间的价格),而无向图实现的是确定两个顶点之间是否有边,用一个二维布尔型数组存储对应顶点是否有边,我们的想法是可以将是否有边来改为数字,用票价来实现是否有联系。那么问题又来了,没有联系的如何来界定?定义为最大还是最小?如果定义为最小为零的话,我们的选择就要在排除零的前提下进行Dijkstra算法,但是如果我们将无联系的看作是无穷大的情况下,就会产生不一样的效果,在选取权重较小即最便宜路径的时候就会很方便,而且事实上提供的算法也是要将无联系的设为最大,就如同教材内容总结部分叙述的算法一样。但是,无论是无穷大还是无穷小多是用什么表示?和数序符号一样么?API提供了解决办法(侯泽洋同学提供):
Double.POSITIVE_INFINITY
表示的是正无穷大值,Double.NEGATIVE_INFINITY
表示的是负无穷大值。这样就可以实现了,但是Dijkstra算法如何书写呢?又是一个问题好丧...在网上查了几个,没有适用成功所以通过看侯泽洋的博客找到了一个不错的博客,用他的算法来实现的具体代码,而且侯泽洋的代码层次设计的非常好,实现的也干净利落,很强大,是我值得学习的。此外,最短路径就可以用到Graph内的shortestPathLength
方法和iteratorShortestPath
实现,我在此基础上通过返回最短路径的数值,如果是1的话就会存在直达列车,大于1的话就会是不可直达,在同通过遍历最短路径的迭代方法进行输出最短路径就好。
代码托管
上周考试错题总结
错题已经在上周博客写过...
结对与互评
点评(王禹涵)
- 博客中值得学习的或问题:
- 广度优先遍历和深度优先遍历的分析最好和最小生成树的算法分析的那么细致就好,建议问题二的代码可以附上去。
- 代码中值得学习的或问题:
- 分析了最小生成树的Prim算法,很强大,通过你的分析有助于我对算法的理解,很棒!
- 基于评分标准,我给本博客打分:7分。
- 得分情况如下:
- 正确使用Markdown语法(加1分)
- 模板中的要素齐全(加1分)
- 教材学习中的问题和解决过程, 三个问题加3分
- 代码调试中的问题和解决过程, 一个问题加1分
- 感想,体会不假大空的加1分
- 点评认真,能指出博客和代码中的问题的加1分
点评(方艺雯)
- 博客中值得学习的或问题:
- 图片清晰,总结的也很到位,也可以对本章的算法进行简单的总结,博客中的问题二和代码中的问题二可以整合一下。
- 代码中值得学习的或问题:
- 代码中的问题总结的很好,特别是那个迭代器的广度优先遍历用到分析的特别细致,建议可以和算法内容进行联系,或是在分析代码的过程中查一些算法的相关知识。
- 基于评分标准,我给本博客打分:9分。
- 得分情况如下:
- 正确使用Markdown语法(加1分)
- 教材学习中的问题和解决过程, 二个问题加2分
- 代码调试中的问题和解决过程, 四个问题加4分
- 感想,体会不假大空的加1分
- 点评认真,能指出博客和代码中的问题的加1分
互评对象
-
本周结对学习情况
20172314方艺雯
20172323王禹涵 -
结对学习内容:图和一堆算法
感悟
第十五章的图感觉和红黑树一样,要构造很乱的数据结构来实现,此外还有一堆算法可以优化图的操作(算法作用很好,但是难于理解,有的思路就难理解)。好在有些代码给出了,但是书上的遍历代码就没给全,导致始终读不懂一个方法具体干嘛的,具体实现的策略也不同于之前的数据结构。要保持好的心态来学习代码,切莫急功近利...
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 0/0 | 1/1 | 15/15 | |
第二周 | 703/703 | 1/2 | 20/35 | |
第三周 | 762/1465 | 1/3 | 20/55 | |
第四周 | 2073/3538 | 1/4 | 40/95 | |
第五周 | 981/4519 | 2/6 | 40/135 | |
第六周 | 1088/5607 | 2/8 | 50/185 | |
第七周 | 1203/6810 | 1/9 | 50/235 | |
第八周 | 2264/9074 | 2/11 | 50/285 | |
第九周 | 2045/11119 | 1/12 | 50/335 |