1、概念
节点叫顶点vertex,顶点之间的连线叫边edge。
树、线性表是形式受限的图。
图是数据结构的最通用的形式。
G = ( V, E ),V是顶点集合,E是边集合。
|V|,|E|:顶点数、边数。
|E|:0 ~ O( |V| )。
包含所有可能的边,叫完全图。
有向图、无向图、标号图、带权图。
路径path,路径长度length = 边数。
简单路径:路径顶点各不相同。
回路:路径首尾是同一个节点,路径长度>=3。
密集图:边数较多的图。
稀疏图:边数较少的图。
简单回路:简单路径的回路。全部概念如下图:
连通的无向图:每个顶点到任意其他顶点都有路径。
连通分量:无向图的最大连通子图。
(有向)无环图:不带回路的(有向)图。
自由树:不带简单回路的连通无向图。连通且有|V| - 1条边的图。
2、图的表示方法
相邻矩阵:两个顶点边的权值,没有边返回0,所以!0表示顶点间有边相连。
领接表:链表存储边(Edge),边存储指向下一个的顶点和边权值。
3、图ADT(抽象基类)
class Graph{
public:
virtual int n() = 0; //顶点数
virtual int e() = 0; //边数
virtual int first(int v) = 0; //顶点v相邻第一个邻居顶点
virtual int next(int v, int v_n) = 0; //返回顶点v的下一相连顶点
//比如0-1-2-3-4-5:顶点都相连
//next(0, 3) = 4;
//next(0, 5) = 6;即到达尾部,返回顶点数。
virtual void setEdge(int v1, int v2, int weight) = 0; //存储边(顶点v1、v2,权重weight)
//可以以此创建图
virtual void delEdge(int v1, int v2) = 0; //删除边(v1, v2)
virtual int weight(int v1, int v2) = 0; //边(v1, v2)的权重,如果不存在返回0
virtual int getMark(int v1) = 0; //返回v1的标号值
virtual void setMark(int v1, int value) = 0; //设置顶点v1的标号值
}
图的实现(矩阵)
//边edge的类
//为什么只存储了一点顶点和权重?
//因为这些信息已经足够遍历图了。想象一下,初始提供一个顶点。
class Edge{
public:
//vertex:表示指向的顶点。
//weight:表示与vertex相连边的权重
int vertex, weight;
Edge(){ vertex = -1; weight = -1; }
Edge(int v, int w){ vertex = v; weight = w; }
}
class GraphMatrix : public Graph{
private:
int numVertex, numEdge; //顶点数,边数
int** matrix; //矩阵数组:下标值为顶点索引,值为权重
int* mark; //顶点标记,是否访问过
//函数接口声明
public:
GraphMatrix(int numVert);
~GraphMatrix();
int n();
int e();
int first(int v);
int next(int v1, int v2);
void setEdge(int v1, int v2, int wgt);
void delEdge(int v1, int v2);
int weight(int v1, int v2);
int getMark(int v);
void setMark(int v, int val);
//函数定义
public:
GraphMatrix(int numVert){
int i, j;
numVertex = numVert;
numEdge = 0;
mark = new int[numVert];
for(i = 0; i < numVertex; i++) mark[i] = UNVISITED;
maxtrix = (int**)new int*[numVertex];
for(i = 0; i < numVertex; i++) *(maxtrix+i) = new int[numVert];
for(i = 0;i < numVertex; i++)
for(int j = 0;j < numVertex;j++)
matrix[i][j] = 0;
}
~GraphMatrix(){
delete[] mark;
for(int i = 0;i < numVertex; i++) delete[] matrix[i];
delete[] maxtrix;
}
int n(){ return numVertex; }
int e(){ return numEdge; }
int first(int v){
int i;
for(i = 0;i < numVertex; i++) if(matrixp[v][i] != 0) return i;
return i;
}
int next(int v1, int v2){
int i;
for(i = v2 + 1;i < numVertex; i++) if(matrix[v1][i] != 0) return i;
return i;
}
void setEdge(int v1, int v2, int wgt){
Assert(wgt > 0, "Illegal weight value.");
if(matrix[v1][v2] == 0) numEdge++;
matrix[v1][v2] = wgt;
}
void delEdge(int v1, int v2){
if(matrix[v1][v2] != 0) numEdge--;
matrix[v1][v2] = 0;
}
int weight(int v1, int v2){ return matrix[v1][v2]; }
int getMark(int v){ return mark[v]; }
void setMark(int v, int val){ mark[v] = val; }
}
图的实现(邻接表)
class GraphLink : public Graph{
private:
int numVertex, numEdge;
List<Edge>** vertex; //注意,下标是顶点索引,链表项是与它相连的边。
int *mark; //顶点标记,是否访问过
public:
GraphLink(int numVert){
int i, j;
numVertex = numVert;
numEdge = 0;
mark = new int[numVert];
for(i = 0; i < numVertex; i++) mark[i] = UNVISITED;
vertex = (List<Edge>**) new List<Edge>*[numVertex];
for(i = 0; i < numVertex; i++)
vertex[i] = new LList<Edge>();
}
~GraphLink(){
delete[] mark;
for(int i = 0;i < numVertex; i++) delete[] vertex[i];
delete[] vertex;
}
int n(){ return numVertex; }
int e(){ return numEdge; }
int first(int v){ //也就是第一条边指向的顶点
Edge it;
vertext[v]->setStart();
if(vertext[v]->getValue(it)) return it.vertex;
else return numVertex;
}
int next(int v1, int v2){
Edge it;
vertex[v1]->getValue(it);
if(it.vertex == v2) vertex[v1]->next();
else{
//循环直到找到v2顶点
vertex[v1]->setStart();
while(vertext[v1]->getValue(it) && (it.vertex <= v2))
vertex[v1]->next();
}
if(vertex[v1]->getValue(it)) return it.vertex;
else return numVertex;
}
void setEdge(int v1, int v2, int wgt){
Assert(wgt > 0, "Illegal weight value.");
Edge it(v2, wgt);
Edge curr;
vertex[v1]->getValue(curr);
if(curr.vertex != v2){
//循环直到找到顶点v2
for(vertex[v1]->setStart(); vertex[v1]->getValue(curr);vertex[v1]->next())
if(curr.vertex >= v2) break;
}
if(curr.vertex == v2) vertex[v1]->remove(curr);
else numEdge++;
vertex[v1]->insert(it);
}
void delEdge(int v1, int v2){
Edge curr;
vertex[v1]->getValue(curr);
if(curr.vertex != v2)
//循环直到找到顶点v2
for(vertex[v1]->setStart();vertex[v1]->getValue(curr);vertex[v1]->next())
if(curr.vertex >= v2) break;
if(curr.vertex == v2){
vertex[v1]->remove(curr);
numEdge--;
}
}
int weight(int v1, int v2){
Edge curr;
vertex[v1]->getValue(curr);
if(curr.vertex != v2)
//循环直到找到顶点v2
for(vertex[v1]->setStart();vertex[v1]->getValue(curr);vertex[v1]->next())
if(curr.vertex >= v2) break;
if(curr.vertex == v2) return curr.weight;
else return 0;
}
int getMark(int v){ return mark[v]; }
void setMark(int v, int val){ mark[v] = val; }
}
4、图的周游(遍历)
一次遍历可能无法全部遍历到,一个数组保存所有节点访问状态,遍历过的就标记。
每个节点vertex执行一次遍历算法:
vertex已被访问,则无需执行,
vertex已被访问,则执行遍历。
void graphTraverse(const Graph* G){
for(v = 0;v < G->n(); v++)
G->setMark(v, UNVISITED);
for(v = 0;v < G->n();v++)
if(G->getMark(v) != UNVISITED)
doTraverse(G, v); //从图G的v顶点开始遍历。
}
//深度优先遍历
//递归调用先天特性 就符合 深度优先的特点
void DFS(Graph* G, int v){
// PreVisit(G, v); // 访问节点前做点啥
G->setMark(v, UNVISITED);
for(int w = G->first(v); w < G->n(); w = G->next(v, w))
if(G->getMark(w) == UNVISITED) DFS(G, w);
// PostVisit(G, v); // 访问节点后做点啥
}
//广度优先遍历:先访问直接相连的点。
//需要用一个队列来实现。
void BFS(Graph* G, int start, Queue<int>* Q){
int v, w;
q->enqueue(start);
G->setMakr(start, VISITED);
while(Q->lengt()!=0)
Q->dequeue(v);
// PreVisit(G, v); //访问节点前做点啥
for(int w = G->first(v); w < G->n(); w = G->next(v, w))
if(G->getMark(w) == UNVISITED) BFS(G, w);
// PostVisit(G, v); //访问节点后做点啥
}
5、拓扑排序
优先访问顶点的“所有前顶点”规则的遍历。
有向图DAG才有拓扑排序。
比如工作任务必须先执行全部的先决任务(即“入边”或者是前顶点)。
//拓扑排序算法(PostVisit版深度优先)
//注意最终输出的是“逆”拓扑排序!!还要再把结果序列颠倒一下。
//所以以下不是完整的拓扑排序!!!!
void topsort(Graph* G){
int i;
for(i = 0;i < G->n(); i++) G->setMark(i, UNVISITED);
for(i = 0;i < G->n(); i++) //可能存在孤立的顶点,所以每个都要进行一次拓扑。
if(G->getMark(i) == UNVISITED)
tophelp(G, i);
}
void tophelp(Graph* G, int v){
G->setMark(v, VISITED);
//printout(v); //注意!不是在previsit打印,
//如果是在这里打印,倒序之后就不符合拓扑排序。
for(int w = G->first(v); w < G->n(); w = G->next(v, w))
if(G->getMark(w) == UNVISITED)
tophelp(G, w);
printout(v); //必须在postvisit打印。
}
//基于队列的拓扑排序算法
//算法原理:
// 1、保存每个顶点的“前”顶点数包括:
// 直接“前”顶点
// “间接前”顶点,如a->b->c>d,ab是d的间接前顶点。
// 其实就是所有先决条件(任务)
// 2、没“前”顶点的顶点才能入队。
// 3、出队时,后续所有相连顶点的顶点数-1:
// 相当于执行当前任务,
// 也算是为其他相关顶点减了个先决“任务”
void topsort(Graph* G, Queue<int>* Q){
int Count[G->n()];
int v, w;
for(v = 0; v < G->n(); v++) Count[v] = 0;
for(v = 0; v < G->n(); v++)
for(w = G->first(v); w < G->n(); w = G->next(v, w))
Count[w]++; //上面注释第1步:记录“前”顶点数
for(v = 0; v < G->n(); v++)
if(Count[v] == 0)
Q->enqueue(v); //上面注释第2步:
while(Q->length() != 0){
Q->dequeue(v); //上面注释第3步:
printout(v);
for(w = G->first(v); w < G->n(); w = G->next(v, w)){
Count[w]--; //上面注释第3步
if(Count[w] == 0) Q->enqueue(w); //上面注释第2步
}
}
}
6、(单源)最短路径原则
给定一个顶点,找出到所有其他顶点的最短路径。
如下图,求A到D的最短路径。
Dijkstra算法
算法思想:
下面是伪数学表达式,不是严格成立,但符合大脑思考方式。
d(s, vn+j) = d(s, v) + d(v, v)
v图中非s顶点。
d**(s, v):顶点 v 距离 顶点 s 的“目前”最短距离。
d(v, v):顶点v到直接相连的顶点v的距离(边权)。
从实际实现上,d只能算是目前掌握到的“目前最短距离”,我们必须顶点遍历完才是“真正最短距离”**。所以遍历的过程,最短距离不断接近准确值。
这里也反应了一个问题,就是求顶点v到顶点s的最短路径,其实是求所有顶点到s的最短路径。
算法复杂度:O( **|V|2 )**
//寻找顶点s到所有其他顶点的最短距离
//D:保存每个顶点到s顶点的“目前”最短距离
//s:目标顶点s
//初始状态,D[s] = 0,其他都 = INFINITY。
void Dijkstra(Graph* G, int* D, int s){
int i;
int v; //在D中,s的最近顶点
int w; //w是v能next到的顶点
for(i = 0; i < G->n(); i++){
v = minVertex(G, D); //找出离s“最近”顶点v。
//顶点v与s是“目前”不相连的,
//就是目前还没有距离信息,还没遍历到。
if(D[v] == INFINITY) return;
G->setMark(v, VISITED); //避免重复访问
//算法核心:根据目前的D(目前最短距离)
for(w = G->first(v); w < G->n(); w = G->next(v, w))
//s到v的目前最短距离 + v与顶点w相连的权边 < s到w的目前最短距离。
if(D[w] > (D[v] + G->weight(v, w)))
//更新s到w的最短距离为:s到v最短距离 + v到w的权边
D[w] = D[v] + G->weight(v, w);
}
}
//找出D中的最小值,而且是未访问的节点。
int minVertex(Graph* G, int* D){
int i, v;
for(i = 0; i < G->n(); i++)
if(G->getMark(i) == UNVISITED){
v = i;
break;
}
for(i++; i < G->n();i++)
if((G->getMark(i) == UNVISITED) && (D[i] < D[v]))
v = i;
return v;
}
Dijkstra算法(优先队列)
核心思想相同,但是用了最小值堆来找最短距离顶点。
算法复杂度:O( (|V| + |E|) * log |E| )
密集图时,复杂度接近:O( |V|**2* log |E| )
//保存每个顶点的索引和
class DijkElem{
public:
int vertex, distance;
DijkElem(){ vertex = -1; distance = -1; }
DijkElem(int v, int d){ vertex = v; distance = d; }
}
//返回
void Dijkstra( Graph* G, int* D, int s ){
int i, v, w;
DijkElem temp(s, 0);
DijkElem E[G->e()];
E[0] = temp;
minheap<DijkElem, DDComp> H(E, 1, G->e()); //堆目前有1个元素,堆总空间G->e()
//保存未被访问的顶点,各节点就是目前到s
//最短距离的顶点
for(i = 0; i < G->n(); i++){
do{
if(!H.removemin(temp)) //取出根节点,也就是最短值节点
return; //堆没有节点了
v = temp.vertex;
}while(G->getMark(v) == VISITED);
G->setMark(v, VISITED);
if(D[v] == INFINITY) return;
for(w = G->first(); w < G->n(); w = G->next(v, w))
if(D[w] > (D[v] + G->weight(v, w))){
D[w] = D[v] + G->weight(v, w);
temp.distance = D[w];
temp.vertex = w;
H.insert(temp);
}
}
}
算法分析
核心思想相同,但查找目前最短距离顶点的上各做了“取舍”。
minvertex是扫描数组,找的更慢点但找的稳定,不怕多,
minheap是最小值堆,是找的更快点但找的不稳定,多了代价就大。
minvertex:算法复杂度:O( **|V|2 )
minheap: 算法复杂度:O( (|V| + |E|) log |E| )*
边数过多时(密集图):
minvertex:算法复杂度:O( **|V|2 )
minheap: 算法复杂度:O( |V|2 * log |E| )
7、双源最短路径
找每对顶点的最短长度路径。
复杂度:O(**|V|**)**
密集图最好选择,较容易实现。
void Floyd(Graph* G){
int D[G->n()][G->n()];
for(int i = 0; i < G->n(); i++)
for(int j = 0; j < G->n(); j++)
D[i][j] = G->weight(i, j) //初始化为任意两点之间的权重。
for(int k = 0; k < G->n(); k++)
for(int i = 0; i < G->n(); i++)
for(int j = 0; j < G->n(); j++)
if(D[i][j] > (D[i][k] + D[k][j]))
D[i][j] = D[i][k] + D[k][j];
}
8、最小支撑树MST
minimum-cost spanning tree。
数学定义:G是连通无向图,MST是下图中的一个:包含所有顶点和部分边的连通图。所有边之和是所有这些图中最小的。
加深理解:把所有城市连在一起的,路长度最小的路线。
没有回路,所以是|V| - 1条边的树结构。
求最小支撑树:Prim算法
0、初始顶点N
1、找出N的最小权边的顶点N
2、找出与N、N相连的最小权边的点N
3、……
4、找出与N、……、N相连的最小权边N
//D:保存与各个索引顶点的相连的最小权边
// 其实D[s] = 0,其他 = INFINITY
//s:起始顶点
//类似最短路径算法。
void Prim(Graph* G, int* D, int s){
int V[G->n()]; //V存储MST树节点,V[v]=w,顶点v和w直接相连。
int i, w;
for(i = 0; i < G->n(); i++){
int v = minVertex(G, D);
G->setMark(v, VISITED);
if(v != s) AddEdgetToMST(V[v], v);
if(D[v] == INFINITY) return;
for(w = G->first(v); w < G->n(); w = G->next(v, w))
if(D[w] > G->weight(v, w)){ //w当前保存的最小权边 比vw边更大,替换掉
D[w] = G->weight(v, w); //v,next到w的权最小
V[w] = v; //w顶点来来源是v。
}
}
}
//使用优先队列的Prim
//类似最短路径算法的优先队列。
void Prim(Graph* G, int* D, int s){
int i, v, w;
int V[G->n()];
DijkElem temp;
DijkElem E[G->e()];
temp.distance = 0;
temp.vertex = s;
E[0] = temp;
minheap<DijkElem, DDComp> H(E, 1, G->e());
for(i = 0; i < G->n(); i++){
do{
if(!H.removemin(temp)) return;
v = temp.vertex;
}while(G->getMark(v) == VISITED);
G->setMark(v, VISITED);
if(v != s) AddEdgetToMST(V[v], v);
if(D[v] == INFINITY) return;
for(w = G->first(v); w < G->n(); w = G->next(v, w))
if(D[w] > G->weight(v, w)){
D[w] = G->weight(v, w);
V[w] = v;
temp.distance = D[w];
temp.vertex = w;
H.insert(temp);
}
}
}
求最小支撑树:Kruskal算法
算法复杂度:
最差O(**|E| log |E|**)
一般O(**|V| log |E|**)
1、将所有顶点分为|V|个等价类。(UNION/FIND树)
2、优先处理权最小的边,边把两个等价类相连,则合并等价类
3、重复2步骤。
4、最后只剩一个等价类。