五、图
图是比较复杂的数据结构,它由顶点和顶点之间的弧组成。任何两个顶点之间都可能存在弧,利用计算机存储图的完整信息,就要求能存储图的类型(有向或无向,带权或无权)、顶点个数、弧的条数及每个顶点的信息、每对顶点之间是否存在弧或弧的信息。根据这些信息,可以完整描述图,继而对图进行操作。本章介绍两种图的存储结构,它们既有共性,又有特性。共性部分作为基类在 AGraph 类中,特性部分作为派生类分别在 MatrixGraph 和 AdjListGraph 中。
实现:
//图抽象类的定义
template<typename VerT>class AGraph
{//定义顶点结构模板的图抽象类
private:
int LocateVex(VerT u)const; //返回顶点 u 的序号,不存在则返回 -1
void DFS(int i, void(*visit) (VerT), bool Visited[])
{//从顶点[i]出发递归深度优先遍历图
Visited[i] = true; //设置访问标志为 true (已访问)
visit(GetVex(i)); //访问顶点[i]
for(int j = FirstAdjVex(i); j >= 0; j = NextAdjVex(i, j))
//从顶点[i]的第一个邻接顶点[j]开始
if (!Visited[j]) //邻接顶点[j]尚未被访问
DFS(j, visit, Visited);
}
protected:
int ArcNum; //图的弧数
bool Weight; //true:带权;false:无权
bool Directed; //true:有向;false:无向
public:
void CreateFromFile(char* FileName); //由数据文件创建图
bool PutVex(VerT v, VerT value); //对顶点 v 赋新值 value
virtual int FirstAdjVex(int i)const=0;
//返回顶点[i]的第一个邻接顶点的序号,不存在则返回 -1
virtual int NextAdjVex(int i, int j)const=0;
//返回顶点[i](相对于顶点[j])的下一个邻接顶点的序号,不存在则返回 -1
void InsertVex(VerT v); //增添新顶点 v (不增添与顶点的相关弧)
bool DeleteVex(VerT v); //删除顶点 v 及相关弧
bool InsertArc(VerT v, VerT w);//增添弧<v,w>,若是无向的,则还增加对称弧<w,v>
bool DeleteArc(VerT v, VerT w);//删除弧<v,w>,若是无向的,则还删除对称弧<w,v>
void Display()const; //显示图的存储数据
void DFSTraverse(void(*visit) (VerT))
{//从顶点[0]起,深度优先递归遍历图
int i;
bool *Visited = new bool[GetVexNumOrArcNum(0)];
for(i = 0; i < GetVexNumOrArcNum(0); i++)
Visited[i] = false; //未被访问
for(i = 0; i < GetVexNumOrArcNum(0); i++)
if (!Visited[i])
DFS(i, visit, Visited);
cout << endl;
delete[] Visited;
}
void BFSTraverse(void(*visit) (VerT))
{//从顶点[0]起,广度优先递归遍历图
int i, j, k;
bool *Visited = new bool[GetVexNumOrArcNum(0)];
queue<int> q; //存储顶点序号
for(i = 0; i < GetVexNumOrArcNum(0); i++)
Visited[i] = false; //未被访问
for(i = 0; i < GetVexNumOrArcNum(0); i++)
if (!Visited[i])
{
Visited[i] = true;
visit(GetVex(i));
q.push(i);
while(!q.empty())
{
j = q.front();
q.pop();
for(k = FirstAdjVex(j); k >= 0; k = NextAdjVex(j, k))
if (!Visited[k])
{
Visited[k] = true;
visit(GetVex(k));
q.push(k);
}
}
}
cout << endl;
delete[] Visited;
}
virtual int GetVexNumOrArcNum(int i)const=0; //返回图的顶点数(i=0)或弧数(i=1)
bool GetDirectedOrWeight(int i)const
{//返回图的有向性(i = 0)或带权性(i = 1)
if (i == 0)
return Directed;
else
return Weight;
}
virtual VerT GetVex(int i)const=0; //返回顶点[i]的值
};
//对图的两个顶点相等的宏定义和定义权值类型
#define EQ(a, b) (a.name == b.name)
typedef int VRType; //定义定点关系(权值)类型为整型
1. 图的邻接矩阵存储结构
由于图的顶点和弧在结构上完全没有关系,因此图类的模板就有两种:顶点结构 VerT 模板和弧结构 ArcT 模板。
实现:
//图的邻接矩阵类
template<typename VerT, typename ArcT>class MatrixGraph: public AGraph<VerT>
{//带模板并继承 AGraph<VerT> 的邻接矩阵类
protected:
int VexNum; //图的顶点数
VerT vexs[MAX_VERTEX_NUM]; //顶点向量
ArcT arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; //邻接矩阵,存储弧信息
int LocateVex(VerT u)const
{//返回与 u 具有相同关键字的顶点序号,不存在则返回-1
int i;
for(i = 0; i < VexNum; ++i)
if EQ(u, vexs[i])
return i;
return -1;
}
public:
MatrixGraph()
{//构造函数,构造一个空图
VexNum = ArcNum = 0;
}
void DestroyGraph()
{//销毁图
for(int i = VexNum - 1; i >= 0; i--)
DeleteVex(vexs[i]);
}
void CreateFromFile(char* FileName)
{//由数据文件创建图
int i, j, k;
VRType w = 0; //初始设置为无权图
VerT v1, v2;
ifstream fin(FileName);
fin >> boolalpha >> Directed; //输入有向性
fin >> boolalpha >> Weight; //输入带权性
if (Weight) //带权图
w = INFINITY;
fin >>VexNum;
for(i = 0; i < VexNum; ++i)
InputVertexFromFile(fin, vexs[i]); //输入顶点信息
fin >> ArcNum;
for(i = 0; i < VexNum; ++i)
for(j = 0; j <VexNum; ++j)
arcs[i][j].adj = w; //不邻接
if (Weight) //带权图
for(i = 0; i < VexNum; ++i) //给对角线元素赋值 0
arcs[i][i].adj = 0;
if (!Weight) //无权图
w = 1;
for(k = 0; k < ArcNum; ++k)
{
InputVertexFromFile(fin, v1); //输入弧尾名称
InputVertexFromFile(fin, v2); //输入弧头名称
if (Weight) //带权图
fin >> w;
i = LocateVex(v1); //弧尾序号
j = LocateVex(v2); //弧头序号
arcs[i][j].adj = w; //权值或 1 (表示有弧)
InputArcFromFile(fin, arcs[i][j]); //输入弧除权值外的信息
if (!Directed) //无向
arcs[j][i] = arcs[i][j]; //对称单元信息相同
}
fin.close();
}
bool PutVex(VerT v, VerT value)
{//对顶点 v 赋新值 value
int k = LocateVex(v);
if (k < 0)
return false;
vexs[k] = value;
return true;
}
void InsertVex(VerT v)
{//在图中增添新顶点 v (不增添弧)
int i;
VRType j = 0; //初始设为无权图
if (Weight) //带权图
j = INFINITY;
vexs[VexNum] = v;
for(i = 0; i < VexNum; i++)
arcs[VexNum][i].adj = arcs[i][VexNum].adj = j;
arcs[VexNum][VexNum].adj = 0; //对角元素
VexNum++;
}
bool DeleteVex(VerT v)
{//删除图中顶点 v 及相关的弧
int i, j, k;
k = LocateVex(v);
if (k < 0)
return false;
for(i = 0; i < VexNum; i++)
DeleteArc(v, vexs[i]); //删除由顶点 v 发出的所有弧
if (Directed) //有向
for(i = 0; i < VexNum; i++)
DeleteArc(vexs[i], v); //删除发向顶点 v 的所有弧
for(j = k+1; j < VexNum; j++)
vexs[j-1] = vexs[j]; //[k]后面的顶点向量依次前移
for(i = 0; i < VexNum; i++)
for(j = k+1; j < VexNum; j++)
arcs[i][j-1] = arcs[i][j]; //移动待删除顶点之右的矩阵元素
for(i = 0; i < VexNum; i++)
for(j = k+1; j < VexNum; j++)
arcs[j-1][i] = arcs[j][i]; //移动待删除顶点之下的矩阵元素
VexNum--;
return true;
}
bool InsertArc(VerT v, VerT w)
{//增添弧<v,w>,若是无向的,则还增添对称弧<w,v>
int i, j;
i = LocateVex(v);
j = LocateVex(w);
if (i < 0 || j < 0)
return false;
ArcNum++;
if (Weight) //带权图
cin >> arcs[i][j].adj;
else //无权图
arcs[i][j].adj = 1;
InputArc(arcs[i][j]); //输入弧的其他信息
if (!Directed) //无向
arcs[j][i] = arcs[i][j];
return true;
}
bool DeleteArc(VerT v, VerT w)
{//删除弧<v,w>,若是无向的,则还删除对称弧<w,v>
int i, k;
VRType j = 0; //初始设为无权图
if (Weight) //带权图
j = INFINITY;
i = LocateVex(v);
k = LocateVex(w);
if (i < 0 || k < 0 || i == k)
return false;
if (arcs[i][k].adj != j) //有弧<v,w>
{
arcs[i][k].adj = j; //删除弧<v,w>
if (!Directed) //无向
arcs[k][i] = arcs[i][k];
ArcNum--;
}
return true;
}
int FirstAdjVex(int i)const
{//返回顶点[i]的第一个邻接顶点的序号,不存在则返回 -1
VRType j = 0;
if (Weight) //带权图
j = INFINITY;
for(int k = 0; k < VexNum; k++)
if (i != k && arcs[i][k].adj != j)
return k;
return -1; //没有邻接顶点
}
int NextAdjVex(int i, int j)const
{//返回顶点[i](相对于顶点[j])的下一个邻接顶点的序号,不存在则返回 -1
VRType m = 0;
if (Weight) //带权图
m = INFINITY;
for(int k = j+1; k < VexNum; k++)
if (i != k && arcs[i][k].adj != m)
return k;
return -1;
}
VerT GetVex(int i)const
{//返回顶点[i]的值
if (i >= VexNum || i < 0)
exit(1);
return vexs[i];
}
ArcT GetArc(int i, int j)const
{//返回弧<i,j>的信息
return arcs[i][j];
}
void Display()const
{//输出邻接矩阵存储结构的图
int i, j;
string s1 = "无向", s2 = "无权图";
if (Directed)
s1 = "有向";
if (Weight)
s2 = "带权图";
cout << VexNum << "个顶点" << ArcNum << "条弧的" << s1 << s2
<< "。顶点依次是:";
for(i = 0; i <VexNum; ++i)
Visit(GetVex(i));
cout << endl << "arcs.adj" << endl;
for(i = 0; i < VexNum; i++)
{
for(j = 0; j < VexNum; j++)
if (arcs[i][j].adj == INFINITY)
cout << setw(2) << "#" << " "; //用 # 代替无穷,表示不连通
else
cout << setw(2) << arcs[i][j].adj << " ";
cout << endl;
}
for(i = 0; i < VexNum; i++)
if (Directed) //有向
{
for(j = 0; j < VexNum; j++)
if (i != j && (!Weight && arcs[i][j].adj ||
Weight && arcs[i][j].adj != INFINITY))
//非对角线元素,同时无权且 .adj 的值不为 0 或带权且 .adj 的值不为无穷(#)
OutputArc(Directed, vexs[i], vexs[j], arcs[i][j]);
//输出弧的信息
}//加括号为避免 if-else 对配错
else //无向,输出上三角
for(j = i+1; j < VexNum; j++)
if (!Weight && arcs[i][j].adj || Weight && arcs[i][j].adj!=INFINITY)
OutputArc(Directed, vexs[i], vexs[j], arcs[i][j]); //输出弧的信息
}
int GetVexNumOrArcNum(int i)const
{//返回图的顶点数(i = 0)或弧数(i = 1)
if (i == 0)
return VexNum;
else
return ArcNum;
}
};
图的邻接矩阵存储结构用二维矩阵存储两点之间的弧。无论两顶点之间是否有弧,存储空间都已经预留。这很适合存储稠密图,即弧多的图。对于弧少的图,这种存储结构空间浪费很大。这种情况最好采用邻接表存储结构,或用稀疏矩阵代替二维矩阵。
2. 图的邻接表存储结构
图的邻接表存储结构的顶点信息采用顺序存储结构,每个顶点的邻接顶点采用链式存储结构。为了实现高效编程,其中的顺序存储结构用了 STL 的 vector 向量类实现,链式存储结构用 STL 的 list 表类实现。
实现:
//图的邻接表类
template<typename VerT, typename ArcT>class Articul; //声明带模板的关节点类
template<typename VerT, typename ArcT>class AdjListGraph: public AGraph<VerT>
{//带模板并继承 AGraph<VerT> 的邻接表类
friend Articul<struct V, struct A>;
protected:
struct ArcTNode //弧结点的结构
{
int adjvex; //邻接顶点的序号
ArcT arc; //弧信息
};
struct VerTNode //顶点的结构
{
VerT vex; //顶点信息
list<ArcTNode> arcs; //list 链表类存储该顶点所有出弧的信息
};
vector<VerTNode> vexs; //vector 向量类存储所有顶点
int LocateVex(VerT u)const
{//返回与 u 有相同关键字的顶点在图中的序号,不存在则返回 -1
for(int i = 0; i < vexs.size(); i++)
if EQ(u, vexs[i].vex)
return i;
return -1;
}
public:
AdjListGraph()
{//构造函数
ArcNum = 0;
}
VerT GetVex(int i)const
{//i 是图中某个顶点的序号,返回顶点[i]的信息
if (i >= vexs.size() || i < 0)
exit(1);
return vexs[i].vex;
}
void CreateFromFile(char* FileName)
{//由数据文件创建图
int i, j, k;
VerT v1, v2;
ArcTNode e;
VerTNode v;
ifstream fin(FileName);
fin >> boolalpha >> Directed;
fin >> boolalpha >> Weight;
fin >> j; //输入顶点数
for(i = 0; i < j; i++)
{
InputVertexFromFile(fin, v.vex);
vexs.push_back(v); //加到末尾
}
fin >> ArcNum; //输入弧数
for(k = 0; k < ArcNum; ++k)
{
InputVertexNameFromFile(fin, v1); //输入弧尾
InputVertexNameFromFile(fin, v2); //输入弧头
i = LocateVex(v1); //弧尾的序号
j = LocateVex(v2); //弧头的序号
e.adjvex = j;
if (Weight) //带权图
fin >> e.arc.weight;
InputArcFromFile(fin, e.arc);
vexs[i].arcs.push_front(e); //加入表头
if (!Directed) //无向
{
e.adjvex = i;
vexs[j].arcs.push_front(e); //加入表头
}
}
fin.close();
}
bool PutVex(VerT v, VerT value)
{//对顶点 v 赋新值 value
int k = LocateVex(v);
if (k < 0)
return false;
vexs[k].vex = value;
return true;
}
void InsertVex(VerT v)
{//在图中增添新顶点 v (不增添弧)
VerTNode vv;
vv.key = v;
vexs.push_back(vv);
}
bool DeleteVex(VerT v)
{//删除图中顶点 v 及其相关的弧
int i, k;
list<ArcTNode>::iterator p;
k = LocateVex(v);
ArcNum -= vexs[k].arcs.size();
vexs.erase(vexs.begin() + k); //删除 vexs[k]
for(i = 0; i < vexs.size(); i++)
{
for(p = vexs[i].arcs.begin(); p != vexs[i].arcs.end(); p++)
if (p->adjvex == k)
{
vexs[i].arcs.erase(p); //删除指向 k 的弧
if (Directed) //有向图
ArcNum--;
break;
}
for(p = vexs[i].arcs.begin(); p != vexs[i].arcs.end(); p++)
if (p->adjvex > k)
p->adjvex--; //序号减 1
}
return true;
}
bool InsertArc(VerT v, VerT w)
{//增添弧<v,w>,若是无向的,则还增添对称弧<w,v>
ArcTNode e;
int i, j;
string s1 = "边", s2 = "——"; //无向的情况
i = LocateVex(v);
j = LocateVex(w);
if (i < 0 || j < 0)
return false;
ArcNum++;
e.adjvex = j;
InputArc(Directed, Weight, v, w, e.arc); //输入弧的信息
vexs[i].arcs.push_front(e);
if (!Directed)
{
e.adjvex = i;
vexs[j] = arcs.push_front(e);
}
return true;
}
bool DeleteArc(VerT v, VerT w)
{//删除弧<v,w>,若是无向的,则还删除对称弧<w,v>
list<ArcTNode>::iterator p;
int i, j;
i = LocateVex(v);
j = LocateVex(w);
if (i < 0 || j < 0 || i == j)
return false;
for(p = vexs[i].arcs.begin(); p != vexs[i].arcs.end(); p++)
if (p->adjvex == j)
{
vexs[i].arcs.erase(p); //删除该结点
ArcNum--;
break;
}
if (!Directed) //无向
for(p = vexs[j].arcs.begin(); p != vexs[j].arcs.end(); p++)
if (p->adjvex == i)
{
vexs[j].arcs.erase(p); //删除该结点
break;
}
return true;
}
int FirstAdjVex(int i)const
{//返回顶点[i]的第一个邻接顶点,不存在则返回 -1
list<ArcTNode>::const_iterator p = vexs[i].arcs.begin(); //只读指针
if (p != vexs[i].arcs.end())
return p->adjvex;
else
return -1;
}
int NextAdjVex(int i, int j)const
{//返回顶点[i](相对于顶点[j])的下一个邻接顶点的序号,不存在则返回 -1
list<ArcTNode>::const_iterator p;
for(p = vexs[i].arcs.begin(); p != vexs[i].arcs.end(); p++)
if (p->adjvex == j)
{
p++;
break;
}
if (p == vexs[i].arcs.end())
return -1;
else //p->adjvex == j
return p->adjvex;
}
void Display()const
{//输出邻接表存储结构的图
int i;
list<ArcTNode>::const_iterator p;
string s1 = "无向", s2 = "无权图";
if (Directed)
s1 = "有向";
if (Weight)
s2 = "带权图";
cout << vexs.size() << "个顶点" << ArcNum << "条弧的" << s1 << s2
<< "。顶点依次是:";
for(i = 0; i < vexs.size(); i++)
{
for(p = vexs[i].arcs.begin(); p != vexs[i].arcs.end(); p++)
if (Directed || i < p->adjvex) //有向或无向两次中的一次,输出弧
OutputArc(Directed, Weight, vexs[i].vex,vexs[p->adjvex].vex,p->arc);
cout << endl;
}
}
int GetVexNumOrArcNum(int i)const
{//返回图的顶点数(i = 0)或弧数(i = 1)
if (i == 0)
return vexs.size();
else
return ArcNum;
}
};
3. 图的深度优先遍历和广度优先遍历
对图的搜索就是对图中顶点的遍历,也就是根据某种规则对图中的每个顶点访问一次且仅访问一次。为了不重复访问所有的顶点,需要为顶点向量设立一个访问标志数组 Visited[] ,并置其初值为 false(未被访问)。遍历时只访问那些未被访问过的顶点,且在访问后,将其访问标志的值改为 true 。当所有的顶点访问标志都为 true 时,则已完成遍历。遍历一般从图的第一个顶点开始,接下来并不是按照顶点的存储顺序遍历,而是按照顶点的邻接关系确定遍历的顺序。有两个搜索原则:深度优先遍历和广度优先遍历。
深度优先遍历算法的主要思想是:在访问一个顶点(假设为 A )之后,不是依照顶点的存储顺序访问下一个顶点(假设为 B ),而是先依次访问顶点 A 的所有邻接顶点。并且访问了顶点 A 的第一个邻接顶点(假设为 C )之后,又去先访问顶点 C 的所有邻接顶点。所以,对顶点 B (假设它与顶点 A 不连通)的访问是在对与 A 连通的所有顶点的访问之后进行的。这类算法用递归比较方便。
广度优先遍历算法的主要思想是:先访问图的第一个顶点,然后依次访问这个顶点的所有邻接顶点,再依次访问这些邻接顶点的所有邻接顶点。这需要建立一个先进先出的队列,依次将访问过的顶点入队。当前一个顶点的所有邻接顶点都被访问过了,就出队一个顶点,在访问这个顶点的所有邻接顶点并将这些邻接顶点入队,直至所有顶点都被访问过了。
所谓的第一个邻接顶点、第二个邻接顶点不是由图的拓扑关系(逻辑结构)决定的,它取决于图的存储(物理)结构。若存储结构不同,则它的某个顶点的第一个邻接顶点、第二个邻接顶点也可能不同。这会造成同一个图的同一种搜索得出的结果不同。当然,对任一种图的存储结构,如果顶点的输入顺序不同,搜索的顺序也不同。
4. 图的应用
4.1 无向图的连通分量和生成树
具有 n 个顶点的无向连通图至少有 n-1 条边,如果只有 n-1 条边,则不会形成环,这样的图称为 “生成树” 。连通图可通过遍历构造生成树,非连通图的每个连通分量可构造一棵生成树,整个非连通图构造为生成森林。
推荐阅读:
数据结构之无向图的连通分量和生成树
4.2 最小生成树
推荐阅读:
Prim 算法(1)
最小生成树-Prim算法和Kruskal算法
最小生成树(普利姆算法、克鲁斯卡尔算法)
4.3 关节点和重连通分量
推荐阅读:
重连通分量 (Biconnected Component)
8.4 图的连通性—关节点和重连通分量
连通图关节点求解详解--tarjan算法,重连通分量的求解--栈
4.4 拓扑排序和关键路径
详解图的应用(最小生成树、拓扑排序、关键路径、最短路径)
拓扑排序和关键路径