【数据结构】图

本篇博文旨在介绍数据结构中的图;介绍了图以及图的有关概念;介绍了图的两种实现方式,并用代码进行了实现;介绍并实现了图的广度优先遍历和深度优先遍历;


图是数据结构中的一种非线性结构,由顶点以及顶点相连的边构成

图包括有向图和无向图

无向图中,当两个顶点存在连接关系时,不区分该连接是A到B点,还是B到A点


有向图中,两个顶点存在连接关系时,要区分是A可以到B,还是B可以到A


图的基本概念

完全图

图中,每个顶点都与其他顶点有连线,有N*(N-1)/2条边

权重

若图中的边具有数值信息,那么该数值就为该边的权重

临接顶点

一条边的两个顶点互为邻接顶点

一个顶点具有相邻边的个数

路径

图中的一个顶点A,通过其他顶点(x1,x2,x3....)到达一个顶点B后,这中间经过的顶点就为A顶点B顶点之间的路径

连通图和强连通图

在无向图中,当一个顶点A可以通过其他顶点到达顶点B时,A,B两个顶点就互为连通

如果一个图中的所有顶点都是连通的,那么该无向图称为连通图

在有向图中,若每一对顶点都存在路径,则称此图为强连通图

生成树

一个无向连通图的生成树是它的极小连通子图,若图中有N-1个顶点,那么该图的生成树有N-1个顶点

图的两种存储方式

邻接矩阵

将所有顶点的信息组成顶点表, 利用二维矩阵来表示图中的连通关系


邻接矩阵实现图

//方法1:邻接矩阵

template<typename V,typename W>
class GraphMatrix
{
public:
	GraphMatrix(V* vertexs,size_t n,const W& invalid = W(),bool IsDirected = false)
		:_vertexs(vertexs,vertexs + n),_isDirected(IsDirected)
	{
		_matrix = new W*[n];
		for (size_t i = 0; i < n; ++i)
		{
			_matrix[i] = new W[n];
			for (size_t j = 0; j < n; ++j)
			{
				_matrix[i][j] = invalid;
			}
		}
	}

	~GraphMatrix()
	{}

	int GetIndex(const V& v)
	{
		for (size_t i = 0; i < _vertexs.size(); ++i)
		{
			if (_vertexs[i] == v)
				return i;
		}
		assert(false);
		return -1;
	}

	void AddEdge(const V& v1,const V& v2,const W& w)
	{
		size_t src = GetIndex(v1);
		size_t des = GetIndex(v2);
		_matrix[src][des] = w;

		if (_isDirected == false)
			_matrix[des][src] = w;
	}
protected:
	vector<V> _vertexs;//定点的集合
	W** _matrix;//邻接矩阵边的集合
	bool _isDirected;//是否是有向图
};

void TestGraphMatrix()
{
	string city[] = { "北京","上海","广州","杭州","西安" };
	GraphMatrix<string,double> gpm(city,sizeof(city) / sizeof(city[0]));
	gpm.AddEdge("北京",300.3);
	gpm.AddEdge("北京",850.5);
	gpm.AddEdge("北京",299);
	gpm.AddEdge("北京","西安",475);
}

邻接表

使用数组存储顶点的信息,用链表来对链接关系进行存储


邻接表实现图

//方法2:用邻接表实现图
template<typename V,typename W>
class GraphLink
{
	typedef Edge<W> Node;
public:
	GraphLink()
	{}

	GraphLink(V* vertexs,bool IsDirected = false)
	{
		_vertexs.resize(n);
		for (size_t i = 0; i < n; ++i)
		{
			_vertexs[i] = vertexs[i];
		}
		_linkTables.resize(n,NULL);
	}

	~GraphLink()
	{}

	int GetIndex(const V& v)
	{
		for (size_t i = 0; i < _vertexs.size(); ++i)
		{
			if (v == _vertexs[i])
				return i;
		}
		assert(false); 
		return -1;
	}

	void _AddEdge(const size_t& src,const size_t& des,const W&w)
	{
		//进行头插
		Node* newNode = new Node(src,des,w);
		newNode->_next = _linkTables[src];
		_linkTables[src] = newNode;
	}
	void AddEdge(const V& v1,const W& w)
	{
		size_t src = GetIndex(v1);
		size_t des = GetIndex(v2);
		_AddEdge(src,w);

		if (_isDirected == false)
			_AddEdge(des,src,w);
	}
protected:
	vector<V> _vertexs;//顶点的集合,用来存储顶点的值
	vector<Node*> _linkTables;//边的集合
	bool _isDirected;//是否是有向图
};

邻接矩阵和邻接表的对比

1、当一个图需要经常查找两点之间的权值关系时,利用邻接矩阵进行查询,直接定位,效率高,时间复杂度为O(1)

而邻接表需要对链接表进行一一遍历,如果该点链接的顶点比较多,查找起来效率很低,时间复杂度为O(N)

2、当我们从存储空间上比较时,邻接表又是优于邻接矩阵的

邻接表只需要存储链接的顶点,而邻接矩阵却保存了所有顶点的关系(包括不连通的两个顶点的权值)

结论:一般情况下,稀疏图用邻接表,稠密图用邻接矩阵

图的深度优先和广度优先遍历

深度优先遍历

这里我们可以根据二叉树的前序遍历理解,当遍历一条路径时,一直访问直到其没有路径时返回,再遍历其他的路径

