赞
踩
在自动化程序分析中,图和树的一些算法起到了至关重要的作用,所以在开始自动化程序分析的研究前,我用了两天复习了一遍数据结构中的图。本章主要内容有图的基本概念,图的存储和图相关的经典算法,并在附录中附带上完整代码的仓库链接。
注:文章中并不会提到关于图的所有概念和定理,只有和程序分析中相关性比较大的那部分。文章中若有错误请指出,代码也存在可优化的空间,欢迎把你的优化代码提到issue中。
数据结构中说的图是平面图,也就是由顶点和顶点间的连线组成的。图的符号表达式为 G = ( V , E ) \textcolor{orange}{G=(V,E)} G=(V,E),其中G就是图,V是顶点集是有限非空集合,E是边集是二元子集。
和顶点相关的概念有度(记作D),而在有向图中度再度被分化为出度(记作I_D)和入度(记作O_D)。
注:对于度的助记符号是我自定义的,没有官方的定义。
图2.2.1无向图
该图中
D ( A ) = 2 \textcolor{orange}{D(A) = 2} D(A)=2, D ( B ) = 3 \textcolor{orange}{D(B) = 3} D(B)=3, D ( D ) = 3 \textcolor{orange}{D(D) = 3} D(D)=3, D ( C ) = 2 \textcolor{orange}{D(C) = 2} D(C)=2。
V = { A , B , C , D } \textcolor{orange}{V = \{A,B,C,D\}} V={A,B,C,D}
E = { < A , B > , < B , A > , < A , D > , < D , A > , < D , B > , < B , D > , < B , C > , < C , B > , < C , D > , < D , C > } \textcolor{orange}{E = \{<A,B>,<B,A>,<A,D>,<D,A>,<D,B>,<B,D>,<B,C>,<C,B>,<C,D>,<D,C>\}} E={<A,B>,<B,A>,<A,D>,<D,A>,<D,B>,<B,D>,<B,C>,<C,B>,<C,D>,<D,C>}
图2.3.1有向图
该图中
I _ D ( A ) = 1 \textcolor{orange}{I\_D(A) = 1} I_D(A)=1, O _ D ( A ) = 1 \textcolor{orange}{O\_D(A) = 1} O_D(A)=1, I _ D ( B ) = 2 \textcolor{orange}{I\_D(B) = 2} I_D(B)=2, O _ D ( B ) = 1 \textcolor{orange}{O\_D(B) = 1} O_D(B)=1, I _ D ( C ) = 0 \textcolor{orange}{I\_D(C) = 0} I_D(C)=0, O _ D ( C ) = 2 \textcolor{orange}{O\_D(C) = 2} O_D(C)=2, I _ D ( D ) = 2 \textcolor{orange}{I\_D(D) = 2} I_D(D)=2, O _ D ( D ) = 1 \textcolor{orange}{O\_D(D) = 1} O_D(D)=1。
V = { A , B , C , D } \textcolor{orange}{V = \{A,B,C,D\}} V={A,B,C,D}
E = { < A , B > , < D , A > , < B , D > , < C , B > , < C , D > } \textcolor{orange}{E = \{<A,B>,<D,A>,<B,D>,<C,B>,<C,D>\}} E={<A,B>,<D,A>,<B,D>,<C,B>,<C,D>}
权值是根据实际情况定义的。例如在程序分析中,我们可以把每个顶点看作是设置的探针,此时的权值可以表示为两个探针之间执行的时间,或者执行的指令数。通常在程序分析中用的是有向图,因为我们程序的指令执行都是有方向的,那么基于有向图,我们可以实现 数据流分析 \textcolor{BrickRed}{数据流分析} 数据流分析。而无向图可应用于城市的规划上,例如每个顶点就是一座城市,权值可代表城市间的距离。然后可以基于图做一些规划,例如寻找最短路径。
图2.4.1带权有向图
图2.4.2带权无向图
图2.5.1完全图
完全图是无向图中衍生的一个概念。完全图的定义是图中任意节点与其他所有节点都相连。因此,通过观察图2.1.5,能够得到判定一个无向图是否为完全图的方法:
对于
G
=
(
V
,
E
)
而言,
∀
v
∈
V
,
D
(
v
)
=
N
−
1
,(
N
表示图中所有顶点个数)。
对于G = (V,E)而言, \forall v \in V,D(v) = N-1,(N表示图中所有顶点个数)。
对于G=(V,E)而言,∀v∈V,D(v)=N−1,(N表示图中所有顶点个数)。
即,任意一个图中的顶点的度等于图中所有顶点数减1。
环的定义是从起始点出发沿着邻接点走最终又回到了起始点,所经过的顶点就形成了一个环。
图2.6.1无向图
在图2.6.1中,有3个环:
图2.6.2有向图
在图2.6.2中,有两个环:
从上面列出的图来看,可以归纳出以下图的基本定理:
在程序中,我们用一种叫做邻接矩阵的数据结构存储图,它是一个二维数组,数组的下标就是图中的各顶点,数组中的元素是个布尔值,表示两个顶点是否相邻。
程序中数组的下标是从0开始的,所以我们事先定义图中A是0,按字母顺序递增下标,则该图对应的邻接矩阵为:
0(A) | 1(B) | 2© | 3(D) | |
---|---|---|---|---|
0(A) | 0 | 1 | 0 | 0 |
1(B) | 0 | 0 | 0 | 1 |
2© | 0 | 1 | 0 | 1 |
3(D) | 1 | 0 | 0 | 0 |
注意:我这里规定了邻接矩阵的每一行r与每一列c的交点处,元素值为1代表图中顶点r与顶点c相邻,元素值为0表示不相邻。也就是说我是横着看的,当然,你也可以按照自己的习惯竖着看。但请一定要记住你的习惯,这在后面编码时要保持习惯的一致性,否则容易出问题。
图遍历算法的实际意义:用于获取从指定顶点出发能够到达哪些顶点,可判断图连通性和获取图的联通分量个数。
连通性的判断在实际应用中就太普遍了,例如网络拓扑中各个终端的联通检测,电力系统中的连通性检测,交通运输网中连通性检测等等。
图遍历算法有两种:
std::shared_ptr<std::vector<uint32_t>> graph::bfs(uint32_t v) { std::vector<uint32_t> resVector; std::queue<uint32_t> Q; bool* visited = nullptr; if (v == 0 || v > _NumOfVertex + 1) goto end_ret; visited = new(std::nothrow) bool[_NumOfVertex]; if (visited == nullptr) goto end_ret; // 清除一下标记 memset(visited, false, sizeof(bool) * _NumOfVertex); // 标记初始顶点为已访问 visited[v-1] = true; // 初始顶点入队 Q.push(v-1); // 遍历队列 while (!Q.empty()) { uint32_t p = Q.front(); resVector.push_back(p+1); Q.pop(); for (uint32_t i = 0; i < _NumOfVertex; i++) { // 依次找与p点相邻的点 if (!visited[i] && _AdjMatrix[p * _NumOfVertex + i]) { visited[i] = true; Q.push(i); } } } end_ret: if (visited) delete[] visited; return std::make_shared<std::vector<uint32_t>>(resVector); }
本小节讲解的是非递归的DFS实现。
std::shared_ptr<std::vector<uint32_t>> graph::dfs(uint32_t v) { std::vector<uint32_t> resVector; std::stack<uint32_t> S; bool recurse_over; bool* visited = nullptr; if (v == 0 || v > _NumOfVertex + 1) goto end_ret; visited = new(std::nothrow) bool[_NumOfVertex]; if (visited == nullptr || _AdjMatrix == nullptr) return nullptr; // 清除一下标记 memset(visited, false, sizeof(bool) * _NumOfVertex); visited[v - 1] = true; S.push(v-1); while (!S.empty()) { uint32_t p = S.top(); recurse_over = true; for (uint32_t i = 0; i < _NumOfVertex; i++) { if (!visited[i] && _AdjMatrix[p * _NumOfVertex + i]) { visited[i] = true; S.push(i); recurse_over = false; // 只要找到一个邻接点就入栈,然后立马跳出循环,并对该点进行重复上面的操作 // 实现模拟递归 break; } } if (recurse_over) { resVector.push_back(p+1); S.pop(); } } end_ret: if (visited) delete[] visited; return std::make_shared<std::vector<uint32_t>>(resVector); }
拓扑排序是针对于无环图来讲的,同时它也可以用来检测图中是否存在环。
拓扑排序常用来表示存在依赖关系的事务。
课程选修要求如图
图中的含义是有:
选修《软件开发》就需要先选修《C++语言》、《数据结构》、《数据结构》和《数据库设计》中的任意一门。
选修《C++语言》的前提是先选修《C语言》
选修《数据结构》的前提是先选修《C语言》
选修《数据库设计》的前提是先选修《数据结构》
如果对《软件开发》这门课程进行拓扑排序,则得到的结果为:
同时意味着这也是学习《软件开发》这门课程比较合理的学习路线。
本节介绍的拓扑排序算法会扩展到无向图中,用来判断图是否存在环。
先来说指定一个顶点v,给出以这个顶点为终点的拓扑排序算法:
std::shared_ptr<std::vector<uint32_t>> graph::topologySort(uint32_t v) { std::vector<uint32_t> resVector; std::queue<uint32_t> Q; std::vector<uint32_t> V(_IsDirected ? _InDegree : _Degree); if (v == 0 && v > _NumOfVertex) goto end_ret; for (int32_t i = 0; i < _NumOfVertex; i++) { if ((_IsDirected && V[i] == 0) || (!_IsDirected && V[i] == 1)) Q.push(i); } while (!Q.empty()) { int32_t p = Q.front(); Q.pop(); resVector.push_back(p + 1); if (p + 1 == v) break; for (int32_t i = 0; i < _NumOfVertex; i++) { if (_AdjMatrix[p * _NumOfVertex + i]) { V[i] -= 1; if ((_IsDirected && V[i] == 0) || (!_IsDirected && V[i] == 1)) Q.push(i); } } } end_ret: return std::make_shared<std::vector<uint32_t>>(resVector); }
再说说使用拓扑排序判断图中是否存在环的算法,与指定点的拓扑排序算法大同小异:
bool graph::hasCycle() { std::queue< uint32_t> Q; int32_t num = 0; std::vector<uint32_t> V(_IsDirected ? _InDegree : _Degree); for (int32_t v = 0; v < _NumOfVertex; v++) { if ((_IsDirected && V[v] == 0) || (_IsDirected && V[v] == 1)) { Q.push(v); num += 1; } } while (!Q.empty()) { int32_t v = Q.front(); Q.pop(); for (int32_t i = 0; i < _NumOfVertex; i++) { if (_AdjMatrix[v * _NumOfVertex + i]) { V[i] -= 1; if ((_IsDirected && V[i] == 0) || (_IsDirected && V[i] == 1)) { Q.push(i); num += 1; } } } } return num != _NumOfVertex;; }
本小节主要讲的应用场景是给定一个起始点和终点,获取从起始点到达终点的所有路径。这种应用场景在程序分析可以是解析函数间的调用关系,请看下图:
假设图中各顶点代表函数A,B,…,H,图中反映的是他们之间的调用关系。我想知道从函数A到函数G都有哪些调用路径。从图中我们可以很清楚的知道有这些路径:
A − > B − > F − > G \textcolor{orange}{A->B->F->G} A−>B−>F−>G
A − > D − > F − > G \textcolor{orange}{A->D->F->G} A−>D−>F−>G
A − > C − > E − > G \textcolor{orange}{A->C->E->G} A−>C−>E−>G
注意:一条路径上重复的点不走第二次。
那么如何让计算机去实现这种功能呢?
整个算法基于DFS的思想,假设输入起始点为v1,终点为v2。
std::shared_ptr<std::vector<std::vector<uint32_t>>> graph::getAllPath(uint32_t v1, uint32_t v2) { std::stack<uint32_t> S; std::vector<uint32_t> path; std::vector<std::vector<uint32_t>> resVec; std::vector<int32_t> pos; bool recursed = false; bool* visited = nullptr; if (v1 == 0 || v1 > _NumOfVertex || (v2 == 0 || v2 > _NumOfVertex)) goto end_exit; visited = new(std::nothrow) bool[_NumOfVertex]; if (visited == nullptr) goto end_exit; // 初始化 for (uint32_t i = 0; i < _NumOfVertex; i++) { visited[i] = false; pos.push_back(0); } S.push(v1 - 1); path.push_back(v1); visited[v1 - 1] = true; while (!S.empty()) { auto v = S.top(); recursed = false; for (uint32_t i = pos[v]; i < _NumOfVertex; i++) { if (!visited[i] && _AdjMatrix[v * _NumOfVertex + i]) { if (i == v2 - 1) { std::vector<uint32_t>tmpVec(path); tmpVec.push_back(i + 1); resVec.push_back(tmpVec); } else { visited[i] = true; S.push(i); path.push_back(i + 1); pos[i] = 0; pos[v] = i + 1; recursed = true; break; } } } if (!recursed) {// 回溯大法 path.pop_back(); v = S.top(); if(pos[v]) pos[v] -= 1; visited[v] = false; S.pop(); } } end_exit: if (visited) delete[] visited; return std::make_shared<std::vector<std::vector<uint32_t>>>(resVec); }
前面在权一小节中提到过寻最短路径的应用场景,同样在程序分析中有很重要意义。说到寻最短路径不得不提到一个著名算法——Dijkstra,应用的场景是寻单源点下的最短路径。也就是说指定一个起始点,该算法会取得该点到图中各点最短的路径距离。而我们稍微增加一些代码就能实现获取起始点的最短/长路径和距离值。
注意:本小节提到的寻最短/长路径是基于带权路径图来说的,无权图不适用这个算法。
整个算法基于BFS思想,假设输入起始点为v1,具体操作如下:
最长路径的求法,就是在比较路径的时候将“<”换成“>”,不再赘述。
std::shared_ptr<std::unordered_map<uint32_t, std::vector<uint32_t>>> graph::DijkstraAlogrithm( uint32_t v1, std::vector<uint32_t>& dis, bool reverse) { std::unordered_map<uint32_t, std::vector<uint32_t>> path; std::queue<uint32_t> Q; uint32_t v; bool* visited = nullptr; visited = new(std::nothrow) bool[_NumOfVertex]; if (visited == nullptr) goto end_ret; // 清除一下标记 memset(visited, false, sizeof(bool) * _NumOfVertex); for (uint32_t i = 0; i < _NumOfVertex; i++) { // 初始化指定源点到各个顶点的最短路径 path.insert(std::pair<uint32_t, std::vector<uint32_t>>(i, std::vector<uint32_t>())); // 初始化距离 if (i == v1 - 1) dis.push_back(0); else dis.push_back(reverse ? 0 : -1); } Q.push(v1 - 1); visited[v1 - 1] = true; while (!Q.empty()) { auto v = Q.front(); Q.pop(); uint32_t s = v; uint32_t prev_dis = reverse ? 0 : -1; for (uint32_t i = 0; i < _NumOfVertex; i++) { if (!visited[i] && _AdjMatrix[v * _NumOfVertex + i]) { auto w = _Weight[v * _NumOfVertex + i] + dis[v]; if (reverse) { if (prev_dis < w) { // 更新本次寻找到的最长路径长度 prev_dis = w; s = i; // s 始终指向本次寻找到的最长路径的终点 } if (w > dis[i]) { // 动态更新最长路径 dis[i] = w; if (path[v].size()) path[i] = std::vector<uint32_t>(path[v]); path[i].push_back(v + 1); } } else { if (prev_dis > w) { // 更新本次寻找到的最短路径长度 prev_dis = w; s = i; // s 始终指向本次寻找到的最短路径的终点 } if (w < dis[i]) { // 动态更新最短路径 dis[i] = w; if (path[v].size()) path[i] = std::vector<uint32_t>(path[v]); path[i].push_back(v + 1); } } } } if (s != v) { // 将本次最短路径终点标记为已访问,意味着从指定源点到s 的最短路径已经找到。 visited[s] = true; Q.push(s); } } end_ret: if (visited) delete[] visited; return std::make_shared<std::unordered_map<uint32_t, std::vector<uint32_t>>>(path); }
完整代码清移步我的代码仓库:https://github.com/singlefreshBird/Algorithm
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。