图的邻接矩阵与邻接表存储方式及优缺点对比

概述

记录一些图的基本概念,以及图的两种表示方式(邻接表和邻接矩阵)的代码实现,最后总结了两种方式的优缺点,还简单介绍了十字链表和逆邻接表

图的部分基本概念(我记不住的)

1、完全图

一个无向图,任意两个顶点之间有且仅有一条边,则称为无向完全图。若一个无向完全图有 n 个顶点,那么它就有 n(n1)/2 条边。
一个有向图,任意两个顶点之间有且仅有方向相反的两条边,则称为有向完全图。若一个有向完全图有 n 个顶点,那么它就有 n(n1) 条边。

2、邻接顶点

无向图中,顶点 u 与 v 之间有一条边(u,v),则称 u 和 v 互为邻接顶点
有向图中,顶点 u 和 v 之间有一条边<u, v>,则顶点 u 邻接到顶点 v,顶点 v 邻接自顶点 u,并称边<u,v>与顶点 u、v 相关联。

3、顶点的度

顶点的度就是与它相关联的边的条数,入度表示边的终点指向顶点的个数,出度表示边的起点指向顶点的个数。

有向图中,顶点的度等于入度与出度的和。

无向图中,顶点的度 =入度 = 出度。

4、连通图:

无向图中,两个顶点之间有路径,则称这两个顶点是连通的。如果如中任意一对顶点都是连通的,则称此图为连通图

在有向图中,若任意一对顶点 vi 和 vj,都有从 vi 到 vj 的路径,则称此图为强连通图

5、生成树:

一个连通图(无向图)的最小子图称为该图的生成树。有 n 个顶点的连通图的生成树有 n - 1 条边。最小生成树就是权值和最小的生成树。

邻接矩阵表示法

在一个一维数组中存储所有的点,在一个二维数组中存储顶点之间的边的权值,以及一个布尔值标记是否是有向图,默认是无向图:

    bool        isDirected;  // 标识是否是有向图,默认为false
    vector<V>       vertex;  // 存储顶点
    vector<vector<W> > edge; // 存储边

graph.h

#ifndef GRAPH_H
#define GRAPH_H

#include <vector>
#include <cassert>
#include <iostream>
using namespace std;

// V -- 图顶点的数据类型
// W -- 图边权值的类型
template <typename V, typename W>
class GraphMatrix
{
public:
    GraphMatrix(const V* _vertex, size_t _size, bool _isDirected = false);

    // 打印图中的所有边
    void printEdge();
    // 向图中添加一条边
    void addEdge(const V& v1, const V& v2, const W& weight);

private:
    // 获取边所在的下标
    int getIndexOfVertex(const V& _vertex);

private:
    bool        isDirected;  // 标识是否是有向图,默认为false
    vector<V>       vertex;  // 存储顶点
    vector<vector<W> > edge; // 存储边

};

#endif //GRAPH_H

graph.cpp

#include "graph.h"


/*
*   public 函数
*/

template<typename V, typename W>
GraphMatrix<V,W>::GraphMatrix(const V* _vertex, size_t _size, bool _isDirected = false)
{
    // 开辟空间并初始化数据
    this->vertex.resize(_size);
    this->edge.resize(_size);
    this->isDirected = _isDirected;

    for (int idx = 0; idx < _size; ++idx)
    {
        this->vertex[idx] = _vertex[idx];
        this->edge[idx].resize(_size);
    }
}

template<typename V, typename W>
void GraphMatrix<V, W>::addEdge(const V& v1, const V& v2, const W& weight)
{
    int start = getIndexOfVertex(v1);
    int end = getIndexOfVertex(v2);

    edge[start][end] = weight;

    // 如果是无向图还需要添加对称的一遍
    if (!isDirected)
        edge[end][start] = weight;
}

template<typename V, typename W>
void GraphMatrix<V, W>::printEdge()
{
    for (int idx = 0; idx < vertex.size(); ++idx)
        cout <<  "" <<  vertex[idx];
    cout << endl;

    for (int idx_row = 0; idx_row < edge.size(); ++idx_row)
    {
        cout << vertex[idx_row] << " ";
        for (int idx_col = 0; idx_col < edge.size(); ++idx_col)
        {
            cout <<  " " << edge[idx_row][idx_col] ;
        }
        cout << endl;
    }
    cout << endl;
}

/*
*   private 函数
*/

