图论之图与图的遍历(C++) -- 每个顶点都是主角

图论之图与图的遍历(C++) – 每个顶点都是主角

  聊图要先聊聊树,一棵树需要有一个根,别的节点都是在根的基础上散发开的,根据节点与根之间和父子节点之间不同的关系形成了很多特殊的树;其实图(连通图)也可以看作是一棵树,无非就是随便选取一个顶点作为根就好了,不同的是树中有根和父子节点的概念,在图中没有,每个顶点都可以是根,也就是说图有着更广泛的性质;
  一个图主要的研究对象就是顶点和边,也就是图中有多少个顶点,各个顶点是否相连,相连的边又分是否又方向,如同公路有双向公路和单向公路之分,边还可以有权值,如同两个地点直接距离的大小;基础的图就是无向无权值图,只考虑两个顶点是否有相连的边即可,因为不一定所有的顶点都相连(直接或间接),这时对于图中的顶点就有连通分量的说法,所谓连通分量这个分量中的顶点直接有直接或间接的路径到达彼此;
  图的存储方式有两种,分别为邻接矩阵和邻接表,邻接矩阵就是用一个二维数组进行记录,邻接表则用链表记录相连的顶点,这两种方式对应数据量的不同和所进行的操作不同,性能也不同;
  以下示例均基于无向无权图,且均给出了建图的类,其中有插入边的方法insertEdge,打印图的方法showGraph和遍历图中一个顶点的连接的所有顶点的迭代器EdgeIterator~~

图的构造 – 邻接矩阵

  邻接矩阵比较简单的玩法就是以数组下标为顶点,因为是二维数组,每个下标均存有一个一维数组,这个数组对应的下标中记录true或false表示两个顶点之间是否有连接

#include <iostream>
#include <vector>
#include <cassert>

using namespace std;

// 图(Graph)
// 邻接矩阵:稠密图的小伙伴
// 无权值玩法
class GraphByMatrix {

private:

    // 矩阵(图),因为没有权值,只要知道有连接或没连接就行,所以用bool
    vector<vector<bool>> g;
    // 图的顶点数
    int vNum;
    // 图的边数
    int eNum;
    // 是否有向
    bool directed;

public:

    // 构造函数
    GraphByMatrix(int vNum, bool directed) {
        assert(vNum >= 0);

        this->vNum = vNum;
        this->directed = directed;
        // 边数初始为0
        this->eNum = 0;
        // 图初始化
        g = vector<vector<bool>>(vNum,vector<bool>(vNum,false));
    }

    // 析构函数
    ~GraphByMatrix() {}

    // 返回顶点个数
    int getVNum() {
        return vNum;
    }
    // 返回边数
    int getENum() {
        return eNum;
    }
    // 插入边
    void insertEdge(int from, int to) {
        assert(from >= 0 && from < vNum);
        assert(to >= 0 && to < vNum);

        // 顶点自己到自己或边已存在,直接返回
        if(from == to || g[from][to] == true)
            return;

        g[from][to] = true;
        // 无向,则反过来也要连起来
        if(!directed) {
            g[to][from] = true;
        }
        // 边的数量+1
        eNum++;
    }

    // 遍历图中所有的边并打印
    void showGraph() {
        if(eNum == 0) {
            cout << "empty graph!!!" << endl;

            return;
        }
        // 展示用
        cout << "\t";
        // 输出顶点信息,用于展示
        for(int i; i < vNum; i++)
            cout << i << "\t";
        cout << endl;
        // 遍历,输出各顶点间是否相连,用true和false直接表示,输出后会是1和0
        for(int i=0; i < vNum; i++) {
            // 输出顶点信息,用于展示
            cout << i << "\t";
            for(int j=0; j < vNum; j++) {
                cout << g[i][j] << "\t";
            }
            cout << endl;
        }
    }

    // 遍历图中某个顶点与别的顶点的连接情况,与上面showGraph方法不同,这个是获取单个顶点的连接情况,不是获取所有的
    // 最简单的办法是将图的变量公有化,这样遍历起来很方便,但带来的问题是外部可以随便修改图,还有一个方式是通过公有方法返回一个复制的图,这样带来的时间和空间消耗巨大
    // 较好的方式是以下面的方式自制迭代器
    class EdgeIterator {
    private:
        // 用引用不用指针,在初始化时用显式初始化,这样就只能用不能改,这样能调用到graph中的私有变量
        GraphByMatrix &graph;
        int v;
        int tail;
    public:
        // 构造函数
        EdgeIterator(GraphByMatrix& g, int v):graph(g) {
            assert(v >= 0 && v < graph.vNum);

            this->v = v;
            // 遍历从下标0开始,因为开始时会调用next(),next中会对tail加1,所以初始化为-1
            this->tail = -1;
        }
        ~EdgeIterator() {}

