1、数据结构中图的应用 图在数据结构中应用十分广泛,对于图来说最重要的当然是算法,而且相当的一部分都是很专业的,一般的人几乎不会接触到;相对而言,结构就显得分量很轻。你可以看到关于图中元素的操作很少,远没有单链表那里列出的一大堆“接口”。一个结构如果复杂,那么能确切定义的操作就很有限。基本储存方法不管怎么说,还是先得把图存起来。不要看书上列出了好多方法,根本只有一个邻接矩阵。如果矩阵是稀疏的,那就可以用十字链表来储存矩阵(见前面的稀疏矩阵(十字链表)。如果我们只关系行的关系,那么就是邻接表(出边表);反之,只关心列的关系,就是逆邻接表(入边表)。下面给出两种储存方法的实现。#ifndef Grap
2、hmem_H#define Graphmem_H#include #include using namespace std;template class Network;const int maxV = 20;/最大节点数template class AdjMatrixfriend class Networkname, dist, AdjMatrix ;public:AdjMatrix() : vNum(0), eNum(0)vertex = new namemaxV; edge = new dist*maxV;for (int i = 0; i maxV; i+) edgei = new d
3、istmaxV;AdjMatrix()for (int i = 0; i maxV; i+) delete edgei;delete edge; delete vertex;bool insertV(name v)if (find(v) return false;vertexvNum = v;for (int i = 0; i maxV; i+) edgevNumi = NoEdge;vNum+; return true;bool insertE(name v1, name v2, dist cost)int i, j;if (v1 = v2 | !find(v1, i) | !find(v2
4、, j) return false;if (edgeij != NoEdge) return false;edgeij = cost; eNum+; return true;name& getV(int n) return vertexn; /没有越界检查int nextV(int m, int n)/返回m号顶点的第n号顶点后第一个邻接顶点号,无返回-1for (int i = n + 1; i vNum; i+) if (edgemi != NoEdge) return i;return -1;private:int vNum, eNum;dist NoEdge, *edge; name
5、*vertex;bool find(const name& v)for (int i = 0; i vNum; i+) if (v = vertexi) return true;return false;bool find(const name& v, int& i)for (i = 0; i vNum; i+) if (v = vertexi) return true;return false;template class LinkedListfriend class Networkname, dist, LinkedList ;public:LinkedList() : vNum(0),
6、eNum(0) LinkedList()for (int i = 0; i vNum; i+) delete verticesi.e;bool insertV(name v)if (find(v) return false;vertices.push_back(vertex(v, new list);vNum+; return true;bool insertE(const name& v1, const name& v2, const dist& cost)int i, j;if (v1 = v2 | !find(v1, i) | !find(v2, j) return false;for
7、(list:iterator iter = verticesi.e-begin();iter != verticesi.e-end() & iter-vID end()verticesi.e-push_back(edge(j, cost); eNum+; return true;if (iter-vID = j) return false;verticesi.e-insert(iter, edge(j, cost); eNum+; return true;name& getV(int n) return verticesn.v; /没有越界检查int nextV(int m, int n)/返
8、回m号顶点的第n号顶点后第一个邻接顶点号,无返回-1for (list:iterator iter = verticesm.e-begin();iter != verticesm.e-end(); iter+) if (iter-vID n) return iter-vID;return -1;private:bool find(const name& v)for (int i = 0; i vNum; i+) if (v = verticesi.v) return true;return false;bool find(const name& v, int& i)for (i = 0; i
9、vNum; i+) if (v = verticesi.v) return true;return false;struct edgeedge() edge(int vID, dist cost) : vID(vID), cost(cost) int vID;dist cost;struct vertexvertex() vertex(name v, list* e) : v(v), e(e) name v;list* e;int vNum, eNum;vector vertices;#endif这个实现是很简陋的,但应该能满足后面的讲解了。现在这个还什么都不能做,不要急,在下篇将讲述图的DF
10、S和BFSDFS和BFS对于非线性的结构,遍历都会首先成为一个问题。和二叉树的遍历一样,图也有深度优先搜索(DFS)和广度优先搜索(BFS)两种。不同的是,图中每个顶点没有了祖先和子孙的关系,因此,前序、中序、后序不再有意义了。仿照二叉树的遍历,很容易就能完成DFS和BFS,只是要注意图中可能有回路,因此,必须对访问过的顶点做标记。 最基本的有向带权网#ifndef Graph_H #define Graph_H #include #include using namespace std; #include Graphmem.h template class Network public: N
11、etwork() Network(dist maxdist) data.NoEdge = maxdist; Network() bool insertV(name v) return data.insertV(v); bool insertE(name v1, name v2, dist cost) return data.insertE(v1, v2, cost); name& getV(int n) return data.getV(n); int nextV(int m, int n = -1) return data.nextV(m, n); int vNum() return dat
12、a.vNum; int eNum() return data.eNum; protected: bool* visited; static void print(name v) cout v; private: mem data; ; #endif 你可以看到,这是在以mem方式储存的data上面加了一层外壳。在图这里,逻辑上分有向、无向,带权、不带权;储存结构上有邻接矩阵和邻接表。也就是说分开来有8个类。为了最大限度的复用代码,继承关系就非常复杂了。但是,多重继承是件很讨厌的事,什么覆盖啊,还有什么虚拟继承,我可不想花大量篇幅讲语言特性。于是,我将储存方式作为第三个模板参数,这样一来就省得涉
13、及虚拟继承了,只是这样一来这个Network的实例化就很麻烦了,不过这可以通过typedef或者外壳类来解决,我就不写了。反正只是为了让大家明白,真正要用的时候,最好是写专门的类,比如无向无权邻接矩阵图,不要搞的继承关系乱七八糟。 DFS和BFS的实现public: void DFS(void(*visit)(name v) = print) visited = new boolvNum(); for (int i = 0; i vNum(); i+) visitedi = false; DFS(0, visit); delete visited; protected: void DFS(in
14、t i, void(*visit)(name v) = print) visit(getV(i); visitedi = true; for (int n = nextV(i); n != -1; n = nextV(i, n) if (!visitedn) DFS(n, visit); public: void BFS(int i = 0, void(*visit)(name v) = print)/n没有越界检查 visited = new boolvNum(); queue a; int n; for (n = 0; n vNum(); n+) visitedn = false; vis
15、itedi = true; while (i != -1)/这个判断可能是无用的 visit(getV(i); for (n = nextV(i); n != -1; n = nextV(i, n) if (!visitedn) a.push(n); visitedn = true; if (a.empty() break; i = a.front(); a.pop(); delete visited; DFS和BFS函数很难写得像树的遍历方法那么通用,这在后面就会看到,虽然我们使用了DFS和BFS的思想,但是上面的函数却不能直接使用。因为树的信息主要在节点上,而图的边上还有信息。 测试程序#
16、include using namespace std; #include Graph.h int main() Networkchar, int, LinkedList a; a.insertV(A); a.insertV(B); a.insertV(C); a.insertV(D); a.insertE(A, B, 1); a.insertE(A, C, 2); a.insertE(B, D, 3); cout DFS: ; a.DFS(); cout endl; cout BFS: ; a.BFS(); cout endl; return 0; 老实说,这个类用起来真的不是很方便。不过能
17、说明问题就好。 无向图要是在纸上随便画画,或者只是对图做点示范性的说明,大多数人都会选择无向图。然而在计算机中,无向图却是按照有向图的方法来储存的存两条有向边。实际上,当我们说到无向的时候,只是忽略方向在纸上画一条线,难不成那线“嗖”的就出现了,不是从一头到另一头画出来的? 无向图有几个特有的概念,连通分量、关节点、最小生成树。下面将分别介绍,在此之前,先完成无向图类的基本操作。无向图类template class Graph : public Networkpublic:Graph() Graph(dist maxdist) : Network (maxdist) bool insertE(
18、name v1, name v2, dist cost)if (Network:insertE(v1, v2, cost)return Network:insertE(v2, v1, cost);return false;仅仅是添加边的时候,再添加一条反向边,很简单。连通分量这是无向图特有的,有向图可要复杂多了(强、单、弱连通),原因就是无向图的边怎么走都行,有向图的边好像掉下无底深渊就再也爬不上来了。有了DFS,求连通分量的算法就变得非常简单了对每个没有访问的顶点调用DFS就可以了。void components()visited = new boolvNum(); int i, j = 0
19、;for (i = 0; i vNum(); i+) visitedi = false;cout Components: endl;for (i = 0; i vNum(); i+)if (!visitedi) cout ( +j ); DFS(i); cout = dfni) cout =就显得很尴尬了,因为只能等于不可能大于。还要注意的是,生成树的根(DFS的起始点)是单独判断的。void articul()dfn = new intvNum(); low = new intvNum(); int i, j = 0, n;for(i = 0; i vNum(); i+) dfni = lo
20、wi = 0;/初始化for (i = 0; i vNum(); i+)if (!dfni)cout ( +j ); dfni = lowi = count = 1;if (n = nextV(i) != -1) articul(n); bool out = false;/访问树根while (n = nextV(i, n) != -1)if (dfnn) continue;if (!out) cout getV(i); out = true; /树根有不只一个子女articul(n);/访问其他子女cout endl;delete dfn; delete low;private:void a
21、rticul(int i)dfni = lowi = +count;for (int n = nextV(i); n != -1; n = nextV(i, n)if (!dfnn)articul(n);if (lown = dfni) cout getV(i);/这里只可能else if (dfnn lowi) lowi = dfnn;/回边判断int *dfn, *low, count; 最小生成树说人是最难伺候的,真是一点不假。上面刚刚为了“提高可靠性”添加了几条多余的边,这会儿又来想办法怎么能以最小的代价把所有的顶点都连起来。可能正是人的这种精神才使得人类能够进步吧看着现在3GHz的C
22、PU真是眼红啊,我还在受500MHz的煎熬,然后再想想8086 正如图的基本元素是顶点和边,从这两个方向出发,就能得到两个算法Kruskal算法(从边出发)、Prim算法(从顶点出发)。据说还有别的方法,恕我参考资料有限,不能详查。 最小生成树的储存显然用常用的树的储存方法来储存没有必要,虽然名曰“树”,实际上,这里谁是谁的“祖先”、“子孙”并不重要。因此,用如下的MSTedge结构数组来储存就可以了。 template class MSTedge public: MSTedge() MSTedge(int v1, int v2, dist cost) : v1(v1), v2(v2), co
23、st(cost) int v1, v2; dist cost; bool operator (const MSTedge& v2) return (cost v2.cost); bool operator (const MSTedge& v2) return (cost v2.cost); bool operator = (const MSTedge& v2) return (cost = v2.cost); ; Kruskal算法最小生成树直白的讲就是,挑选N1条不产生回路最短的边。Kruskal算法算是最直接的表达了这个思想在剩余边中挑选一条最短的边,看是否产生回路,是放弃,不是选定然后重
24、复这个步骤。说起来倒是很简单,做起来就不那么容易了判断是否产生回路需要并查集,在剩余边中找一条最短的边需要最小堆(并不需要对所有边排序,所以堆是最佳选择)。 Kruskal算法的复杂度是O(eloge),当e接近N2时,可以看到这个算法不如O(N2)的Prim算法,因此,他适合于稀疏图。而作为稀疏图,通常用邻接表来储存比较好。另外,对于邻接矩阵储存的图,Kruskal算法比Prim算法占不到什么便宜(初始还要扫描N2条“边”)。因此,最好把Kruskal算法放在Link类里面。 template int Link:MinSpanTree(MSTedge* a) MinHeapMSTedge E; int i, j, k, l = 0; MFSets V(vNum); list:iterator iter; for (i = 0; i begin(); iter != verticesi.e-end(); iter+) E.insert(MSTedge(i, iter-vID, iter-cost);/建立边的堆 for (i = 0; i eNum & l vNum; i+)/Kruskal Start j = V.find(E.top()
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1