template<typename V, typename W>
int GraphMatrix<V, W>::getIndexOfVertex(const V& v)
{
    for (int idx = 0; idx < vertex.size(); idx++)
    {
        if (vertex[idx] == v)
            return idx;
    }

    // 如果没有找到就说明发生了错误
    assert(false);
    return -1;
}

test.cpp

#include "graph.cpp"

#include <string>
#include <vector>
#include <iostream>
using namespace std;

void TestGraphMatrix()
{
    // 无向图
    const char* str = "ABCDE";
    GraphMatrix<char, int> graph(str, strlen(str));
    graph.addEdge('A', 'D', 8);
    graph.addEdge('A', 'E', 9);
    graph.addEdge('B', 'E', 2);
    graph.addEdge('A', 'B', 6);
    graph.printEdge();

    cout << "--------------------------------" << endl;

    // 有向图
    GraphMatrix<char, int> graph1(str, strlen(str), true);
    graph1.addEdge('A', 'D', 8);
    graph1.addEdge('A', 'E', 9);
    graph1.addEdge('B', 'E', 2);
    graph1.addEdge('E', 'C', 6);
    graph1.printEdge();


}

int main()
{
    TestGraphMatrix();
    return 0;
}

输出结果:

  A B C D E
A 0 6 0 8 9
B 6 0 0 0 2
C 0 0 0 0 0
D 8 0 0 0 0
E 9 2 0 0 0

-----------

  A B C D E
A 0 0 0 8 9
B 0 0 0 0 2
C 0 0 0 0 0
D 0 0 0 0 0
E 0 0 6 0 0

邻接表表示法

邻接表是把顶点之间的边当做链表上的结点,其中边结点数据成员有:

  • 起点的索引
  • 终点的索引
  • 边所在权值
  • 指向下一个结点的指针
// W -- 边对应权值的类型
template <typename W>
struct EdgeNode
{
    W                weight;  // 边所对应权值
    unsigned int startIndex;  // 边起点的索引
    unsigned int   endIndex;  // 边终点的索引
    EdgeNode<W>*   nextNode;  // 指向下个结点
};

用一个一维数组来存储所有的顶点。一个一维数组来存储顶点所对应的链表的头指针(结点表示以当前顶点为起点的边)。

bool                isDirected; 
vector<V>               vertex; // 存储所有顶点
vector<EdgeNode<W>*> linkTable; // 存储顶点的边

graph.h

#ifndef GRAPH_H
#define GRAPH_H

#include <vector>
#include <cassert>
#include <iostream>
using namespace std;

// W -- 边对应权值的类型
template <typename W>
struct EdgeNode
{
    W                weight;  // 边所对应权值
    size_t startIndex;  // 边起点的索引
    size_t   endIndex;  // 边终点的索引
    EdgeNode<W>*   nextNode;  // 指向下个结点
    EdgeNode(size_t start, size_t end, const W& _weight)
        : startIndex(start)
        , endIndex(end)
        , weight(_weight)
    {}
};

// V -- 图顶点的数据类型
// W -- 图边权值的类型
template <typename V, typename W>
class GraphLink
{
public:
    typedef EdgeNode<W> node;

    GraphLink(const V* _vertex, size_t _size, bool _isDirected = false);
    // 打印图中的边
    void printEdge();
    // 向图中添加一条边
    void addEdge(const V& v1, const V& v2, const W& weight);

private:
    // 获取顶点所在索引
    size_t getIndexOfVertex(const V& v);
    // 添加一条边
    void __addEdge(size_t startIndex, size_t endIndex, const W& weight);

private:
    bool         isDirected; 
    vector<V>        vertex; // 存储所有顶点
    vector<node*> linkTable; // 存储顶点的边
};


#endif //GRAPH_H

graph.cpp

#include "graph.h"


/*
*   public 函数
*/

template<typename V, typename W>
GraphLink<V, W>::GraphLink(const V* _vertex, size_t _size, bool _isDirected)
{
    // 开辟空间并初始化数据
    this->vertex.resize(_size);
    this->linkTable.resize(_size);
    this->isDirected = _isDirected;

    for (size_t i = 0; i < _size; i++)
    {
        this->vertex[i] = _vertex[i];
    }
}

