《大话数据结构》第七章:图(笔记)

第七章:图

学好了图,基本就等于理解了数据结构的精神

图(Graph):是由顶点的有穷非空集合和顶点之间边的集合组成,通常为G(V,E),其中,G是表示一个图,V(Vertex)是图G中顶点的集合,E(Edge)是图G中边的集合。

图的定定义与术语:

  • 按照有无方向,分为无向图有向图,无向图由顶点和边构成,有向图由顶点和弧构成,弧有弧尾弧头。
  • 按照边或者弧的多少,分为稀疏图稠密图。如果任意两个人顶点之间都存在边叫完全图,有向的叫做有向完全图。若无重复的边或顶点到自身的边则叫简单图(顶点不重复)。
  • 图中顶点之间有临接点、依附的概念。无向图中顶点的边树叫做度,有向图顶点分为入度出度
  • 图上的边或弧带有权则称为
  • 图中顶点间存在路径,两个顶点之间存在路径则说明是连通的,如果路径最终回到起点则称为,当中不重复叫简单路径。若任意两个顶点都是连通的,则图就是连通图,有向则称为强连通图。图中有子图,若子图极大(子图顶点最多)连通则就是连通分量,有向的则称为强连通分量
  • 无向图连通且n个顶点n-1条边叫做生成树。有向图中一顶点入度为0其余顶点入度为1的叫做有向树。一个有向图由若干颗树构成生成森林。

图的存储结构:

图的存储结构相对于链表与树来说就更加复杂

  • 邻接矩阵(Adjacency Matrix)存储方式,用两个数组来表示图,一个数组存储顶点信息,一个二维数组(邻接矩阵)存储边或弧的信息。
    在这里插入图片描述
    在网图中,每条边带有权重
    在这里插入图片描述
    在这里插入图片描述

  • 邻接表(Adjacency List),将数组与链表结合的存储方法,结点存入数组,将结点的孩子进行链式存储
    在这里插入图片描述

  • 十字链表(Orthogonal List),将领接表与邻接矩阵结合起来

重新定义顶点表:

重新定义边表结点:
在这里插入图片描述
在有向图的应用中,十字链表是非常好的数据结构模型。

  • 邻接多重表:
    在这里插入图片描述
    其中ivex和jvex是与某条边依附的两个顶点在顶点表中的下标。ilink指向依附顶点ivex的下一条边,jlink指向依附顶点jvex的下一条边。

  • 边集数组:由两个一维数组构成。一个是存储顶点的信息,另一个是存储边的信息,这个边数组每个数据元素由下一条边的起点下标(begin)、终点下标(end)和权(weight)组成。

图的遍历

图的遍历(Traversing Graph):从图中某一顶点出发访遍图中其余顶点,且使每个顶点仅被访问一次。

  • 深度优先遍历(Depth First Search),也有称为深度优先搜索,简称为DFS。

递归实现A—B—D—E—I—C—F—G—H

深度优先过程:

"""
深度优先遍历
"""

def DFS_traverse(head_node):
    if not head_node:
        return
    print(head_node.val)
    if head_node.left:
        return DFS_traverse(head_node.left)
    if head_node.right:
        return DFS_traverse(head_node.right)

深度优先遍历其实就是递归的过程,对与图来说就是将其转换为树进行前序遍历

  • 广度优先搜索(Breadth First Search),又称为广度优先搜索,简称BFS。

队列实现: A—B—C—D—E—F—G—H—I

"""
广度优先遍历,队列实现
"""


def BFS_traverse(head_node):
    if not head_node:
        return None
    queue_list = []
    queue_list.append(head_node)
    while queue_list:
        root = queue_list.pop(0)
        print(root.val)
        if root.left:
            queue_list.append(root.left)
        if root.right:
            queue_list.append(root.right)

如果说图的深度遍历类似树的前序遍历,那么图的广度遍历就类似树的层次遍历。它与DFS在时间复杂度上是一致的,不同之处仅仅在于对顶点访问的顺序不同。

