前言
经过,一段时间的学习,终于接触到了 图 这一重要的数据结构。与此同时,图在我们的生活中也起到了至关重要的作用。例如,网易云音乐的推荐、最短路径的查找等,这些场景都能利用图得到解决或者优化。
图
定义
维基百科定义:在数学上,一个图(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,不断循环。
我们需要存储节点是否被访问,同时还需要存储它上一个节点,我们用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的所有子节点入队,然后队首出队,同时队首子节点入队,不断循环。
因为我们是不断加入子节点的,同一个节点的子节点到原始节点的距离就一定相同,当然距离不可能超过节点的总个数,距离我们使用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