前言:
图和树一样都是一种非线性结构。和前面的另外两种数据结构线性表和树相比,图是一种更加灵活,更加多变(可以具有的统一性质更少了)的数据结构。线性表是一对一的线性结构,树是一对多的非线性结构,而图则是任意数据节点间,多对多的非线性结构,它的每一个节点都可以与其他任意多个节点相关联。灵活性使得图可以用来描述、求解我们实际生活中的问题,同时也使得我们使用它的难度加大了。
一、图的存储表示方法
1. 以邻接矩阵为存储结构
struct information //边的附带信息
{
int r;
int info;
};
struct matrix //邻接矩阵储存图
{
int vexnum; //顶点数
int arcnum; //边数
char* vexs; //节点数组
information** acrs; //矩阵
};
从中可以看出,用邻接矩阵来储存图的话,主要的空间耗费在于储存节点连接信息的矩阵上,如果是比较稀疏的图(即边数较少),或者是无向图(边没有方向)的话,那么这种存储方式无疑会带来极大的浪费。
2. 以邻接表为存储结构
struct edgenode
{
int num;
char nodedata;
edgenode* next;
};
struct vnode
{
char nodedata;
edgenode* first;
};
从中可以看出,相较于邻接矩阵,用邻接表来存储图的话,有效的减少了对空间的浪费。实际上,这里邻接表的方法就是把邻接矩阵中的每一行,分别组织为一个单链表,由此去除了没有用到的空间占用。
二、建立 graph 类并初始化
class graph
{
public:
matrix realgraph; //邻接矩阵
vnode* adjlist; //邻接表
int** adj; //输入边信息的矩阵
int n, e; //n为节点数,e为边数
void create_matrix();
void creat_list(); //建立一个邻接表储存矩阵的图
int getnumber(char word);
void DFSvisit();
void DFS(matrix g, int v);
int getFirstNeighbor(matrix g, int v);
int getNextNeighbor(matrix g, int v, int w);
void BFSvisit();
void BFS(matrix g, int iTemp);
private:
bool* visited; //邻接矩阵节点遍历判断
bool* visited2; //邻接表节点遍历判断
queue<int> qMatrixBfs; //队列 用于邻接矩阵广度优先遍历
queue<int> qListBfs; //队列 用于邻接表广度优先遍历
};
graph::graph()
{
cout << "请分别输入图的顶点数和边数" << endl;
cin >> n >> e;
realgraph.vexnum = n;
realgraph.arcnum = e;
realgraph.vexs = new char[n];
realgraph.acrs = new information*[n];
for (int i = 0; i <= n - 1; i++)
{
realgraph.acrs[i] = new information[n];
}
adjlist = new vnode[n];
visited = new bool[n];
visited2 = new bool[n];
for (int i = 0; i <= n - 1; i++)
{
visited[i] = false;
visited2[i] = false;
}
adj = new int*[e];
for (int i = 0; i <= e - 1; i++)
{
adj[i] = new int[2];
}
}
三、邻接矩阵的深度优先和广度优先遍历
1. 初始化邻接矩阵
void graph::create_matrix()
{
int i = 0;
int j = 0;
char** a = new char*[e];
for (int i = 0; i <= e - 1; i++)
{
a[i] = new char[2];
}
cout << "请输入" << n << "个字符用做图的顶点(邻接矩阵)" << endl;
for (i = 0; i <= realgraph.vexnum - 1; i++)
{
cin >> realgraph.vexs[i];
}
for (i = 0; i < realgraph.vexnum; i++) //初始化矩阵为所有顶点都没有边
{
for (j = 0; j < realgraph.vexnum; j++)
{
realgraph.acrs[i][j].r= 0;
}
}
cout << "请依次输入有边相连的两个顶点序号(如:a b)" << endl;
for (i = 0; i < realgraph.arcnum; i++)
{
cin >> a[i][0] >> a[i][1];
adj[i][0] = getnumber(a[i][0]);
adj[i][1] = getnumber(a[i][1]);
}
for (int i = 0; i <= realgraph.arcnum - 1; i++)
{
realgraph.acrs[adj[i][0]][adj[i][1]].r = 1;
/*realgraph.acrs[adj[i][1]][adj[i][0]].r = 1;*/
}
}
int graph::getnumber(char word)
{
for (int i = 0; i <= realgraph.vexnum - 1; i++)
{
if (word == realgraph.vexs[i])
return i;
}
cout << "请输入正确符号!" << endl;
return 100;
}
getnumber( ) 函数用于传入一个字符,返回该字符在节点数组中的位置。通过这种方法,获得一条边的两个端点的顺序值,由此建立一个矩阵。
2. 邻接矩阵深度优先遍历
void graph::DFSvisit()
{
int i;
for (i = 0; i < realgraph.vexnum; i++)
{
if (visited[i] == false)
{
DFS(realgraph, i);
}
}
}
void graph::DFS(matrix g, int v) //递归函数体
{
int i;
int w;
cout << g.vexs[v] << " ";
visited[v] = true;
w = getFirstNeighbor(g, v); //v的邻接节点w
while (w>=0)
{
if (visited[w] == false)
{ //如果w还没有被遍历过,那么递归遍历w及其邻接节点,如果已经遍历过,那么遍历w之后的邻接点
DFS(g, w);
} //如果此邻接点已被遍历,那么就从它的下一个邻接点开始遍历
w = getNextNeighbor(g, v, w); //获得当前节点v的邻接点w之后的一个邻接点
}
}
int graph::getFirstNeighbor(matrix g, int v)
{
for (int i = 0; i < g.vexnum; i++)
{
if (g.acrs[v][i].r != 0 && v != i) //排除邻接矩阵中对角线上的点
{
return i;
}
}
return -1;
}
int graph::getNextNeighbor(matrix g, int v, int w)
{
for (int i = w + 1; i < g.vexnum; i++)
{
if (g.acrs[v][i].r != 0 && v != i)
{
return i;
}
}
return -1;
}
深度优先遍历邻接矩阵的方法其实很简单,就是对于每一个节点(即矩阵的每一行),查看它的第一个相邻的节点是什么(有边即为相邻),对此相邻节点再查看它的第一个相邻的节点,利用递归函数重复此过程。若是当前结点的第一个相邻节点已经遍历过了,那么就查看它的下一个相邻节点,然后再调用递归函数,就和上面的步骤是一样的了。
3.邻接矩阵广度优先遍历
void graph::BFSvisit()
{
int i;
for (i = 0; i < realgraph.vexnum; i++)
{
visited[i] = false;
}
for (i = 0; i < realgraph.vexnum; i++) //对所有没有遍历到的节点调用BFS函数遍历
{
if (visited[i] == false)
{
BFS(realgraph, i);
}
}
}
void graph::BFS(matrix g, int iTemp)
{
int i;
cout << g.vexs[iTemp]; //遍历输出当前节点
visited[iTemp] = true;
qMatrixBfs.push(iTemp);
while (!qMatrixBfs.empty()) //循环直至队列为空
{
iTemp = qMatrixBfs.front(); //获取队列的当前头结点
qMatrixBfs.pop();
for (i = 0; i < g.vexnum; i++)
{
//然后遍历此节点的所有邻接节点并将它们加入队列当中,重复此步骤
if (g.acrs[iTemp][i].r != 0 && iTemp != i && visited[i] == false)
{
cout << g.vexs[i]<<" ";
visited[i] = true;
qMatrixBfs.push(i);
}
}
}
}
广度优先遍历比深度优先遍历还要简单。主要的操作对象是一个队列,这里我没有自己建立一个队列结构。主要的思想就是将当前节点的所有相邻节点都遍历一次,且遍历的同时将它们依次放入队列之中。然后再弹出队列的队首,对它重复上面的操作,直至队列为空。不过要注意的是,这样遍历完的只是联通的节点,对于一些孤立的点没有办法遍历到,所以BFSvisit( )函数中的循环不能少。
四、邻接表的深度优先和广度优先遍历
1.初始化邻接表
void graph::creat_list()
{
int i;
cout << "请输入" << n << "个字符用做图的顶点(邻接表)" << endl;
for (i = 0; i < n; i++)
{
cin >> adjlist[i].nodedata;
adjlist[i].first = NULL;
}
edgenode* edgTempLeft;
edgenode* edgTempRight;
int iTempLeft, iTempRight;
for (i = 0; i < e; i++)
{
edgTempLeft = new edgenode;
edgTempRight = new edgenode;
iTempLeft = adj[i][0];
iTempRight = adj[i][1]; //获得边的两个端点
//其实就是根据输入的边(如ab),新建一个节点b,然后将它插入到a的右边的链表中(插入到头部)
edgTempRight->nodedata = adjlist[iTempRight].nodedata; //如果是有向图则只用这部分即可
edgTempRight->num = iTempRight;
edgTempRight->next = adjlist[iTempLeft].first;
adjlist[iTempLeft].first = edgTempRight;
edgTempLeft->nodedata = adjlist[iTempLeft].nodedata; //如果是无向图则需要将相反的方向也链接起来
edgTempLeft->num = iTempLeft;
edgTempLeft->next = adjlist[iTempRight].first;
adjlist[iTempRight].first = edgTempLeft;
}
}
注释的部分就是邻接表的主要思想,或者说原理
2.邻接表的深度优先遍历
void graph::DFS_list(vnode* adjlist, int v)
{
cout << adjlist[v].nodedata << " "; //完成对当前节点的遍历
visited2[v] = true;
edgenode* edgTemp; //寻找与当前节点相连的第一个节点,即链表的首节点
edgTemp = adjlist[v].first;
while (edgTemp!=NULL) //如果有相连的节点且没有遍历过的话则递归遍历
{
if (visited2[edgTemp->num] == false)
{
DFS_list(adjlist, edgTemp->num);
}
edgTemp = edgTemp->next; //没有就选择下一个相连的节点,继续循环递归直至所有相连的节点都遍历过了
}
}
3.邻接表的广度优先遍历
void graph::BFS_list(vnode* adjlist, int v)
{
int i;
edgenode* edgTemp ; //临时节点
for (i = 0; i <= n - 1; i++)
visited2[i] = false;
cout << adjlist[v].nodedata << " "; //完成对当前节点的遍历
visited2[v] = true;
qListBfs.push(v);
while (qListBfs.empty != true) //循环直至所有的节点都遍历了
{
edgTemp = adjlist[qListBfs.front()].first; //弹出队首,遍历所有和它相连且没有遍历过的节点
qListBfs.pop();
while (edgTemp != NULL)
{
if (visited2[edgTemp->num] == false)
{
cout << edgTemp->nodedata << " "; //完成一次遍历,并且将之加入队列当中
qListBfs.push(edgTemp->num);
visited2[edgTemp->num] = true;
}
edgTemp = edgTemp->next; //下一个节点
}
}
}
可以看出邻接表的深度优先和广度优先遍历的实现思路,都和邻接矩阵的基本一样。
五、总结
以上就是邻接矩阵、邻接表的初始化和两种主要的遍历方法,两者的深度优先和广度优先遍历都十分的相似,个人觉得这是因为邻接表和邻接矩阵的本质其实是相同的。
另外,对于队列结构和递归函数的使用方法,有一些可以总结的地方,个人感觉队列和栈一样往往用来保存某种状态(?)或者说路径点(?),将外循环的跳出条件设置为队列或栈为空(以及其他相应条件),然后在内循环中进行相应的入队、入栈操作,最后将出队、出栈操作放置在两层循环的中间,而各类判定语句(if)就按需要设置,感觉是遍历非线性数据结构的一个思路吧,大概。