        // 开始遍历,并返回第一个连接的顶点
        int begin() {
            // 重置遍历下标
            tail = -1;

            return next();
        }
        // 获取一个连接的顶点
        int next() {
            // 从之前遍历到的下标开始继续获取连接的顶点
            for(++tail; tail < graph.getVNum(); tail++) {
                if(graph.g[v][tail]) {

                    return tail;
                }
            }

            // 没有找到有连接的顶点,返回-1
            return -1;
        }
        // 判断是否遍历结束
        bool end() {

            return tail >= graph.getVNum();
        }

    };

};

图的构造 – 邻接表

  邻接表的思路是用一个数组下标作为各个顶点,每个数组元素存放的是一个链表,链表中的成员就与改下标顶点相连的顶点,所以链表中的元素存放的不是像邻接矩阵中的true或false,而是顶点;示例还是用vector嵌套vector的结构,因为vector是动态的,被嵌套的vector在一开始不分配空间,随着连接顶点增加再增加空间,与链表添加过程相似,可以用链表替代~~

#include <iostream>
#include <vector>
#include <cassert>

using namespace std;

// 图(Graph)
// 邻接表:稀疏图的小伙伴
// 无权值玩法
class GraphByChain {

private:

    // 邻接表(图),因为没有权值,这里可以直接存放顶点的值来表示与哪个顶点相连,因为vector是动态的,也有链表的特性,所以这里也用vector,可以改成链表
    vector<vector<int>> g;
    // 图的顶点数
    int vNum;
    // 图的边数
    int eNum;
    // 是否有向
    bool directed;

public:

    // 构造函数
    GraphByChain(int vNum, bool directed) {
        assert(vNum >= 0);

        this->vNum = vNum;
        this->directed = directed;
        // 边数初始为0
        this->eNum = 0;
        // 图初始化
        g = vector<vector<int>>(vNum,vector<int>());
    }

    // 析构函数
    ~GraphByChain() {}

    // 返回顶点个数
    int getVNum() {
        return vNum;
    }
    // 返回边数
    int getENum() {
        return eNum;
    }
    // 插入边
    void insertEdge(int from, int to) {
        assert(from >= 0 && from < vNum);
        assert(to >= 0 && to < vNum);

        // 顶点自己到自己,直接返回
        if(from == to)
            return;
        // 边已存在,直接返回
        for(int i=0; i < g[from].size(); i++) {
            if(g[from][i] == to) {

                return;
            }
        }

        g[from].push_back(to);
        // 无向,则反过来也要连起来
        if(!directed) {
            g[to].push_back(from);
        }
        // 边的数量+1
        eNum++;
    }

    // 遍历图中所有的边并打印
    void showGraph() {
        if(eNum == 0) {
            cout << "empty graph!!!" << endl;

            return;
        }
        // 遍历,输出链表的形式,也就是每个顶点与别的顶点的连接链条
        for(int i=0; i < vNum; i++) {
            cout << i;
            for(int j=0; j < g[i].size(); j++) {
                cout << " - " << g[i][j];
            }
            cout << endl;
        }
    }

	// 迭代器类
    class EdgeIterator {
    private:
        // 用引用不用指针,在初始化时用显式初始化,这样就只能用不能改,这样能调用到graph中的私有变量
        GraphByChain &graph;
        int v;
        int tail;
    public:
        // 构造函数
        EdgeIterator(GraphByChain& g, int v):graph(g) {
            this->v = v;
            // 遍历从下标0开始,因为调用next()会+1,所以先设为-1
            this->tail = -1;
        }
        ~EdgeIterator() {}

        // 开始遍历,并返回第一个连接顶点
        int begin() {
            // 重置遍历下标
            tail = -1;

            return next();
        }
        // 获取一个连接顶点
        int next() {
            tail++;
            if(tail < graph.g[v].size()) {

                return graph.g[v][tail];
            }

            // 没有找到有连接的顶点,返回-1
            return -1;
        }
        // 判断是否遍历结束
        bool end() {

            return tail >= graph.g[v].size();
        }

    };

};

  通过两种图中打印图的方法showGraph和图中一个顶点的连接顶点迭代器可以看出,对于邻接矩阵,遍历一次图的时间复杂度是O(v^2)(v代表vertex,顶点数),获取一个顶点的连接情况需要遍历一次所有顶点,时间复杂度是O(v);而对于邻接表,因为一个顶点相连的顶点是以链表的形式记录的,所以直接通过链表的节点数就知道与该顶点相连的顶点个数,也就是边数,这样获取一个顶点连接情况的时间复杂度是O(e)(e代表edge,边数),遍历一次图的时间复杂度是O(n*v);

