1. 图的介绍
1.1 图到底是个什么东西
图(graph)按是由顶点(vertex)和连接顶点的边(edges)构成的离散结构。离散数学里面的定义如下:
图G = (V,E)由顶点(或结点)的非空集V和边集E构成,每条边有一个或两个顶点与它相连,这样的顶点称为边的端点。边连接它的端点。
这样干讲属实有点无聊,来个图做例子:
1.2 图到底用来干嘛
图几乎可以用来表现所有类型的结构或系统,从交通网络到通信网络,从下棋游戏到最优流程,从任务分配到人际交互网络,图都有广阔的用武之地。打个比较具体一点的比方:
GPS导航就用到了图的算法。
1.3 为什么要有图
图的提出是为了更好的解决多对多的关系,线性表他仅仅局限于一个直接前驱和一个直接后继的关系,而树也仅仅只能有一个直接前驱也就是父节点。
图论〔Graph Theory〕是数学的一个分支。它以图为研究对象。图论中的图是由若干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用点代表事物,用连接两点的线表示相应两个事物间具有这种关系。
2. 图的概念介绍
为以后介绍图算法,提供背景知识。
2.1 顶点和边(vertex and edge)
上面介绍过了,不多重复,这里给出一些比较精准的定义,装一下b,虽然这个确实没什么用。
2.2 权重(weight)
边的权重(又或者是边的长度),是一个比较核心的概念,即每条边所对应的值。边的权重可以是零甚至可以是负数,比较不是一个真正的地图,给个带权重的简单图看一下。
在上面的地图例子中,对应的就是广州到东莞之间的距离(这个距离是我乱写,的不要当真):
2.3 路径与最短路径(path and the shortest path)
在图上任取两顶点,分别作为起点(start vertex)和终点(end vertex),我们可以规划许多条由起点到终点的路线,这个路线就叫做路径,如果两点之间存在路径,那么这两个顶点是连通的。
路径也有权重。路径经过的每一条边,沿路加权重,权重总和就是路径的权重(通常只加边的权重,而不考虑顶点的权重),所谓的最短路径就是权重最小的边。
那么,在后面我会用Dijkstra算法来介绍最短路径。
2.4 连通图/连通分量(connected graph/connected component)
每个点之间都存在路径,那么称这个图为连通图。
上面那个图,虽然不是一个连通图,但是他有两个连通子图,我们把一个图的最大连通子图称为它的连通分量,连通分量这些特点:
- 是子图
- 子图是连通的
- 子图含有最大顶点数
最大连通子图”指的是无法再扩展了,不能包含更多顶点和边的子图。
打个比方:
2.5 环(loop)
环,也称为环路,是一个与路径相似的概念。在路径的终点添加一条指向起点的边,就构成一条环路。通俗点说就是画圈圈。
深度优先搜索算法里面用到了环的概念。
2.6 有向图与无向图(Directed Graph and Undirected Graph)
有向图和无向图之间的区别在于方向性,用只有一条边的简单图打个比方,下面这个无向图:
这个是有向图:
你可以把无向图抽象成这样的有向图:
2.7 顶点的度
顶点的度,指的是图中和顶点V相关联的边的数目,度 = 入度 + 出度。在有向图中,所谓入度和出度,通俗的讲,入度指的是箭头指向自己这个点,出度指的是箭尾从自己那个点出来。在无向图中,由于没有方向的限制,出度等于入度。
2.8 稀疏图和稠密图(sparse graph and dense graph)
这个说白了就是边多少的问题,最主要是用在图论算法的构建中,时间复杂性和空间复杂性的选择问题,下面是定义
稀疏图:边的条数|E|远远小于顶点V:|V|^2的图
稠密图:边的条数|E|远远接近于顶点V:|V|^2的图
3. 图的实现方式
那么好,对于图而言,有两种常见的实现方式:邻接矩阵和邻接链表。那么在leetcode刷题的时候我发现,大部分图算法都是直接在矩阵上面进行,所以我会重点讲解邻接矩阵,邻接链表一笔带过。邻接链表十分直观的把图给表现出来,用链表的形式存储A和指向B的指针,以及边等乱七八糟的,而邻接矩阵是将整个图所在的空间抽象成一个矩阵,在这个矩阵上有点就为1,没点就为0。这里先给出示例图,有点印象。图算法的实现我都是基于无向邻接矩阵,当然我会介绍如何把无向变成有向。
4. 邻接链表的实现方式
邻接链表:
- 对于每个点,存储着一个链表,用来指向所有与该点直接相连的点
- 对于有权图来说,链表中元素值对应着权重
下面是算法导论里面的定义,然后再给出设计思路:
那么好,对于设计好的结点的结构体,应该包含几个元素:
- 自己的字母存储,例如ABCDEFG
- 一个关于边的链表,用来存储与其他点所连接的边的权值
- 在边的结构体那里应该可以指出,两个点是什么,两点确定一条直线,以及下一个边的指针构成一个链表
因此就有了以下示意图:
对着这个示意图进行设计,具体做法:
//头文件:AdjListUndirGraph.h
#ifndef ADJLIST_UNDIR_GRAPH_H //邻接表实现无向图
#define ADJLIST_UNDIR_GRAPH_H
#include <iostream>
#include <queue>
using namespace std;
#define DEFAULT_SIZE 4
struct AdjListArcNode //边结点定义
{
int adjVex;
AdjListArcNode *nextArc;
AdjListArcNode():nextArc(NULL) {
}
AdjListArcNode(int _adjVex,AdjListArcNode *_nextArc=NULL)
{
adjVex=_adjVex;
nextArc=_nextArc;
}
};
struct AdjListVexNode //顶点结点定义
{
char data;
AdjListArcNode *firstArc;
AdjListVexNode():firstArc(NULL) {
}
AdjListVexNode(char VexValue,AdjListArcNode *_firstArc=NULL)
{
data=VexValue;
firstArc=_firstArc;
}
};
enum VisitStatus{
VISIT,UNVISIT};
class AdjListUndirGraph //邻接表实现无向图定义
{
public:
AdjListUndirGraph(int _vexMaxNum=DEFAULT_SIZE);
AdjListUndirGraph(char *Vexs,int _vexMaxNum,int _vexNum);
~AdjListUndirGraph();
void InsertArc(int vex1,int vex2); //输入需合法,不考虑平行边,且vex1不等于vex2
void Show();
void DFSTraverse();
void BFSTraverse();
//friend istream& operator>>(istream &in,)
private:
VisitStatus *tag;
AdjListVexNode *vexTable;
int vexNum;
int vexMaxNum;
int arcNum;
void DFS(int startVex); //深度优先搜索
void BFS(int startVex); //广度优先搜索
};
AdjListUndirGraph::AdjListUndirGraph(int _vexMaxNum)
{
vexMaxNum=_vexMaxNum;
vexNum=0;
arcNum=0;
tag=new VisitStatus[vexMaxNum];
vexTable=new AdjListVexNode[vexMaxNum];
}
AdjListUndirGraph::AdjListUndirGraph(char *Vexs,int _vexMaxNum,int _vexNum)
{
vexMaxNum=_vexMaxNum;
vexNum=_vexNum;
arcNum=0;
tag=new VisitStatus[vexMaxNum];
vexTable=new AdjListVexNode[vexMaxNum];
for(int i=0;i<vexNum;i++)
{
tag[i]=UNVISIT;
vexTable[i].data=Vexs[i];
}
}
AdjListUndirGraph::~AdjListUndirGraph()
{
if(tag!=NULL)
delete[] tag;
if(vexTable!=NULL)
delete[] vexTable;
}
void AdjListUndirGraph::InsertArc(int vex1,int vex2)
{
vexTable[vex1].firstArc=new AdjListArcNode(vex2,vexTable[vex1].firstArc);
vexTable[vex2].firstArc=new AdjListArcNode(vex1,vexTable[vex2].firstArc);
arcNum++;
}
void AdjListUndirGraph::Show()
{
for(int i=0;i<vexNum;i++)
{
cout<<vexTable[i].data<<": ";
AdjListArcNode *p=vexTable[i].firstArc;
while(p!=NULL)
{
cout<<p->adjVex<<" ";
p=p->nextArc;
}
cout<<endl;
}
}
void AdjListUndirGraph::DFS(int startVex)
{
if(tag[startVex]!=VISIT)
{
cout<<vexTable[startVex].data;
tag[startVex]=VISIT;
AdjListArcNode *p=vexTable[startVex].firstArc;
for(;p!=NULL;p=p->nextArc)
DFS(p->adjVex);
}
}
void AdjListUndirGraph::DFSTraverse()
{
for(int i=0;i<vexNum;i++)
if(tag[i]==UNVISIT)
{
DFS(i);
cout<<endl;
}
}
void AdjListUndirGraph::BFS(int startVex)
{
if(tag[startVex]!=VISIT)
{
queue<int> q;
q.push(startVex);
tag[startVex]=VISIT;
cout<<vexTable[startVex].data;
int i;
AdjListArcNode *p;
while(!q.empty())
{
i=q.front();
q.pop();
for(p=vexTable[i].firstArc;p!=NULL;p=p->nextArc)
{
if(tag[p->adjVex]==UNVISIT)
{
tag[p->adjVex]=VISIT;
cout<<vexTable[p->adjVex].data;
q.push(p->adjVex);
}
}
}
}
}
void AdjListUndirGraph::BFSTraverse()
{
for(int i=0;i<vexNum;i++)
if(tag[i]==UNVISIT)
{
BFS(i);
cout<<endl;
}
}
#endif
//main.cpp
#include "AdjListUndirGraph.h"
int main()
{
char vexs[12]="ABCDEFGHIJK";
AdjListUndirGraph graph(vexs,11,11);
graph.InsertArc(0,1);
graph.InsertArc(0,4);
graph.InsertArc(0,3);
graph.InsertArc(1,2);
graph.InsertArc(1,4);
graph.InsertArc(2,4);
graph.InsertArc(4,5);
graph.InsertArc(5,8);
graph.InsertArc(4,6);
graph.InsertArc(6,7);
graph.InsertArc(3,6);
graph.InsertArc(9,10);
graph.Show();
graph.BFSTraverse();
return 0;
}
5. 邻接矩阵的实现
邻接矩阵:
- 在 n 个顶点的图需要有一个 n × n 大小的矩阵
- 在一个无权图中,矩阵坐标中每个位置值为 1 代表两个点是相连的,0 表示两点是不相连的
- 在一个有权图中,矩阵坐标中每个位置值代表该两点之间的权重,0 表示该两点不相连
- 在无向图中,邻接矩阵关于对角线相等
算法导论的定义:
很烦,很长,我先给出一个示意图解释怎么看邻接矩阵,然后再进行构造思路说明:
边权表和顶点表的读法差不多,先讲顶点表(数字比较简单好看一点):