深度优先更适合目标比较明确,以找到目标为主要目的的情况,而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。

最小生成树

针对网图,例如对多个村庄架设通信网络,求最小成本。每个结点间的连线带权值。我们把构造连通网的最小代价生成树称为最小生成树(Minimum Cost Spanning Tree)。找最小生成树,经典的有两种算法,普利姆(Prim)算法克鲁斯卡尔(Kruskal)算法

关键之处:

  • 普利姆算法,创建一个列表用于保存顶点下标adjvex[],创建一个列表用于保存相关顶点间的权值lowcost[],选择一个顶点开始,利用邻接矩阵更新权值列表,保存每一步顶点到顶点列表。(P247)
  • 克鲁斯卡尔,因为权值都在边上,所以以边为目标构建,主要利用边集数组。按权值从小到大排序,定义一个数组parent[]用来判断边与边是否形成环路。(P251)

如下代码已知边集数组(Kruskal算法),对边集数组按代价升序排列,求最小代价:

#include<vector>
#include<algorithm>
using namespace std;

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 返回最小的花费代价使得这n户人家连接起来
     * @param n int n户人家的村庄
     * @param m int m条路
     * @param cost intvector<vector<>> 一维3个参数,表示连接1个村庄到另外1个村庄的花费的代价
     * @return int
     */
    int miniSpanningTree(int n, int m, vector<vector<int> >& cost) {
        // write code here
        vector<int> parent;
        parent.resize(n+1, 0); //初始化parent,存放当前下标节点所指向节点

        sort(cost.begin(), cost.end(), MyCompare()); //按cost升序排列

        int minCost = 0, a, b;
        for (int i = 0; i < m; i++)
        {
            a = Find(parent, cost[i][0]);
            b = Find(parent, cost[i][1]);

            if (a != b)
            {
                parent[a] = n;
                minCost += cost[i][2]; //累加代价
            }
        }
        return minCost;
    }
    int Find(vector<int> parent, int f)
    {
        while (parent[f] > 0) //循环寻找最后最后一个指向节点!!!
            f = parent[f];
        return f;
    }
};
class MyCompare //自定义谓词将边集数组按cost排序
{
public:
    bool operator()(vector<int> a, vector<int> b)
    {
        return a[2] < b[2];
    }
};

普利姆算法像是走一步看一步的思维方式,逐步生成最小生成树。而克鲁斯卡尔算法则更具有全局意识,直接从最短权值入手,寻找最后答案。

最短路径

针对路劲决策问题,在网图与非网图中,最短路径的含义不同,在非网图中没有边上权值,所谓最短路径,其实就是指两顶点之间经过的边数最少的路径;对于网图来说,最短路径,是指两顶点之间经过的边上权值之和最少的路径,第一个顶点是源点,最后一个顶点是终点。研究网图更有实际意义。

  • 迪杰特斯拉(Dijkstra)算法,这是一个按照路径递增的次序产生最短路径的算法。利用邻接矩阵,创建一个final[]列表用于标记顶点最短路径,P[v]的值为前驱顶点下标,D[v]表示最短路径长度和。(P261)
