数据结构 - 图

转载注明出处
原文地址 : http://47.99.144.106/archives/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84-%E5%9B%BE

概念

图(graph)是由一些点(vertex)和这些点之间的连线(edge)所组成的;其中,点通常称为 顶点(vertex) ,而点到点之间的连线通常称之为 边或者弧(edge) 。通常记为G=(V,E)。

权重:当边带有数字标签时,可以将这些数字称为 权重 ,并且说这个图是一个 加权图

路径:一个顶点到另一个顶点的边的序列

连通的图:图中的每一个顶点到其他的每一个顶点都有一条路径

完全的图:从每一个顶点到其他的每一个顶点都有一条边

分类

根据边是否有方向,图又分为 有向图无向图

性质

N个节点的完全有向图边的数目:N*(N-1)

N个节点的完全无向图边的数目:N*(N-1)/ 2

表示方式

图有 邻接矩阵邻接链表 两种表示方式

邻接矩阵

  1. 如果图中有顶点 i 到顶点 j 的一条边,则G[i] [j]= 1

  2. 如果边有权重,权重值可以占用矩阵单元格,没有边的单元格要用不在允许的权重值范围之内的一个值来表示

邻接表

  1. 图的邻接表是N个链表的一个数组
  2. 只有从 ij 有一条边时,第i个链表才包含了顶点j的一个节点
  3. 当边有权重时,权重可以作为节点的一个数据字段

两种表示的分析

相邻矩阵邻接表
判断两个给定点之间是否有一条边O(1)与列表长度成相信关系
找出一个给定点的所有相邻节点O(N)取决于给定点的列表长度
内存O(N^2)N个指针的一个数组
使用场景稠密图稀疏图

遍历

深度优先遍历(depth-first traversal)

图的 深度优先遍历类似于树的先序遍历, 采用的搜索方法的特点是尽可能先对纵深方向进行搜索。

假设给定图G的初态是所有顶点均未曾访问过

首先访问出发点v,并将其标记为已访问过;

然后依次从v出发搜索v的每个邻接点w。若w未曾访问过,则以w为新的出发点继续进行深度优先遍历,直至图中所有和源点v有路径相通的顶点(亦称为从源点可达的顶点)均已被访问为止。

若此时图中仍有未访问的顶点,则另选一个尚未访问的顶点作为新的源点重复上述过程,直至图中所有顶点均已被访问为止。【1】

下面用一个具体的例子来说明:

Tip:使用VisuAlgo来可视化观察整个过程

  1. 首先标记点0,然后按字典序寻找未标记的相邻点进行遍历(点1)。
  2. 标记点1,按字典序寻找未标记的相邻点继续遍历(点2)。
  3. 标记点7,由于7没有相邻点,回溯点0。
  4. 标记点3,同样步骤标记点5和6
  5. 尝试标记点6的相邻点5,由于5已经标记过了,回溯到点0。
  6. 标记点4。
  7. 尝试标记点1,由于点1已经标记过了,回溯到点0,结束遍历。
实现
def dfs(graph, start):
    """
    :param graph: 邻接表存储的图
    :param start: 开始遍历的源节点
    :return: dfs结果序列
    """
    visited = []    # 存放访问过的节点的列表
    stack = [start]      # 构造一个堆栈
    while stack:     # 堆栈空时结束
        current = stack.pop()   # 堆顶出栈
        if current not in visited:  # 判断当前结点是否被访问过
            visited.append(current)     # 如果没有访问过,则将其加入访问列表
        for next_adj in graph[current]:  # 遍历当前结点的下一级
            if next_adj not in visited:  # 没有访问过的全部入栈
                stack.append(next_adj)
    return visited

广度优先遍历(breadth-first traversal)

广度优先搜索以队列作为算法中的集合,类似于树的层序遍历。

仍然以上面的例子来说明:

  1. 首先标记点0,按顺序将1,3,4加入队列
  2. 取队头的1,将2加入队列
  3. 取队头的3,将5和6加入队列
  4. 取队头的4,尝试将1加入队列,但1已经被访问过了。
  5. 取队头的2,将7加入队列
  6. 重复以上步骤至结束
