c ++图形界面编程
更新 :您可以在这里观看有关C ++中图形表示的视频:
图形被正式定义为一组顶点V和一组连接这些顶点的边E :
G =(V,E)
每个边都是一对顶点。 对于有向图,边是有序对,对于无向图,边是无序的。
我们如何用C ++表示图? 本文总结了使用C ++标准模板库(STL)可用的各种选项。
对于每种方法,我们将实现一个简单的算法来检查图是否为Eulerian ,即奇数节点的数量是否正好为0或2。这被称为“柯尼斯堡七桥”问题。
方法1:定义的直接转换:G =(V,E)
这可能是用C ++表示图形的最直接的方法:我们仅将数学定义直接转换为STL容器。
在此方法中,可以使用以下两个STL容器之一来表示V和E : std::vector, std::set, std::list
。
考虑以下简单图形:
为了简单起见,在所有可用选项中,我们选择:
- V:std :: vector
- E:std :: vector
- 边:std :: pair
现在,可以将图定义为一个类,并可以使用STL的统一初始化轻松进行初始化 :
class Graph {
public :
Graph( std :: vector < int > &v, std :: vector < std ::pair< int , int >> &e)
: v_(v), e_(e) {}
bool IsEulerWalkable () ;
void PrintGraph () ;
std :: vector < int > v_;
std :: vector < std ::pair< int , int >> e_;
};
int main () {
std :: vector < int > v = { 0 , 1 , 2 , 3 };
std :: vector < std ::pair< int , int >> e = {{ 1 , 3 }, { 1 , 3 }, { 3 , 0 }, { 3 , 0 },
{ 0 , 2 }, { 2 , 1 }, { 2 , 3 }};
Graph g (v, e) ;
std :: cout << g.IsEulerWalkable() << std :: endl ;
}
请注意,上述代码中的顶点和边初始化与输入图定义的匹配程度如何。
有了上面的定义和初始化,我们现在可以将IsEulerWalkable()
实现为:
bool Graph::IsEulerWalkable() {
// Create a table to hold degree of each vertex
std :: vector < int > degrees(v_.size());
// Iterate all edges
for ( auto e : e_) {
degrees[e.first]++;
degrees[e.second]++;
}
int countOdds = 0 ;
// Iterate through degree table and count the number of odd ones
for ( auto d : degrees) {
if (d % 2 == 1 ) {
countOdds++;
}
}
return (countOdds == 0 || countOdds == 2 );
}
上面的代码很容易说明:我们遍历所有边缘并增加边缘上每个顶点的度数。 最后,我们计算具有奇数边的顶点数,如果计数为0或2,则返回true。
方法2:邻接表
在此方法中,从概念上讲,我们将图形表示为顶点和相邻顶点集之间的映射,而不是为V和E设置两个单独的集合。 考虑下图:
可以使用下表来表示此映射:
为了在C ++中对此建模,我们可以创建一个名为Vertex
的新类,该类具有两个成员变量:
-
vertex_number
:一个整数变量 -
adjacents
:一组相邻的顶点 struct Vertex { Vertex( int v, std :: set < int > a) : vertex_number(v), adjacents(a) {} int vertex_number; std :: set < int > adjacents; }; class Graph { public : Graph( std :: vector <Vertex> v) : v_(v) {} bool IsEulerWalkable () ; std :: vector <Vertex> v_; }; int main () { Graph g ({Vertex( 0 , { 1 , 2 , 3 }), Vertex( 1 , { 0 , 2 , 3 }), Vertex( 2 , { 0 , 1 , 3 }), Vertex( 3 , { 0 , 1 , 2 })}) ; std :: cout << g.IsEulerWalkable() << std :: endl ; }
再次注意,我们如何使用统一初始化将图形初始化为表示该图形的输入表。
具有以上定义,我们可以将
IsEulerWalkable()
重写为:bool Graph::IsEulerWalkable() { // Create a table to hold degree of each vertex std :: vector < int > degrees(v_.size()); // Iterate vertices for ( auto v : v_) { degrees[v.vertex_number] = v.adjacents.size(); } // Iterate through degree table and count the number of odd ones int countOdds = 0 ; for ( auto d : degrees) { if (d % 2 == 1 ) { countOdds++; } } return (countOdds == 0 || countOdds == 2 ); }
注意我们如何使用STL的
size()
函数来获取每个节点的度数:使用邻接表,每个节点的程度已经可用。
方法3:邻接矩阵
邻接矩阵是一个二维矩阵,如果在顶点i和j之间存在边,则条目i,j为1 。
如果该图是无向的,则邻接矩阵是对称的:如果条目i,j为1 ,则条目j , i也为1 。
邻接矩阵可以使用二维矩阵表示:
class Graph { public : Graph( std :: vector < std :: vector < int >> &adjacency) : adjacency_(adjacency) {} bool IsEulerWalkable () ; void PrintGraph () ; std :: vector < std :: vector < int >> adjacency_; }; int main () { std :: vector < std :: vector < int >> adjacency = {{ 0 , 1 , 1 , 0 , 0 }, { 1 , 0 , 1 , 1 , 1 }, { 1 , 1 , 0 , 1 , 0 }, { 0 , 1 , 1 , 0 , 1 }, { 0 , 1 , 0 , 1 , 0 }}; Graph g (adjacency) ; std :: cout << "g.isEulerWalkable(): " << g.IsEulerWalkable() << std :: endl ; return 0 ; }
再次注意,使用统一初始化的邻接矩阵初始化与给定表的匹配程度如何。
通过上面的类定义,我们可以将
IsEulerWalkable()
重写为:bool Graph::IsEulerWalkable() { // Create a table to hold degree of each vertex std :: vector < int > degrees(adjacency_.size()); // Iterate all edges for ( int i = 0 ; i < adjacency_.size(); i++) { for ( int j = 0 ; j < adjacency_.size(); j++) { if (adjacency_[i][j] == 1 ) { degrees[i]++; } } } int countOdds = 0 ; // Iterate through degree table and count the number of odd ones for ( auto d : degrees) { if (d % 2 == 1 ) { countOdds++; } } return (countOdds == 0 || countOdds == 2 ); }
请注意,要获取每个顶点的度数,我们必须迭代每行并添加
1's
数量。邻接矩阵的主要问题是它的大小。 对于
n
个顶点的图,我们需要n^2
个条目。 但是,优点是我们可以仅通过检查O(1)
时间中的条目i , j来检查节点i和j是否相邻。三种方法的比较
这些方法如何比较? 下表总结了主要区别:
第一种方法,直接平移,似乎具有较小的内存大小,但是找到顶点的相邻点需要
O(m)
,其中m是边的数量。邻接表提高了查找顶点相邻点的运行时复杂度。 但是,如果我们只想检查节点i,j是否相邻,则运行时间仍然不是恒定的。
邻接矩阵具有最大的空间,但是由于向量访问时间的性质,您可以快速检查两个节点是否在恒定时间内相邻。
在std :: vector,std :: set和std :: list之间进行权衡:
正如我在一开始所提到的,在使用STL的向量,集合或列表表示顶点和边缘之间需要权衡取舍。
让我们回顾一些主要差异:
std :: set:
- 一套自动排序
- 插入/删除/查找集合需要O(log n)
std :: unordered_set:
- 无序集未排序
- 插入/删除/查找集合需要O(log 1)
std :: vector:
- 向量未排序
- 插入/删除/查找集合需要O(n)
std :: list:
- 列表未排序
- 在集合中查找需要O(n)
- 插入/删除集合需要O(1)
基于此,如果要使边缘或顶点保持排序,则可能应该使用一组。 如果要非常快的搜索时间,则应使用unordered_set。
如果查找时间不那么重要,则可以使用列表或向量。 但是还有一个更重要的原因,那就是人们可能使用向量/列表而不是集合来表示多图:
多重图形类似于图形,但是在两个节点之间可以有多个边。
set或unordered_set不能包含重复项。 例如,在上图所示的多重图形中,我们将在0和1之间有两个相似的边:
(0,1)
和(0,1)
。 不能使用集合来表示。 因此,为了表示多重图形,您可能需要使用std :: vector,list或multiset
。摘要
我们展示了如何使用以下三种方法之一在C ++中表示图形:图形定义的直接翻译,邻接表和邻接矩阵。 如果您准备进行技术面试,请确保您了解每种方法的区别以及优缺点。
通过GTest,Glog和Abseil来查看有关Bazel和Visual Studio Code的其他视频:
翻译自: https://hackernoon.com/graph-representation-in-c-job-interview-cheatsheet-sp1a3vgx
c ++图形界面编程