图的连通分量 – 深度优先

  给定一堆顶点,再给定一堆这些顶点的边,不一定每个顶点都能通过直接或间接的方式连接,也就是不一定任意两个顶点间都存在路径,这时就有了找出哪些顶点是相互可达到的意义了,这些相互可到达的顶点称为连通分量;
  找连通分量的过程可以看作是将有边相连的顶点都访问一遍的过程,这就有两种访问方式了,一种是深度优先,深度优先使用递归的方式,从一个顶点开始往其连接的顶点走,然后再走其连接的顶点的连接顶点走,走到没路了,再回过头来走,与递归的方式契合;在这个过程中因为需要知道哪些顶点是连通的,就要记录一次递归能访问到哪些顶点,所以需要两个数组,一个记录这个顶点是否已访问,一个记录顶点所属的连通分量;
  以下的示例适用于上面的邻接矩阵和邻接表,进行遍历时用到了图中的枚举器EdgeIterator,因为两种图的枚举器提供的调用方法一致,所以无需考虑用的是那种图~~

#include <iostream>
#include <cassert>
#include <queue>

using namespace std;

// 图的连通分量寻找与划分
template <typename Graph>
class DiscoverClosedGraph {

private:

    // 图引用,只用不改
    Graph &graph;
    // 记录顶点是否已访问
    bool* visited;
    // 记录顶点所属连通分量
    int* vGraphIndex;
    // 连通分量个数(图的个数)
    int gCount;

    // 深度优先遍历查找连通分量,深度优先搭配递归实现
    void DFS(int v) {
        // 将当前顶点设为已访问
        visited[v] = true;
        // 设置当前顶点所属连通分量
        vGraphIndex[v] = gCount;

        // 在进行Graph调用时,默认是调用import中的类,在前面加了typename就是指用上面自定义类型的类
        // 用图中设置的遍历器进行遍历,因为邻接矩阵和邻接表有相同类型的遍历器,所以直接遍历即可,不用考虑图的类型
        typename Graph::EdgeIterator ei(graph,v);
        for(int i = ei.begin(); !ei.end(); i = ei.next()) {
            if(!visited[i]) {
                DFS(i);
            }
        }
    }

public:

    // 构造函数,图要显式初始化
    DiscoverClosedGraph(Graph& g):graph(g) {
        visited = new bool[graph.getVNum()];
        vGraphIndex = new int[graph.getVNum()];
        gCount = 0;
        // 初始化
        for(int i=0; i < graph.getVNum(); i++) {
            visited[i] = false;
            vGraphIndex[i] = 0;
        }

        // 获取连通图
        for(int i=0; i < graph.getVNum(); i++) {
            // 有未访问的顶点时,说明有一个新的连通分量,连通分量序号+1,进行该连通分量的连通遍历
            if(!visited[i]) {
                gCount++;
                // 这里可以用深度优先或广度优先实现
                //DFS(i);
                // 这个玩法暂时有bug
                BFS(i);
            }
        }
    }

    // 析构函数
    ~DiscoverClosedGraph() {
        delete[] visited;
        delete[] vGraphIndex;
    }

    // 返回连通分量个数
    int getGraphCount() {

        return gCount;
    }

    // 查询两顶点是否相连
    bool isConnected(int v1, int v2) {
        assert(v1 >= 0 && v1 < graph.getVNum());
        assert(v2 >= 0 && v2 < graph.getVNum());

        return vGraphIndex[v1] == vGraphIndex[v2];
    }

};

图的连通分量 – 广度优先

  广度优先是选一个顶点,先走完这个顶点所有连接的顶点后,再选取一个这个顶点连接的顶点进行相同操作,为了满足先来后到,需要用到队列;这里只给出广度优先的方法,将上面的DFS方法改为下面的BFS方法即可;

// 广度优先遍历查找连通分量,广度优先搭配队列实现
void BFS(int v) {
    queue<int> q;
    q.push(v);
    // 这里用于设置第一个顶点,因为后续的已访问在下面进行连接顶点遍历加入队列时已经设置
    // 将当前顶点设为已访问
    visited[v] = true;
    // 设置当前顶点所属连通分量
    vGraphIndex[v] = gCount;
    while(!q.empty()) {
        int qV = q.front();
        q.pop();
        // 将队列中弹出的当前顶点作为待遍历连接顶点的顶点
        typename Graph::EdgeIterator ei(graph,qV);
        // 若连接的顶点未访问,则压入队列
        for(int i = ei.begin(); !ei.end(); i = ei.next()) {
            if(!visited[i]) {
                // 这里要进行顶点已访问设定,如果不设定,在弹出队列前会被认为未访问而重复加入队列,造成重复判断
                visited[i] = true;
                vGraphIndex[i] = gCount;
                q.push(i);
            }
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值