这里我们用递归实现

代码实现

注意:此代码需要些到邻接表实现的图里

	//用哈希来判断一个定点是否被访问过
	//时间复杂度为O(1)
	void DFS_ByVector(const V& src)
	{
		size_t isrc = GetIndex(src);

		//定义vector,用于判断是否访问过
		vector<bool> v;
		v.resize(_vertexs.size(),false);
		v[isrc] = true;
		_DFS_ByVector(isrc,v);
		cout << endl;
	}
	//判断定点是否访问过的时间复杂度为O(1)
	void _DFS_ByVector(const size_t src,vector<bool>&v)
	{
		Node* cur = _linkTables[src];
		cout << GetV(src) << " ";
		while (cur)
		{
			size_t dst = cur->_des;
			if (v[dst] == false)
			{
				v[dst] = true;
				_DFS_ByVector(dst,v);
			}
			cur = cur->_next;
		}
	}

广度优先遍历

这里我们也是利用二叉树来理解,这次用的是层序遍历,每一次遍历与该节点相邻的所有节点,遍历完成后,再遍历与相邻节点直接相邻的节点。

代码实现

注意:此代需要写到邻接表实现的图里

	void BFS_ByVector(const V&src)
	{
		//利用队列来存储
		queue<size_t> q;
		size_t isrc = GetIndex(src);
		q.push(isrc);
		
		//利用vector进行判断定点是否被访问过
		vector<bool> v;
		v.resize(_vertexs.size(),false);
		v[isrc] = true;

		while (!q.empty())
		{
			size_t tmp = q.front();
			cout << GetV(tmp) << " ";
			q.pop();
			Node* cur = _linkTables[tmp];
			while (cur)
			{
				size_t dst = cur->_des;
				//如果没有访问该节点,就进行访问,并标记
				if (v[dst] == false)
				{
					v[dst] = true;
					q.push(dst);
				}
				cur = cur->_next;
			}
		}
		cout << endl;
	}

最小生成树

在连通图中,有N个顶点,那么用N-1条边来所有顶点串起来,并且没有环

并且,这N-1条边都是连通图本身的边

贪心算法

在解决问题的时候,总是做出在当前情况下的最优解。

注意:当前最优,也就是局部最优。并不能保证整体最优

用贪心算法实现最小生成树的两种算法

Kruskal算法

每次选出权重最小的边,若该边的加入不会导致出现环,那么加入该边

下面的图截自数据结构书籍


Prim算法

从一个顶点开始,选取该点邻接的权重最小的边,如果该边不会造成环的出现,那么加入该边


克鲁斯卡尔求最小生成树的算法步骤

1、利用优先级队列将边按照权值的大小排列(由小到大)

2、依次访问优先级队列的以一个元素,如果最小生成树加上该边,没有构成环,则加入这条边

3、将优先级队列的首元素出队

注意点:

并查集(前篇博文有讲,不理解的童鞋可以点击链接进行查看http://www.voidcn.com/article/p-bztyudqy-ws.html)来判断一条边的两个点是否在一个集合中

如果已经在一个集合中,那么加入该边就会造成环的出现,就不加入该边

如果不在,就加入该边

下面给出克鲁斯卡尔算法的最小生成树的代码实现

注:此代码需要放入上面邻接表实现图的代码中

//求最小生成树
	bool Kruskal(GraphLink<V,W>& mintree)
	{
		mintree._vertexs = _vertexs;
		mintree._linkTables.resize(_vertexs.size(),NULL);
		mintree._isDirected = _isDirected;

		struct Compare
		{
			bool operator()(Node* l,Node* r)
			{
				return l->_weight < r->_weight;
			}
		};

		//定义优先级队列,把所有边进行存储
		priority_queue<Node*,vector<Node*>,Compare> pq;
		for (size_t i = 0; i < _vertexs.size(); ++i)
		{
			Node* cur = _linkTables[i];
			while (cur)
			{
				//只存储定顶点数比目标定点小的
				if (cur->_src < cur->_des)
					pq.push(cur);

				cur = cur->_next;
			}
		}

		//定义并查集,判断加入的两个定点是否带环
		UnionFindSet ufs(_vertexs.size());

		while (!pq.empty())
		{
			Node* top = pq.top();
			pq.pop();
			size_t isrc = top->_src;
			size_t ides = top->_des;

			//如果没有造成环,添加此边
			if (ufs.IsInSet(isrc,ides) == false)
			{
				//添加该线
				mintree._AddEdge(isrc,ides,top->_weight);
				mintree._AddEdge(ides,isrc,top->_weight);
				ufs.Union(isrc,ides);

				//所有元素在一个集合里,返回true
				if (ufs.SetSize() == 1)
					return true;
			}

		}
		return false;
	}

图的完整代码

请看GitHub链接 https://github.com/haohaosong/DataStruct/blob/master/Graph.h

相关文章

【啊哈!算法】算法3:最常用的排序——快速排序       ...
匿名组 这里可能用到几个不同的分组构造。通过括号内围绕的正...
选择排序:从数组的起始位置处开始,把第一个元素与数组中其...
public struct Pqitem { public int priority; ...
在编写正则表达式的时候,经常会向要向正则表达式添加数量型...
来自:http://blog.csdn.net/morewindows/article/details/6...