图,一种复杂的数据结构,在实际生活中起着举足轻重的作用。如路网(线路规划),社交网络描述等等。现在我们就以下面这个简单无向图(图1)为例,来说明使用邻接矩阵 实现图在计算机中的存储和操作的方法。
图1
不同于树,我们在习惯上称呼如图中数字编号的店为顶点而非节点。在树的概念中,存在一个特殊的节点称之为根,其起着好似渔网的主绳的作用,缺之不可。而在图这种数据结构中,更加倾向于平等地看待每一个顶点。
在下面给出的实现中,我们用一维数组来表示邻接矩阵。若图有n个顶点,则数组大小即为n*n。第row行,col列的元素实际上存储在数组下标为row*n+col的位置。
关于邻接矩阵的定义这里不再赘述,下图2给出图1所示简单无向图的邻接矩阵,希望能够帮助读者理解。
图2
下面给出以邻接矩阵表示图的C++实现,其中封装了对图的常用操作(具体见代码注释):
/*节点类*/
class Node
{
public:
Node(char identifier = 0);
char m_identifier; //顶点编号
bool m_isVisited; //顶点访问标志位:true表示已经被访问
};
Node::Node(char identifier)
{
m_identifier = identifier;
m_isVisited = false;
}
/*图类*/
class Graph
{
public:
Graph(int capacity);
~Graph();
void resetNode(); //重置所有顶点的访问标志位为false,未访问
bool addNode(Node *pNode); //添加新顶点
bool addEdgeForUndirectedGraph(int row, int col, int val = 1); //添加边以构造无向图,val表示权值,默认连通
bool addEdgeForDirectedGraph(int row, int col, int val = 1); //添加边以构造有向图,val表示权值,默认连通
void printMatrix(); //打印邻接矩阵
void depthFirstTraverse(int nodeIndex); //深度优先遍历,指定第一个点
void widthFirstTraverse(int nodeIndex); //广度优先遍历,指定第一个点
private:
bool getValueOfEdge(int row, int col, int &val); //获取边权值
void widthFirstTraverseImplement(vector<int> preVec); //利用vector实现广度优先遍历
int m_iCapacity; //图容量,即申请的数组空间最多可容纳的顶点个数
int m_iNodeCount; //图的现有顶点个数
Node *m_pNodeArray; //存放顶点的数组
int *m_pMatrix; //为了方便,用一维数组存放邻接矩阵
};
Graph::Graph(int capacity)
{
m_iCapacity = capacity;
m_iNodeCount = 0;
m_pNodeArray = new Node[m_iCapacity];
m_pMatrix = new int[m_iCapacity*m_iCapacity];
for (int i = 0;i < m_iCapacity*m_iCapacity;i++) //初始化邻接矩阵
{
m_pMatrix[i] = 0;
}
}
Graph::~Graph()
{
delete []m_pNodeArray;
delete []m_pMatrix;
}
void Graph::resetNode()
{
for (int i = 0;i < m_iNodeCount;i++)
{
m_pNodeArray[i].m_isVisited = false;
}
}
bool Graph::addNode(Node *pNode)
{
if (pNode == NULL)
return false;
m_pNodeArray[m_iNodeCount].m_identifier = pNode->m_identifier;
m_iNodeCount++;
return true;
}
bool Graph::addEdgeForUndirectedGraph(int row, int col, int val)
{
if (row < 0 || row >= m_iCapacity)
return false;
if (col < 0 || col >= m_iCapacity)
return false;
m_pMatrix[row*m_iCapacity + col] = val;
m_pMatrix[col*m_iCapacity + row] = val;
return true;
}
bool Graph::addEdgeForDirectedGraph(int row, int col, int val)
{
if (row < 0 || row >= m_iCapacity)
return false;
if (col < 0 || col >= m_iCapacity)
return false;
m_pMatrix[row*m_iCapacity + col] = val;
return true;
}
void Graph::printMatrix()
{
for (int i = 0;i < m_iCapacity;i++)
{
for (int k = 0;k < m_iCapacity;k++)
cout << m_pMatrix[i*m_iCapacity + k] << " ";
cout << endl;
}
}
void Graph::depthFirstTraverse(int nodeIndex)
{
int value = 0;
//访问第一个顶点
cout << m_pNodeArray[nodeIndex].m_identifier << " ";
m_pNodeArray[nodeIndex].m_isVisited = true;
//访问其他顶点
for (int i = 0;i < m_iCapacity;i++)
{
getValueOfEdge(nodeIndex, i, value);
if (value != 0) //当前顶点与指定顶点连通
{
if (m_pNodeArray[i].m_isVisited == true) //当前顶点已被访问
continue;
else //当前顶点没有被访问,则递归
{
depthFirstTraverse(i);
}
}
else //没有与指定顶点连通
{
continue;
}
}
}
void Graph::widthFirstTraverse(int nodeIndex)
{
//访问第一个顶点
cout << m_pNodeArray[nodeIndex].m_identifier << " ";
m_pNodeArray[nodeIndex].m_isVisited = true;
vector<int> curVec;
curVec.push_back(nodeIndex); //将第一个顶点存入一个数组
widthFirstTraverseImplement(curVec);
}
void Graph::widthFirstTraverseImplement(vector<int> preVec)
{
int value = 0;
vector<int> curVec; //定义数组保存当前层的顶点
for (int j = 0;j < (int)preVec.size();j++) //依次访问传入数组中的每个顶点
{
for (int i = 0;i < m_iCapacity;i++) //传入的数组中的顶点是否与其他顶点连接
{
getValueOfEdge(preVec[j], i, value);
if (value != 0) //连通
{
if (m_pNodeArray[i].m_isVisited==true) //已经被访问
{
continue;
}
else //没有被访问则访问
{
cout << m_pNodeArray[i].m_identifier << " ";
m_pNodeArray[i].m_isVisited = true;
//保存当前点到数组
curVec.push_back(i);
}
}
}
}
if (curVec.size()==0) //本层次无被访问的点,则终止
{
return;
}
else
{
widthFirstTraverseImplement(curVec);
}
}
bool Graph::getValueOfEdge(int row, int col, int &val)
{
if (row < 0 || row >= m_iCapacity)
return false;
if (col < 0 || col >= m_iCapacity)
return false;
val = m_pMatrix[row*m_iCapacity + col];
return true;
}
特别需要提醒的是:
- 在实现图遍历时应用了C++标准数组模板vector,读者在使用时应该包含相关头文件。
- 构造边的时候封装了两个函数,一个是对于有向图,一个是对于无向图,读者对比两种图形的邻接矩阵和实现可以很容易理解。
另外,在前面对于二叉树的介绍中,我们使用了数组和链表两种方式来表示树。其实对于图来说,也拥有着两种表示方式。上述邻接矩阵的表示方法即使用数组,另外还有使用链表的如邻接表、十字链表、邻接多重表等等。他们在表示有向图、无向图和运算代价方面各有优劣,这里不再做详细介绍。有兴趣的读者朋友可以查阅相关资料。
点击这里下载完整源码,包括最小生成树等方法的封装。