数据结构(六)图

耗尽温柔 提交于 2019-11-29 14:20:48
  • 术语
  • 实现
  • 算法

一、概述

存在对应关系就连边,邻接关系 顶点与顶点之间的关系

参与定义邻接关系的每个顶点,与这个邻接关系的关系称作关联关系 顶点与相关的某条边的关系

 

本章忽略下面这种边

 

 无向图/有向图

 

 所有边均无方向的图,即无向图undigrapy

 

 

 

反之,有向图digraph中均为有向边directed edge,u、v分别称作边(u, v)的尾(tail)、头(head) 

 

混合图

 

 

 

 路径/环路

 

 有向无环图DAG,在有向图中不包含任何的环路

 

 简单路径和简单环路,都不包含重复的点

欧拉环路,覆盖了图中所有的点

哈密尔顿环路,经过所有顶点,且只经过一次的环路

 

二、邻接矩阵

在计算机中如何以数据结构的方式实现图?

 

Graph模板类

template <typename Tv, typename Te> class Graph { // 顶点类型、边类型
private:
    void reset() { // 所有顶点、边的辅助信息
        for (int i = 0; i < n; i++) { // 顶点
            status(i) = UNDISCOVERED; dTime(i) = fTime(i) = -1;
            parent(i) = -1; priority(i) = INT_MAX;
            for (int j = 0; j < n; j++) // 边
                if (exists(i, j)) status(i, j) = UNDETERMINED;    
        }
    }
public: /*...顶点操作、边操作、图算法:无论如何实现,接口必须统一*/
}; //Graph

 

邻接矩阵与关联矩阵

邻接矩阵

顶点和点的

顶点之间存在关联为1,不存在关联为0

对角线上是自边

 

 

关联矩阵

行为顶点,列为边

顶点与对应的边之间是否存在关联关系,存在关联关系记作1

 

顶点类的实现

typedef enum{UNDISCOVERED, DISCOVERED, VISITED} VStatus; // 顶点的三种状态
template <typename Tv> struct  Vertex{ // 顶点对象(并未严格封装)
    Tv data; int inDegree, outDegree; // 数据、出入度
    VStatus status; // (如上三种)状态
    int dTime, fTime; // 时间标签
    int parent; // 在遍历树种的父节点
    int priority; // 在遍历树中的优先级(最短路径、极短跨边等)
    Vertex (Tv const & d): // 构造新顶点
        data(d), inDegree(0), outDegree(0), status(UNDISCOVERED), // 入度、出度都为0,初始被置为UNDISCOVERED状态
        dTime(-1), fTime(-1), parent(-1),
        priority(INT_MAX){}
};

 

边类型的实现

typedef
enum {UNDETERMINED, TREE, CROSS, FORWARD, BACKWARD}
EStatus;

template <typename Te> struct  Edge{ // 边对象(并未严格封装}
    
    Te data; // 数据
    int weight; // 权重
    EStatus staus; // 类型
    Edge(Te const & d, int w): // 构造新边
        data(d), weight(w), status(UNDETERMINED){}
};

 

基于邻接矩阵实现图结构的一种可行方式

template <typename Tv, typename Te> class GraphMatrix : public Graph<Tv, Te> {
private:
    Vector<Vertex<Tv>> V; // 顶点集
    Vector<Vector<Edge<Te>*>> E; // 边集
public:
    /*操作接口:顶点相关、边相关、...*/
    GraphMatrix() { n = e = 0; } // 构造
    ~GraphMatrix() { // 析构
        for (int j = 0; j < n; j++)
            for (int k = 0; k < n; k++)
                delete E[j][k]; // 清除所有动态申请的边记录
    }
};

 

 顶点集,是顶点构成的向量

边集,是边的向量组成的向量

 

顶点操作

Tv & vertex(int i) { return V[i], data; } // 数据
int inDegree(int i) { return V[i].inDegree; } // 入度
int outDegree(int i) { return V[i].outDegree; } // 出度
VStatus & status(int i) { return V[i].status; } // 状态
int & dTime(int i) { return V[i].dTime; } // 时间标签dTime
int & fTime(int i) { return V[i].fTime; } // 时间标签fTime
int & parent(int i) { return V[i].parent; } // 在遍历树中的父亲
int & priority(int i) { return V[i].priority; } // 优先级数

 

对于任意顶点i,如何枚举其所有的邻接顶点neighbor?

int nextNbr(int i, int j){// 若已枚举至邻居j,则转向下一邻居
    while ((-1 < j) && !exists(i, --j)); // 逆向顺序查找,O(n)
    return j;
} // 改用邻接表可提高置O(1+outDegree(i))

 