template<typename V, typename W>
void GraphLink<V, W>::printEdge()
{
    for (size_t idx = 0; idx < vertex.size(); ++idx)
    {
        cout << vertex[idx] << ": ";

        node* pEdge = linkTable[idx];
        while (pEdge)
        {
            cout << pEdge->weight << "[" << vertex[pEdge->endIndex] << "]-->";
            pEdge = pEdge->nextNode;
        }
        cout << "NULL" << endl;
    }
    cout << endl;
}

template<typename V, typename W>
void GraphLink<V, W>::addEdge(const V& v1, const V& v2, const W& weight)
{
    size_t startIndex = getIndexOfVertex(v1);
    size_t endIndex   = getIndexOfVertex(v2);

    // 防止填加自己指向自己的边
    assert( startIndex!=endIndex);

    __addEdge(startIndex, endIndex, weight);

    // 无向图需要添加对称的一条边
    if (!isDirected)
        __addEdge(endIndex, startIndex, weight);

}



/*
*   private 函数
*/



template <typename V, typename W>
void GraphLink<V, W>::__addEdge(size_t startIndex, size_t endIndex, const W& weight)
{
    // 头插的方式添加边到链表中
    node* pNewEdge = new node(startIndex, endIndex, weight);
    pNewEdge->nextNode = linkTable[startIndex];
    linkTable[startIndex] = pNewEdge;
}

template<typename V, typename W>
size_t GraphLink<V, W>::getIndexOfVertex(const V& v)
{
    for (int idx = 0; idx < vertex.size(); idx++)
    {
        if (vertex[idx] == v)
            return idx;
    }

    // 如果没有找到就说明发生了错误
    assert(false);
    return -1;
}

test.cpp

#include "graph.cpp"
#include <string>
#include <vector>
#include <iostream>
using namespace std;


void TestGraphLink()
{
    // 无向图
    char* str = "ABCDE";
    GraphLink<char, int> graph1(str, strlen(str)); 
    graph1.addEdge('A', 'C', 2);
    graph1.addEdge('D', 'B', 6);
    graph1.addEdge('A', 'B', 4);
    graph1.addEdge('E', 'D', 9);
    graph1.printEdge();

    cout << "------------" << endl;
    // 有向图

    GraphLink<char, int> graph2(str, strlen(str), true);
    graph2.addEdge('D', 'C', 2);
    graph2.addEdge('B', 'E', 6);
    graph2.addEdge('A', 'D', 4);
    graph2.addEdge('E', 'D', 9);
    graph2.printEdge();
}

int main()
{

    TestGraphLink();
    return 0;
}

输出结果:

A: 4[B]-->2[C]-->NULL
B: 4[A]-->6[D]-->NULL
C: 2[A]-->NULL
D: 9[E]-->6[B]-->NULL
E: 9[D]-->NULL

----------------------

A: 4[D]-->NULL
B: 6[E]-->NULL
C: NULL
D: 2[C]-->NULL
E: 9[D]-->NULL

总结与对比

1、在邻接矩阵表示中,无向图的邻接矩阵是对称的。矩阵中第 i 行或 第 i 列有效元素个数之和就是顶点的读。

在有向图中 第 i 行有效元素个数之和是顶点的出度,第 i 列有效元素个数之和是顶点的入度。

2、在邻接表的表示中,无向图的同一条边在邻接表中存储的两次。如果想要知道顶点的读,只需要求出所对应链表的结点个数即可。

有向图中每条边在邻接表中只出现一此,求顶点的出度只需要遍历所对应链表即可。求出度则需要遍历其他顶点的链表。

3、邻接矩阵与邻接表优缺点

邻接矩阵的优点是可以快速判断两个顶点之间是否存在边,可以快速添加边或者删除边。而其缺点是如果顶点之间的边比较少,会比较浪费空间。因为是一个 nn 的矩阵。

而邻接表的优点是节省空间,只存储实际存在的边。其缺点是关注顶点的度时,就可能需要遍历一个链表。还有一个缺点是,对于无向图,如果需要删除一条边,就需要在两个链表上查找并删除。

扩展

逆邻接表

在邻接表中对于有向图有一个很大的缺陷,如果我们比较关心顶点入度那么就需要遍历所有链表。为了避免这种情况出现,我们可以采用逆邻接表来存储,它存储的链表是别的顶点指向它。这样就可以快速求得顶点的入度。

如何对有向图的入度和出度都关心,那么久可以采取十字链表的方式。相当于每一个顶点对应两个链表,一个是它指向别的顶点,一个是别的顶点指向它。

全文完

  • 19
    点赞
  • 74
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值