实现
def bfs(graph, start):
    """
        :param graph: 邻接链表存储的图
        :param start: 开始遍历的源节点
        :return: bfs结果序列
    """
    visited = []  # 存放访问过的节点的列表
    queue = Queue()  # 构造一个队列
    queue.put(start)
    while not queue.empty():  # 队列空时结束
        current = queue.get()  # 取队首元素
        if current not in visited:  # 判断当前结点是否被访问过
            visited.append(current)  # 如果没有访问过,则将其加入访问列表
        for next_adj in graph[current]:  # 遍历当前结点的下一级
            if next_adj not in visited:  # 没有访问过的全部入队
                queue.put(next_adj)
    return visited

最小生成树

当遍历图结构时会隐式地形成一棵树,它的根节点就是遍历开始的顶点。当图中的边有权重时,可以将生成树的所有边的权重加和,以试图找出这个和最小的一颗生成树。

  1. 由于树的结构特点,最小生成树不能成环
  2. 对于N个顶点的连通图,生成树只能有N-1条边,这N-1条边连通着N个顶点。

Kruskal算法

由Joseph Kruskal于1956年提出,其算法思路为:

  1. 将连通图中的所有边取出,按权重从小到大排序

  2. 将边按从小到大的顺序填回图中,条件是:

    如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分;反之,舍去。

  3. 直到具有 n 个顶点的连通网筛选出来 n-1 条边为止

Prim算法

由 Robert C. Prim 于1957年提出,其算法简单描述为:

  1. 对一个加权连通图G=(V,E),初始化Vnew={u}, u为图中的任一节点,Enew={},空集合

  2. 重复下列操作,直到Vnew = V:

    a.在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);

    b.将v加入集合Vnew中,将<u, v>边加入集合Enew中;

单源点最短路径问题

Dijkstra算法

迪杰斯特拉算法(Dijkstra) 是计算一个顶点到其余各顶点最短路径的算法。

算法思想为:

  1. 声明一个集合s来存储已经确定最短路径的顶点 s = {},以及一个数组 dis 来存储各顶点的最短路径
  2. 初始化 s = {a}(起始点),dis = [0, ∞,∞,∞,∞,∞,∞,∞ ]
  3. 找到一条边就能到达起始点的顶点,将最短的那个顶点加入 s 中,更新对应的 dis
  4. 选择一条边就能到达集合中点的顶点,将(边长+直达顶点对应的 dis )最短的那个顶点加入 s ,更新该顶点的 dis
  5. 重复以上步骤直到 s 包含图中所有的顶点

时间复杂度:O(n^2);

多源点最短路径问题

单源点最短路径是求一个源点到图中其他点的距离,结果用一个数组表示。而多源点最短路径是求图中所有顶点到其他点的最短路径,结果用一个矩阵来表示

计算多源点最短路径可以通过对n个顶点都进行一次dijkstra算法来实现,时间复杂度为O(n^3),也可以通过Floyd算法实现

Floyd算法

对于上面这张图,我们可以用一个4 * 4 的矩阵 e 来存储其直达路径,e[i][j]来表示 ij 的距离,如 e[4][3] = 12,如果两个点无法直达,则用 ∞ 来表示。

如果想让任意两点间的路径变短,只有通过引入其他顶点来 中转 ,例如4到3,可以引入顶点1,这样两点间的距离就缩短成了5+6=11,中转点也有可能是多个顶点,如引入顶点1和2,距离就缩短成了5+2+3=10。

我们先假设两点之间不允许经过第三个顶点,那么各个顶点间的最短路径就是直达路径,如下

现在放宽限制,允许通过1号顶点来 中转 ,那么如何求出两点间的最短路径呢?很简单,我们只需要判断e[i][1] + e[1][j] 是否小于 e[i][j],小于更新,否则不变,处理后路径变化如下:

接下来继续放宽条件,允许通过1和2两个顶点来中转,那么我们需要在允许1中转条件下得到的最短路径矩阵(也就是上面这张图)的基础上,判断e[i][2] + e[2][j] 是否小于 e[i][j],结果如下:

以同样的步骤分别继续开放3和4顶点作为中转,结果如下:

Tip:Floyd算法不能解决带有"负权回路"的图,因为带有负权回路的图没有最短路,如下图,从点1到3,每次经过1->2->3->1->2->…->1,每绕一次圈最短路就减1,永远找不到最短路。

参考

【1】 深度优先遍历和广度优先遍历

【2】 Floyd-傻子也能看懂的弗洛伊德算法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值