int firstNbr(int i) {
    return nextNbr(i, n);
} // 首个邻居

顶点n作为有效的邻居,该顶点不存在,是虚拟出来的,假想的哨兵

 

边操作

bool exists(int i, int j) { // 判断边(i, j)是否存在
    return (0 <= i) && (i < n) && (0 <= j) && (j < n)
        && E[i][j] != NULL; // 短路求值
} // 以下假定exists(i, j)... 

 

Te & edge(int i, int j) // 边(i, j)的数据
{
    return E[i][j]->data;
} // O(1)

 

边插入

void insert(Te const& edge, int w, int i, int j){ // 插入(i, j, w)
    if (exists(i, j)) return; // 忽略已有的边
    E[i][j] = new Edge<Te>(edge, w); // 创建新边
    e++; // 更新边计数
    V[i].outDegree++; // 更新关联顶点i的出度
    V[j].inDegree++; // 更新关联顶点j的入度
}

 

边删除

Te remove(int i, int j) { // 删除顶点i和j之间的联边(exists(i, j))
    Te eBak = edge(i, j); // 备份边(i, j)的信息
    delete E[i][j]; E[i][j] = NULL; // 删除边(i, j)
    e--; // 更新边计数
    V[i].outDegree--; // 更新关联顶点i的出度
    V[j].inDegree--; // 更新关联顶点j的入度
    return eBak; // 返回被删除边的信息
}

 

顶点插入

int insert(Tv const & vertex){ // 插入顶点,返回编号
    for (int j = 0; j < n; j++) E[j].insert(NULL); n++; // 1
    E.insert(Vector<Edge<Te>*>(n, n, NULL)); // 2 3
    return V.insert(Vertex<Tv>(vertex)); // 4
}

 

 

 

顶点删除

Tv remove(int i){ // 删除顶点及其关联边,返回该顶点信息
    for(int j=0; j<n; j++)
        if(exists(i, j)) // 删除所有出边
            {delete E[i][j]; V[j].inDegree--;}
    E.remove(i); n--; // 删除第i行
    for(int j=0; j<n; j++)
        if(exists(j, i)) // 删除所有入边及其第i列
        {delete E[j].remove(i); V[j].outDegree--;}
    Tv vBak = vertex(i); // 备份顶点i的信息
    V.remove(i); // 删除顶点i
    return vBak; // 返回被删除顶点的信息
} 

 

优点

 

 缺点

 

 不相邻的边不能相交

不是平面图:

 

 

 

 

 

三、广度优先搜索

化繁为简

遍历使非线性结构转为线性结构

 

 

算法

 

层次

 

 

 以与S点的距离为依据,将所有点划分为若干等价类

支撑树

与树的层次遍历同

 

实现

Graph::BFS()

template <typename Tv, typename Te> // 顶点类型、边类型
void Graph<Tv, Te>::BFS(int v, int & clock) {
    Queue<int> Q; status(v) = DISCOVERED; Q.enqueue(v); // 初始化
    while(!Q.empty()){ // 反复地
        int v = Q.dequue();
        dTime(v) = ++clock; // 取出队首顶点v,并
        for (int u = firstNbr(v); -1 < u; u = nextNbr(v, u)) // 考察v的每一邻居u
            /*视u的状态,分别处理*/
            if(UNDISCOVERED == status(u)){ // 若u尚未被发现,则
                status(u) = DISCOVERED; Q.enqueue(u); // 发现该顶点
                status(v, u) = TREE; parent(u) = v; // 引入树边
            }
            else // 若u已被发现(正在队列中),或者甚至已访问完毕(已出队列),则
                status(v, u) = CROSS; // 将(v, u)归类于跨边
            status(v) = VISITED; // 至此,当前顶点访问完毕
    }
}

 

实例

无向图

 

 

 

 

 

 

 

 

 并非每幅图都只包含一个连通域:

 

 

 如何使bfs搜索足以覆盖整幅图,而不是其中某个连通域

template <typename Tv, typename Te> // 顶点类型、边类型
void Graph<Tv, Te>::bfs(int s){ // s为起始点
    reset(); int clock = 0; int v = s; // 初始化
    do // 逐一检查所有顶点,一旦遇到尚未发现的顶点
        if (UNDISCOVERED == status(v)) // 累计
            BFS(v, clock); // 即从该顶点出发启动一次BFS
    while (s != (v = (++v%n)));
    // 按序号访问,故不漏不重
} // 无论共有多少连通/可达分量...

只有顶点通过检验后,才进行BFS搜索

 

最短路径

顶点之间的距离,是最短通路的长度

 

 BFS所给出的顶点序列,按照到起点的距离,按照非降次序单调排列