#include<vector>
#include<iostream>
using namespace std;

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     *
     * @param n int 顶点数
     * @param m int 边数
     * @param graph intvector<vector<>> 一维3个数据,表示顶点到另外一个顶点的边长度是多少​
     * @return int
     */
    int findShortestPath(int n, int m, vector<vector<int> >& graph) {
        // write code here
        vector<vector<int>> Matirx(n + 1, (vector<int>(n + 1, INT_MAX / 3)));
        for (auto& node : graph)
        {
            Matirx[node[0]][node[1]] = min(Matirx[node[0]][node[1]], node[2]);//初始化临接矩阵,重复节点时取最小
        }
        
        vector<bool> final(n + 1, false); //记录节点是否已被纳入最小路径
        vector<int> path(n + 1, -1); //初始化路径上节点为 -1

        vector<int> minDistance(n + 1, INT_MAX / 3); //分配n+1空间,保存最短路径
        for (int i = 1; i <= n; i++)//初始化 minDistance,与节点1所直接连接节点距离
        {
            minDistance[i] = Matirx[1][i];
        }

        minDistance[1] = 0;
        final[1] = true;
        int lastIn = 1; //已纳入最短路径节点

        for (int i = 1; i < n; i++) //主循环,每次找到顶点1到顶点v的最短路径
        {
            int min = INT_MAX / 3; //当前所知离v1顶点最近距离
            for (int w = 1; w <= n; w++) //寻找离V1最近顶点
            {
                if (!final[w] && minDistance[w] < min)
                {
                    lastIn = w;
                    min = minDistance[w];
                }
            }
            final[lastIn] = true; //找到当前最短路径顶点,置为1
            for (int j = 1; j <= n; j++)
            {
                if (!final[j] && min + Matirx[lastIn][j] < minDistance[j])
                {
                    minDistance[j] = min + Matirx[lastIn][j]; //更新当前路径长度
                    path[j] = lastIn; //设置最短路径上顶点
                }
            }
        }
        return minDistance[n];
    }
};


int main()
{
    Solution a;
    vector<vector<int>> number = { {1,2,2},{1,4,5},{2,3,3},{3,5,4},{4,5,5} };
    cout << a.findShortestPath(5, 5, number) << endl;
    system("pause");
}
  • 佛洛伊德(Floyd)算法,定义D矩阵代表顶点到顶点的最短路径权值和,P代表对应顶点最小路径的前驱矩阵。抛开单点局限思维方式,巧妙运用矩阵的变换。

D 0 [ V ] [ M ] = m i n { D − 1 [ V ] [ M ] , D − 1 [ V ] [ 0 ] + D − 1 [ 0 ] [ w ] } D^0[V][M]=min\{D^{-1}[V][M],D^{-1}[V][0] + D^{-1}[0][w] \} D0[V][M]=min{D1[V][M],D1[V][0]+D1[0][w]}

迪杰斯特拉算法更强调单源点查找路径的方式,比较符合我们的正常思路,容易理解原理,代码稍显复杂。而佛洛伊德算法则抛弃单点的局限思维,运用矩阵的变换,用最清爽的代码实现了多顶点间最短路径求解的方案,原理理解有难度,但算法编写很简洁。

拓扑排序

以上都是有环图的应用,接下来是无环图的应用,即图中是没有环路的。

AOV网(Activity On Vertex):在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网。

设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列V1,V2…Vn,满足若从顶点Vi到Vj有一条路径,则在顶点序列中顶点Vi必须在顶点Vj之前。则我们称这样的顶点序列为一个拓扑序列

算法中运用的辅助数据结构——栈,用于存储处理过程中入度为0的顶点,为了避免每个查找时都要去遍历顶点表找出没有入度为0的顶点。(P270)

关键路径

拓扑排序主要是为解决一个工程能否顺序进行的问题,为解决工程完成需要的最短时间问题,我们需要找出关键路径。

AOE(Activity On Edge Network):在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图表示活动的网称为AOE网。将AOE网中没有入边的顶点称为始点或源点,没有出边的顶点叫做终点或者汇点

把各个路径上各活动持续的时间之和称之为路径长度,从源点到汇点具有最大长度的路径叫做关键路径,在关键路径上的活动叫关键活动

关键路径算法中运用数据结构为——领接表,求事件的最早发生时间、最晚发生时间、最早开工时间、最晚开工时间。(P280)

有向无环图时常应用于工程规划中,通过拓扑排序的方式判断工程是否能进行,分析出是否存在环;用关键路径的算法求出工程完成所需的最短时间问题,提高关键活动效率变可缩短工程完成周期。详细的算法代码实现参考大话数据结构指定页码内容。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值