算法学习——图之无权图

前言

经过,一段时间的学习,终于接触到了 这一重要的数据结构。与此同时,图在我们的生活中也起到了至关重要的作用。例如,网易云音乐的推荐、最短路径的查找等,这些场景都能利用图得到解决或者优化。

定义

维基百科定义:在数学上,一个图(Graph)是表示物件与物件之间的关系的方法,是图论的基本研究对象。
图由节点(vertex)和(edge)组成。由边的有无方向,我们又可以将图分为有向图无向图。同时,根据边上权值的有无,还可以分为有权图无权图。这一篇我们暂时先学习一下无权图的操作。

简单图

一个图如果满足以下两个条件,那么它就是简单图。
1. 没有两条边,它们所关联的两个点都相同(在有向图中,没有两条边的起点终点都分别相同);
2. 每条边所关联的是两个不同的顶点
简单图
非简单图会产生上述两种边:自环边平行边,我们之后给的数据生成图时会避免生成这两种边,同时我们也可以通过代码防止出现。

邻接矩阵

初学图,定义还确实有点多。。邻接矩阵用0和1表示两个节点之间是否存在连接,一般适合于稠密图。有向图图示如下:
邻接矩阵
无向图图示如下:
邻接矩阵

邻接表

邻接表就是与当前节点相连接的节点编号,就像链表一样存储,它对于有向图与无向图的表示是一致的。邻接表适合于稀疏图。
有向图与无向图如下图:
有向
无向

代码实现

由于代码过多,我贴一些主要代码。我们主要是实现无向图两点之间的路径查找。

稠密图DenseGraph

稠密图我们使用邻接矩阵实现,好处就是我们使用邻接矩阵时就已经自动忽略平行边了,因为矩阵只能表示两者之间的联系。
首先,我们用vector来表示二维矩阵,它有以下属性:

private:
    int n, m;//点数和边数
    bool directed;//图是否是有向图
    vector< vector<bool> > g;//二维矩阵

//初始化
public:
    DenseGraph(int n, bool directed){
        this->n = n;
        this->m = 0;
        this->directed = directed;
        //创建n*n矩阵
        for(int i = 0; i < n; i++)
            g.push_back(vector<bool>(n, false));
    }

添加v与w之间的边,当然我们需要先判断一下原来是否有边,当然只要看那个位置是不是1就行了。

    //建立边,忽略平行边
    void addEdge(int v, int w){

        assert(v >= 0 && v < n);
        assert(w >= 0 && w < n);

        if(hasEdge(v, w))
            return;

        g[v][w] = true;

        //判断是否是有向图
        if(!directed)
            g[w][v] = true;

        m++;
    }

    bool hasEdge(int v, int w){
        assert(v >= 0 && v < n);
        assert(w >= 0 && w < n);
        return g[v][w];
    }

设置迭代器:

    public:
        adjIterator(DenseGraph &graph, int v): G(graph){
            this->v = v;
            this->index = -1;
        }

        int begin(){
            index = -1;
            return next();
        }

        int next(){
            for(index += 1; index < G.V(); index++)
                if(G.g[v][index])
                    return index;

            return -1;
        }

        bool end(){
            return index >= G.V();
        }

    };

稀疏表SparseGraph

稀疏表用邻接表实现。图之间联系也用vector表示,属性如下:

private:
    int n, m;
    bool directed;
    vector< vector<int> > g;

public:
    SparseGraph(int n, bool directed){
        this->n = n;
        this->m = 0;
        this->directed = directed;
        //初始化 邻接表为空
        for(int i = 0; i < n; i++)
            g.push_back(vector<int>());
    }

因为邻接表可能在插入的时候存在平行边,我这里使用了之前学到的二分搜索树进行搜索,避免平行边插入,当然我们的样例保证没有平行边,这里只是我一个强迫症。。

    void addEdge(int v, int w){
        assert(v >= 0 && v < n);
        assert(w >= 0 && w < n);

        if(!judgeParallelEdges(g[w], v))
            return;

        g[v].push_back(w);
        //处理自环边及平行边
        if(v != w && !directed )
            g[w].push_back(v);

        m++;
    }

    //判断两点之间是否已经存在连接
    //存在返回False,不存在返回True
    bool judgeParallelEdges(vector<int> v, int w){
        BST<int,int> bst = BST<int,int>();
        for(int i = 0; i < v.size(); i++){
            int key = v[i];
            int value = key;
            bst.insert(key, value);
        }

        if(bst.contain(w))
            return false;

        return true;
    }

