参考资料:KhanAcademy
1. 图(以一个无向图示例):描述多对多的数据结构
2. 图中的基本概念:
2.1. 顶点(Vertex/Vertices):一般用$V$表示顶点集,非空有限。例如“Frank”、“Emily”等,在图的数据结构中更经常用数组“1”、“2”、...来表示顶点。
衡量复杂度时会用到$\Theta(|V|)$($|V|$指顶点集的元素个数),但一般简写为$\Theta(V)$。
2.2. 边(Edge):一般用$E$表示边集,有限。例如依附(incident)于"Frank"和"Emily"的线,就是使"Frank"和"Emily"邻接(adjacent)的边。每条边依附的点只能是2个。
一般用$(u,v)$来表示依附在顶点$u$和顶点$v$上的边。
衡量复杂度时会用到$\Theta(|E|)$($|E|$指边集的元素个数),但一般简写为$\Theta(E)$。
2.3. 权重(Weight):边上可以有权重。
2.4. 方向(direction):边可以是单向的/无向的。
2.5. 图(Graph):图是顶点和顶点间的关系(边)的集合,一般用$G = (V, E)$表示。
如果顶点和顶点之间可以沿着图中的边访问得到,则称这两个顶点之间存在路径(Path)。
假设存在某A路径连接$u,v$两个顶点。如果找不到其他连接$u,v$的路径比A路径更短(无权重时,指边的数量;有权重时,指边上的权重之和),则称A路径为最短路径(Shortest Path)。
3. 图类型:
3.1 无向图(Undire Graph):边是没有方向的,如前面的示例图。$(u,v)$表示$u$和$v$可以互达。
3.2 有向图(Directed Graph):边有方向。每条边是单向的,两个顶点间可以有两条边。$(u,v)$表示$u$可以到达$v$,这个二元组包含了方向的信息,可以称$v$是$u$的邻接点,反之不可。
如果图中的所有顶点都不存在长度大于1的路径可以到达自己,这种图称为有向无环图(DAG: Directed Acyclic Graph)。
不管是哪种图,边都可以赋予权重:
4. 度(degree)
在无向图中,度表示依附于该顶点上的边的数量。
在有向图中
4.1. 出度(out-degree):离开(leave)一个顶点的边的数量。
4.2. 入度(in-degree):进入(enter)一个顶点的边的数量。
5. 有向图的3种表示方法以及其复杂度:
5.1. 边列表(Edge List) :直接以图中的边来表示:
例如[ [0,1], [0,6], [0,8], [1,4], [1,6], [1,9], [2,4], [2,6], [3,4], [3,5], [3,8], [4,5], [4,9], [7,8], [7,9] ]。
5.2. 邻接矩阵(Adjacency Matrix):用$|V|*|V|$的矩阵来表示顶点到顶点是否存在边:
每一行的行号表示以这个数字所代表的顶点,为出发顶点,列号同理,代表的是到达的顶点。
”0“行”1“列的元素数值为1,代表了图中存在$(0,1)$的边;数值为0代表不存在对应的边。
5.3. 邻接列表(Adjacency List):
每一行的行号代表出发的顶点,右边对应的列表存储与该顶点邻接的顶点:
三种方式的时间和空间复杂度比较如下:
时间复杂度 | 空间复杂度 | |
边列表 | $O(E)$ | $\Theta(E)$ |
邻接矩阵 | $O(1)$ | $\Theta(V^2)$ |
邻接列表 | $O(d)$ | $\Theta(V+E)$ |
广度优先搜索(BFS: Breadth-First Search):寻找给定顶点到目标顶点的最短路径
1. 简要思想:
一开始的想法是这样的:目标顶点为$v$。先访问给定顶点$u$,然后找到和给定顶点$u$邻接的顶点,记该集合为$V_1$,看看这个集合里有没有$v$,找到了就可以停止;
如果没有,访问和$V_1$中顶点邻接的顶点,记该集合为$V_2$,看看$V_2$有没有$v$,有则停止,没有则重复这样的过程,直到找到$v$为止。
但这里考虑可能同时寻找到不同的目标顶点的路径,把求最短距离和寻找路径拆分成了两步:先计算$u$到每一个顶点的最短距离,以及这个路径中该顶点的上一个顶点,但不求出整个路径;求出所有最短距离和前顶点,再根据需要求得到某个顶点的最短路径。
细节1.1
不断地寻找与顶点邻接的顶点,使用的是队列(queue)的数据结构。当一个顶点出队,与其邻接的顶点则入队,这样还可以使得距离更远的顶点始终处于队列的更尾端,从而更晚出队被读取。
细节1.2
算法过程会用(distance, precessor)二元组bfsInfo来存储某个顶点和给定顶点的距离以及这个距离所对应的路径里的上一个顶点,二元组所在的下标记为对应的顶点编号,都初始化为null或某个可以识别为特殊状态的数字。
利用这个二元组列表除了方便得到距离之后求路径之外,还有一个好处是可以根据distance的状态,来判断某个顶点是否已经被访问过。为了节省时间,在访问$V_i$时不再需要访问$V_1, V_2, \cdots, V_{i-1}$访问过的顶点。
2. 算法过程:
* 下面的算法实现广度优先搜索整个图,并求出给定顶点到目标顶点的最短路径的距离,求路径可以根据下面的距离顶点二元组和邻接关系进一步得到(这里只在python中实现出来)。
用source表示给定顶点,用bfsInfo表示(distance, precessor)二元组序列,用graph表示图的邻接列表,queue为所用队列。
2.1. for i in V, bfsInfo[i].distance = null; bsfInfo[i].precessor =null; bdsInfo[source].distance = 0; source入队
2.2 当queue不为空时循环:
curr = queue.dequeue() #队首元素出队,赋值给curr;
遍历curr的邻接点graph[curr],判断distance是否为null,为null则说明没有被搜索过,将其入队,并把distance设为curr的distance+1,precessor即为curr;
3. 利用邻接列表实现:
Javascript:
/* A Queue object for queue-like functionality over JavaScript arrays. */ var Queue = function() { this.items = []; }; Queue.prototype.enqueue = function(obj) { this.items.push(obj); }; Queue.prototype.dequeue = function() { return this.items.shift(); }; Queue.prototype.isEmpty = function() { return this.items.length === 0; }; /* * Performs a breadth-first search on a graph * @param {array} graph - Graph, represented as adjacency lists. * @param {number} source - The index of the source vertex. * @returns {array} Array of objects describing each vertex, like * [{distance: _, predecessor: _ }] */ var doBFS = function(graph, source) { var bfsInfo = []; for (var i = 0; i < graph.length; i++) { bfsInfo[i] = { distance: null, predecessor: null }; } bfsInfo[source].distance = 0; var queue = new Queue(); queue.enqueue(source); // Traverse the graph // As long as the queue is not empty: // Repeatedly dequeue a vertex u from the queue. // // For each neighbor v of u that has not been visited: // Set distance to 1 greater than u's distance // Set predecessor to u // Enqueue v // // Hint: // use graph to get the neighbors, // use bfsInfo for distances and predecessors while(!queue.isEmpty()){ var curr = queue.dequeue(); for(var j = 0; j < graph[curr].length; j++){ var check = graph[curr][j]; if(bfsInfo[check].distance === null){ queue.enqueue(check); bfsInfo[check].distance = bfsInfo[curr].distance+1; bfsInfo[check].predecessor = curr; } } } return bfsInfo; }; var adjList = [ [1], [0, 4, 5], [3, 4, 5], [2, 6], [1, 2], [1, 2, 6], [3, 5], [] ]; var bfsInfo = doBFS(adjList, 3); for (var i = 0; i < adjList.length; i++) { println("vertex " + i + ": distance = " + bfsInfo[i].distance + ", predecessor = " + bfsInfo[i].predecessor); } Program.assertEqual(bfsInfo[0], {distance: 4, predecessor: 1});
Python:
class Queue: def __init__(self): self.items = [] def isEmpty(self): return self.items == [] def enqueue(self, item): self.items.insert(0, item) # use insert rather than append def dequeue(self): return self.items.pop() def Bfs(graph, source): bfsInfo = [[None, None] for i in range(len(graph))] # the first column for distance and the second for the precessor bfsInfo[source][0] = 0 queue = Queue() queue.enqueue(source) while queue.isEmpty() == False: curr = queue.dequeue() for check in graph[curr]: if bfsInfo[check][0] == None: # thus haven't been visited queue.enqueue(check) bfsInfo[check][0] = bfsInfo[curr][0] + 1 bfsInfo[check][1] = curr return bfsInfo adjList = [ [1], [0, 4, 5], [3, 4, 5], [2, 6], [1, 2], [1, 2, 6], [3, 5], [] ] result = Bfs(adjList, 3) # print the shortest path from source = 3 to target = 5 source = 3 target = 5 curr = target path = [curr] # store the path in a reversed direction: target -> source while (result[curr][1] != source): curr = result[curr][1] # get the precessor vertex path.append(curr) # and add in the path path.append(source) #print for ind in range(len(path)-1, 0, -1): print(path[ind], '->') print(path[0])