所有顶点被发现和访问的过程如下:

 

 

 

 

 

 BFS树中,每个顶点与S之间的通路,恰好就是原图中两个顶点间的最短通路

 

四、深度优先搜索

算法

 

 

 

 上面四个点,都是随机选择,第四个点现在有控制权,进行扫描

已经被访问的边不会被采用,且做出标记

 

 已经没有任何邻居尚未访问,进行回溯

 回溯到第三个点,也没有任何为访问的邻居

回溯到第二个点,有未访问的邻居,将控制权转交给该邻居

 

 不断重复上述步骤

 

 遍历完毕,得到BFS树

 

实现

template <typename Tv, typename Te> // 顶点类型、边类型
void Graph<Tv, Te>::DFS(int v, int & clock) {
    dTime(v) = ++clock; status(v) = DISCOVERED; // 发现当前顶点v
    for (u = firstNbr; -1 < u; u = nextNbr(v, u)) // 枚举v的每一邻居
    /*视u的状态,分别处理*/
        switch (status(u)) // 视其状态分别处理
        {
        case UNDISCOVERED: // u尚未发现,意味着支撑树可在此拓展
            status(v, u) = TREE; parent(u) = v; DFS(u, clock); break; // 递归
        case DISCOVERED: // u已知被发现但尚未访问完毕,应树被后代指向的祖先
            status(v, u) = BACKWARD; break;
        default: // u已访问完毕(VISITED, 有向图),则视承袭关系分为前向边或跨边
            status(v, u) = dTime(v) < dTime(u) ? FORWARD : CROSS; break;
        }
    /*与BFS不同,含有递归*/
    status(v) = VISITED; fTime(v) = ++clock; // 至此,当前顶点v方告访问完毕
}

 

实例(无向图)

第一秒,a被发现,大写

第二秒,b被发现

 

 第三秒,发现c

 

 第四秒,发现f

 

 第5秒发现h

 

 第6秒发现g

 

 第7秒发现j,但是没有任何有效的邻居

 

 第8秒,沿此前的树边(TREE )回溯

 

 第9秒,发现顶点i

 

 第10秒,顶点d

 

 d有边通往g,但是该边处于discovered状态,上面switch分支的第二种情况

d通往g的边,将会被标记为回边(BACKWARD)

 

 同理,d通往g的边也会被标记

 

 标记完d发出的所有边之后,进行回退

第11秒,回退到i

 

 第12秒,从i回退到g

 

 标记g到f的边

 

 第13秒,g回溯到h

 

 第14秒,h回溯到f

 

 第15秒,回溯到c

 

 第16秒,回溯到b

 

 第17秒,发现e

 

 标记e到a

 

 第18秒,回退到顶点b

 

 第19秒,回退到a

 

 第20秒,a完成了对自己的访问,回到了最初的起点,遍历过程结束

 

 

实例(有向图)

第1秒,发现a

 

 第2秒,发现并访问顶点b

 

 第3秒,发现并访问顶点c,c没有发出任何有效的边

 

 第4秒,c结束,标记fTime标签4,回溯到b

 

 b的所有邻居都已经扫描并处理完毕,标记fTime,回溯到a

 

 a扫描其他邻接顶点,比如c,但c已经访问完毕了(visited)

比较两者的时间标签,a更早被发现,即1小于3

因此,a到c有向边标记为向前边(FORWARD),即祖先指向后代

 

 第6秒,a发现f

 

 第7秒,f发现g

 

 g的第一个邻居顶点a,a已经被发现(discovered),后代指向先辈,后向边或回边

 

 发现回边,就有回路,如上图afg之间有回路

考察g下一个邻居,顶点c,c是visited状态

在整个遍历树中,g和c没有祖先后代的直系血缘关系,因此是跨边(CROSS)

 

 g的所有邻居都已经访问并处理完毕,g访问终止,回退到父顶点f

 

 f所有邻居也处理完毕,继续回溯到a

 

 a的邻接顶点也都处理完毕,结束遍历过程

 

一共用了10秒 遍历了上面5个顶点组成的子图

即从顶点a出发可以达到的区域,称为可达区域

d和e没有到达

 

第11秒从d出发

 

 d到a是跨边

 

 12秒发现顶点e

 

 e到f标记跨边

 

 e访问结束

 

 14秒,结束对d的访问

 

 整个图遍历访问结束

 

下面的粗箭头是两颗遍历树,构成了整体的遍历森林

对未被采纳的边进行了分类:跨边,前向边,后向边

标记了两个时间标签:dTime,fTime

 

 

括号引理

 

 没有直系血缘关系的顶点,比如b和f,活跃期是不重叠的

可用来判断顶点间的直系血缘关系

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!