图算法 - 只需“五步” ,获取两节点间的所有路径(非递归方式)
作者:互联网
在实现 “图” 数据结构时,会遇到 “**获取两点之间是所有路径**” 这个算法问题,网上的资料大多都是利用递归算法来实现(见文末的参考文章)。
我们知道在 JS 中用递归算法很容易会让调用栈溢出,为了能在生产环境中使用,必须要用非递归方式的去实现。
经过一番探索,实现的思路主要来自文章 《[求两点间所有路径的遍历算法](https://blog.csdn.net/lysc_forever/article/details/17500959)》 ,只是该文中并没有给出具体的实现细节,需要自己去实现;最终本文的实现结合类似《[算法 - 调度场算法(Shunting Yard Algorithm)](https://mp.weixin.qq.com/s?__biz=MzI0MzU5NjQxNA==&mid=2247483745&idx=1&sn=ed320a380ca81e883cdb9c91de6e2a76&chksm=e96bec70de1c6566b05c0119f254aaaf0a0894e715913fd2c39ca48413846a651a4b2c5a7aa7&token=2061671007〈=zh_CN#rd)》 中所提及的双栈来完成。
## 1、算法过程
以计算下图为例, **节点 3** 到 **节点 6** 所有路径所有可能的路径为 8 条:
![allpath](https://www.icode9.com/i/l/?n=18&i=blog/337820/201909/337820-20190921114932624-133851152.png)
我们具体讲一下如何获取这 8 条路径的过程。
首先准备两个栈,分别称为 **主栈** 和 **辅栈**:
- **主栈**:每个元素是**单个节点(Vertex)**,用于存放当前路径上的节点;
- **辅栈**:每个元素用于存放主栈对应元素的 **相邻节点列表(Vertex Array)**;该栈是用来辅助 **主栈** 的,其长度和 **主栈** 一致;
### **Step 1**: 建栈
将 `v3`(**节点3**)放到主栈,同时将 `v3` 节点的邻接节点列表 `[v1, v7]` 放到辅栈中:
![首次建栈](https://www.icode9.com/i/l/?n=18&i=blog/337820/201909/337820-20190921114933007-111982104.png)
主栈和辅栈压入让栈长度增长,我个人称之为 **建栈(build stack)**
### **Step 2**: 继续建栈
建栈后,我们查看辅栈,其栈顶是节点列表 `[v1, v7]`:
![查看栈顶](https://www.icode9.com/i/l/?n=18&i=blog/337820/201909/337820-20190921114933308-700814265.png)
我们取出节点列表的第一个元素 `v1`,将其压入到主栈;同时将剩下的节点列表 `[v7]` 重新压回到辅栈:
![压栈](https://www.icode9.com/i/l/?n=18&i=blog/337820/201909/337820-20190921114933632-1548856893.png)
同时查询 `v1` 的邻接节点列表是 `[v3, v0]`,**由于 `v3` 节点已经在主栈里,需要从这个列表中剔除**(这一步很重要),将剔除后的节点列表 `[v0]` 压入 **辅栈** 中:
![继续建栈](https://www.icode9.com/i/l/?n=18&i=blog/337820/201909/337820-20190921114933919-757108021.png)
这一步也让主栈和辅栈长度增长了,所以也是 **建栈(build stack)** 过程
### **Step 3**: 削栈
继续 **Step 2** 的建栈过程,直到我们的主栈栈顶 **v7**,此时辅栈的栈顶是空列表 `[]`:
![当主栈是 v7 的时候,辅栈栈顶是空队列](https://www.icode9.com/i/l/?n=18&i=blog/337820/201909/337820-20190921114934224-116520940.png)
由于辅栈的栈顶是空列表 `[]`,所以没法继续建栈了 —— 这表明这条路径走到尽头了都还没找到目标节点 **v6**。
走到 **此路不通** 的境地,我们就需要开始回退,看看来时的路上的其他岔路。
我们将主栈栈顶的 **v7** 弹出,同时也将辅栈的空列表 `[]` 弹出:
![削栈](https://www.icode9.com/i/l/?n=18&i=blog/337820/201909/337820-20190921114934553-120233886.png)
这一操作将导致 **主栈** 和 **辅栈** 长度减少,该过程我个人称之为 **削栈(cutdown stack)**。
### **Step 4**:获取第一条路径
重复上述的 **Step 2**、**Step 3**,采取策略:
- 只要辅栈栈顶是**非空列表**,我们就建栈
- 只要辅栈栈顶是**空列表**,我们就削栈
直到主栈的顶部节点是目标节点 `v6`:
![主栈栈顶元素是目标元素v6](https://www.icode9.com/i/l/?n=18&i=blog/337820/201909/337820-20190921114934891-1952886305.png)
进行到这里,我们停下来观察一番,发现主栈里的内容已经是一条完整的从 `v3` 到 `v6` 的路径了:
![获取一条从 v3 到 v6 的路径](https://www.icode9.com/i/l/?n=18&i=blog/337820/201909/337820-20190921114935471-2125901050.png)
我们输出当前栈为数组:`['v3', 'v1', 'v0', 'v2', 'v5', 'v6']`,该数组就表示 `v3 -> v1 -> v0 -> v2 -> v5 -> v6` 这条路径。
进行至此,我们终于获取了一条从 `v3` 到 `v6` 的路径。
应该为自己的努力鼓个掌,已经看到胜利的曙光;接下来加个简单的循环就能获取所有的路径。
### **Step 5**: 获取所有路径
重复 **Step 2** - **Step 4** 步骤,采取策略如下:
- 只要辅栈栈顶是**非空列表**,我们就建栈
- 只要辅栈栈顶是**空列表**,我们就削栈
- 只要主栈栈顶是**目标节点**,我们输出路径,同时削栈
重复以上过程,直到**主栈**为空为止。
随着 **建栈(build stack)** 和 **削栈(cutdown stack)** 过程的进行,主栈和辅栈不断变化着,在这个变化的过程中我们就能不断地获取从 `v3` 到 `v6` 的路径,最终就可以获取所有的路径。
## 2、代码实现
### 2.1、伪代码
依据上述过程的描述,很方面将文字转换成伪代码:
```
BEGIN
初始化主栈
初始化辅栈
首次建栈
WHILE 主栈不为空 THEN
获取辅栈栈顶,为邻接节点列表
IF 邻接节点列表不为空 THEN
获取邻接节点列表首个元素
将该元素压入主栈,剩下列表压入辅栈
建栈
ELSE
削栈
CONTINUE
END IF
IF 主栈栈顶元素 === 目标节点 THEN
获取一条路径,保存起来
削栈
END IF
END WHILE
END
```
以上是我们拿无向图来做范例,实际上**该算法也适合有向图**。
### 2.2、实现效果
该双栈算法的 JS 实现已经写到代码库 **[ss-graph](https://github.com/boycgit/ss-graph/blob/2612ae5ecf7100f08a4aa774febdc543c8439c66/src/graph.ts#L377)** 中 ,我们直接拿它来做校验,实际运行效果如下:
> 可前往 https://runkit.com/boycgit/ss-graph 自行修改数据体验:
![运行实际代码,验证算法](https://www.icode9.com/i/l/?n=18&i=blog/337820/201909/337820-20190921114936426-579237870.png)
## 3、总结
最近在复习 “图” 这数据结构,在过程中逐步尝试书写代码去实现个中算法。能够体会得到知识点只有经过自己思考和总结后,才能为之后的融会贯通打下基础。
在本文的学习总结中,有两点体会印象较为深刻:
1. 能用能递归解决的问题,一般都可以用 **循环 + 栈(Stack)** 的方式来解决。
2. 当不知道算法如何实现的时候,比较适合归纳总结的学习方法,即先逐步从简单场景开始演示,等摸索到其中规律之后再着手去实现。
图相关的算法还有很多,有很多经典算法,后续有空会将一些经典的算法实现并整理出来,互有裨益。
## 参考文章
- [Find if there is a path between two vertices in a directed graph](https://www.geeksforgeeks.org/find-if-there-is-a-path-between-two-vertices-in-a-given-graph/):geeksforgeeks 相关面试题,递归实现
- [Print all paths from a given source to a destination](https://www.geeksforgeeks.org/find-paths-given-source-destination/):递归实现,查找所有路径
- [求两点间所有路径的遍历算法](https://blog.csdn.net/lysc_forever/article/details/17500959):较为通俗易懂;,一个保存路径的栈、一个保存已标记结点的数
以下是我的公众号,会时常更新 JS(Node.js) 知识和资讯,欢迎扫码关注交流。
![个人微信公众号](https://www.icode9.com/i/l/?n=18&i=blog/337820/201909/337820-20190921114937064-738840704.jpg)
标签:主栈,辅栈,递归,337820,路径,https,五步,节点 来源: https://www.cnblogs.com/boychenney/p/11562224.html