获取路径

这里获取路径的方法和我们之前所学的树的遍历方法是类似的,我们首先采用深度优先遍历dfs(Depth First Search)。首先选取一个起点0,查找它相连的节点1,不断向下搜索子节点,直到搜索到或者搜索到底;然后重新回到开始的那个点0,再搜索2,不断循环。
Dfs
我们需要存储节点是否被访问,同时还需要存储它上一个节点,我们用from存储,以便搜索到之后找到路径。完成初始化后,我们就能得到v到所有节点的路径。

private:
    Graph &G;
    int s;//源节点
    bool *visited;//是否被访问
    int *from;

    void dfs(int v){
        visited[v] = true;
        //遍历v相邻的节点
        typename Graph::adjIterator adj(G, v);
        for(int i = adj.begin(); !adj.end(); i = adj.next()){
            if(!visited[i]){
                from[i] = v;
                dfs(i);
            }
        }
    }

查找v到w,我们从后往前,通过from依次将路径顺序压入,然后顺序读出即可。

    void path(int w, vector<int> &vec){

        assert( w >= 0 && w < G.V() );

        stack<int> s;

        //放入栈中
        int p = w;
        while(p != -1){
            s.push(p);
            p = from[p];
        }

        //顺序放入vector
        vec.clear();
        while( !s.empty() ){
            vec.push_back( s.top() );
            s.pop();
        }
    }

    //w为目标节点
    void showPath(int w){

        vector<int> vec;
        path(w, vec);
        for(int i = 0; i < vec.size(); i++){
            cout<<vec[i];
            if( i == vec.size() - 1 )
                cout<<endl;
            else
                cout<<" -> ";
        }
    }

最短路径

我们刚刚实现了路径的查找,但是并不是最短的路径。就以上图为例,最后dfs的结果是0-5-3-4-6,但是其实最短的应该是0-6
于是我们就采用了广度优先搜索bfs求出无权图的最短路径。我们这里就会使用队列这个数据结构,首先将初始节点0入队,然后出队;接着将0的所有子节点入队,然后队首出队,同时队首子节点入队,不断循环。
bfs
因为我们是不断加入子节点的,同一个节点的子节点到原始节点的距离就一定相同,当然距离不可能超过节点的总个数,距离我们使用ord表示。当然也和之前一样,需要对上一个节点from进行记录。

private:
    Graph &G;
    int s;
    bool *visited;
    int *from;//上一个节点
    int *ord;//s到每一个节点的最短距离

public:
    ShortestPath(Graph &graph, int s):G(graph){

        //算法初始化
        assert( s >= 0 && s < graph.V() );

        visited = new bool[graph.V()];
        from = new int[graph.V()];
        ord = new int[graph.V()];

        for(int i = 0; i < graph.V(); i++){
            visited[i] = false;
            from[i] = -1;
            ord[i] = -1;
        }

        this->s = s;

        queue<int> q;

        //无向图最短路径算法
        q.push( s );
        visited[s] = true;
        ord[s] = 0;
        while( !q.empty() ){
            int v = q.front();
            q.pop();

            typename Graph::adjIterator adj(G, v);
            for(int i = adj.begin(); !adj.end(); i = adj.next()){
                if( !visited[i] ){
                    q.push(i);
                    visited[i] = true;
                    from[i] = v;
                    ord[i] = ord[v] + 1;//距离+1
                }
            }
        }

    }

完成初始化之后,所有节点到v的距离和上一个节点我们就形成了。然后通过from,查找v到w的最短路径。

    void path(int w, vector<int> &vec){

        assert( w >= 0 && w < G.V() );

        stack<int> s;

        //放入栈中
        int p = w;
        while(p != -1){
            s.push(p);
            p = from[p];
        }

        //顺序放入vector
        vec.clear();
        while( !s.empty() ){
            vec.push_back( s.top() );
            s.pop();
        }
    }

    //w为目标节点
    void showPath(int w){
        assert( w >= 0 && w < G.V() );

        vector<int> vec;
        path(w, vec);

        for(int i = 0; i < vec.size(); i++){
            cout<<vec[i];
            if( i == vec.size() - 1 )
                cout<<endl;
            else
                cout<<" -> ";
        }
    }

图片引用百度图片
代码实现参照liuyubobobo慕课网教程
github repository

猜你喜欢

转载自blog.csdn.net/blueblueskyz/article/details/79499983
今日推荐