基础概念:
图:就是一种有顶点集合和顶点间的关系组成的一种数据结构;也就是图保存着两个集合,一个是顶点的集合,另一个是顶点之间关系的集合
图可以分为有向图和无向图,顾名思义,有向图是指顶点与顶点之间相连的边是有方向的;而无向图则表示顶点与顶点之间相连的边是没有方向的。如下图所示,在有向图中,连接A、B的两条边并不等价,因为指向的方向是不同的。
完全图:是指顶点集合任意两个顶点都互相指向
有n个顶点的集合,在有向图中,边的个数为n*(n-1)的图,为完全图。(每个顶点又要指向其余的n-1个顶点)
有n个顶点的集合,在无向图中,边的个数为n*(n-1)/2的图,为完全图.
在无向图中,若顶点v1到顶点v2有路径,则称顶点v1与v2是连通的。
连通图:如果图中任意一对顶点都是连通的,则称此图为连通图。
强连通图:在有向图中,若有每一对顶点vi和vj之间都存在一条vi→vj的路径,同时也存在一条vj到vi的路径,则称此图是强连通图。
A B C D之间是连通的,E F之间是连通的,但是A,E之间却不连通,因此不是连通图
生成树:一个连通图的最小连通图称为该图的生成树。最小连通图:在有n个顶点的连通图中,有n-1条边一个有3个顶点A、B、C的图中:有两条边。如图所示:一个图的生成树可能会有多个。
图的存储结构:
上面,我们讨论过,图主要存储两个集合:顶点和顶点之间的关系
顶点之间的关系,在带权路径中,我们可以存储权值。在非带权路径中,我们可以用特定的值,表示两顶点之间是否连通。
图的存储结构有两种,一个是邻接矩阵,另一个是邻接表。
邻接矩阵
用数组将顶点保存,用矩阵的形式表示关系 ;不带权路径:1表示顶点之间连通,0表示顶点之间不连通。
注:在无向图中,2和3连通,在矩阵中 [2,3]=1;[3][2]=1;
但是在有向图中,2→3但是3没有指向2,因此矩阵存储时[2][3]=1,[3][2]=0
存储带权路径的图时,两顶点连通并且有权值时,存储权值;否则,则存储无穷大
代码实现:
template<class V,class W,bool Direction=false>
class Graph{
public:
Graph(V* vertex,size_t size)
{
for (size_t i = 0; i < size; ++i)
{
_vertex.push_back(vertex[i]);
_vertexIndex[vertex[i]] = i;
}
_matrix.resize(size);
for (size_t i = 0; i < size; ++i)
{
_matrix[i].resize(size, W());
}
}
int GetIndexofVertex(const V& v)
{
map<V, int>::iterator Index = _vertexIndex.find(v);
if (Index != _vertexIndex.end())
{
return Index->second;
}
return -1;
}
void ConnectVertex(const V& src, const V& des, const W& w)
{
int srcIndex=GetIndexofVertex(src);
if (srcIndex == -1)
{
printf("起始顶点不存在\n");
return;
}
int desIndex = GetIndexofVertex(des);
if (desIndex == -1)
{
printf("目标顶点不存在\n");
return;
}
_matrix[srcIndex][desIndex] = w;
if (Direction == false)
{
_matrix[desIndex][srcIndex] = w;
}
}
private:
std::vector<V> _vertex;//存放顶点
map<V, int> _vertexIndex;//顶点在矩阵中对应下标
std::vector<vector<W>> _matrix;//矩阵:表示边与边之间的关系
};
邻接表
邻接表中依旧使用数组保存顶点的集合,但是顶点之间的边关系使用链表表示
template<class W>
struct ListNode{
size_t _srcIndex;
size_t _desIndex;
W _w;
ListNode<W>* _next;
};
template<class V,class W,bool Direction=flase>
class Graph{
Graph(const V* vertex,size_t size)
{
_vertexIndex.reserve(size);
for (int i = 0; i < vertex.size(); ++i)
{
_vertex.push_back(vertex[i]);
_vertexIndex[vertex[i]] = i;
}
_edge.reserve(n, nullptr);
}
int GetIndexofVertex(const V& v)
{
iterator Index = _vertexIndex.find(v);
if (Index != _vertexIndex.end())
{
return Index->second;
}
return -1;
}
void ConnectVertex(const V& src, const V& des, const W& w)
{
int srcIndex = GetIndexofVertex(src);
if (srcIndex == -1)
{
printf("起始顶点不存在\n");
return;
}
int desIndex = GetIndexofVertex(des);
if (desIndex == -1)
{
printf("目标顶点不存在\n");
return;
}
//申请一个新的节点
ListNode<W>* node = new ListNode;
node->_srcIndex = srcIndex;
node->_desIndex=desIndex;
node->_w = w;
//进行头插
node->_next = _vertexIndex[srcIndex];
_vertexIndex[srcIndex] = node;
if (Direction == false)
{
ListNode<W>* node = new ListNode;
node->_srcIndex = desIndex;
node->_desIndex = srcIndex;
node->_w = w;
node->_next = _vertexIndex[desIndex];
_vertexIndex[desIndex] = node;
}
}
private:
vector<V> _vertex; //顶点集合
map<V, int> _vertexIndex; //顶点对应的链表下标
vector<ListNode<W>*> _edge; //邻接表
};
图的遍历
深度优先遍历
图的深度优先遍历,我们可以通过树的前、中、后序遍历来理解
邻接矩阵 :
void _DFS(int index, vector<bool>& visited)
{
cout << _vertex[index] << endl;
visited[index] = true;
for (size_t i = 0; i < _vertex.size(); ++i)
{
if (_matrix[index][i] !=W() && visited[i] == false)
{
_DFS(i, visited);
}
}
}
void DFS(const V& v)
{
int index = GetIndexofVertex(v);
if (index == -1)
{
printf("查询的目标不存在\n");
return;
}
vector<bool> visited(_vertex.size(), false);
_DFS(index, visited);
return;
}
广度优先遍历
图的广度优先遍历,就像当于树的层序遍历
邻接表:
//和树的层序遍历实现方式相似,使用了一个队列
void BFS(const V& v)
{
int index = GetIndexofVertex(v);
if (index == -1)
{
printf("查询的目标不存在\n");
return;
}
vector<bool> visited(_vertex.size(), false);
queue<int> q;
q.push(index);
while (!q.empty())
{
int front = q.front();
q.pop();
visited[front] = true;
cout << _vertex[front] << endl;
for (int i = 0; i < _matrix.size(); ++i)
{
if (_matrix[front][i] != W() && visited[i] == false)
{
q.push(i);
visited[i] = true;
}
}
}
cout << endl;
}
最小生成树
连通图由 n 个顶点组成,则其生成树必含 n 个顶点和 n-1 条边 。因此构造最小生成树的准则有三1. 只能使用图中的边来构造最小生成树2. 只能使用恰好 n-1 条边来连接图中的 n 个顶点3. 选用的 n-1 条边不能构成回路
构造最小生成树的方法:Kruskal算法和Prim算法。这两个算法都采用了逐步求解的贪心策略
Kruskal算法:(克鲁斯卡尔算法)
在n个顶点,且没有任何边图中,不断取权值最小的一条边(若有多条,则任选一条),如果连接这条边后,不构成回路(即该边的两个顶点是出于不同的连通分量),则加入最小生成树(连接边)
Prim算法:(普利姆算法)
在n个顶点,且没有任何边图中,选取一个顶点作为起点,取该顶点权值最小的一条边(若有多条,则任选一条),连接;在把改变的另一个顶点当做起点一次迭代,直至生成最小生成树。
优点:不必判断是否生成回环
克鲁斯卡尔算法和普利姆算法不一定会生成最小生成树!
这主要是因为它们应用贪心算法,而贪心算法只能求解局部最优解
至于克鲁斯卡尔算法判断是否构成回环的方法:可以使用并查集结构