本章讲述了数据结构——图、队列,新算法——广度优先搜索(breadth-first search BFS)
- 图是由节点 (node) 和边 (edge) 组成的。
- 图是用于模拟不同东西是如何相连的。
- 图分为有向图和无向图,有向图方向确定了关系的方向,无向图确定了关系的双向。
假设你和朋友打牌,需要模拟谁欠谁的钱。可以像下面这样指出 Alex 欠 Rama 的钱。
完整的欠钱图可能类似于下面这样。
在我们生活中图的作用也非常大,这种数据结构可以直观的表示出节点与节点的关系。
例如我自己在回家或者出去旅游时就会用图来描述数据
上面都是有向图,下面讲一下无向图。
无向图就是没有箭头的边构成的图,无向图和有向图有等价关系,如下:
树是一种特殊的图。 不信可以动手画画哦。
上述的都只是为了让人能简单直观的知道图是长什么样的(节点+边),有什么作用(通过模拟连接来描述节点之间的关系),方便于引入广度优先搜索算法。还有很多原理性的东西建议去找《数据结构》之类的书深入了解一下。
这里就不再赘述。
广度优先搜索(breadth-first search BFS)
广度优先搜索主要回答或者说解决两类问题:
- 从节点A出发,有前往节点的B的路径吗?
- 从节点A出发,前往节点B的哪条路径最短?
假设你经营者一个芒果农场,需要寻找芒果销售商,以便将芒果卖给他。在Facebook,你与芒果销售商有联系吗?为此你可在朋友中查找。
这种查找很简单。首先创建一个朋友名单。
然后依次检查名单中的每个人,看看他是否是芒果商。
假设你没有朋友是芒果商,你就需要在朋友的朋友里面找。
检查名单中的每个人时,都将其朋友加入名单末尾。
这样,你不仅在朋友中找,还在朋友的朋友中查找,直到在人际关系网中找到一位芒果销售商。
使用这种算法将你的人际关系网搜索一遍直到找到芒果销售商。这就是广度优先搜索。
查找最短路径
上面告诉了我们广度优先搜索是怎么回答第一类问题的:从节点A出发,有前往节点的B的路径吗?(你的人际网中有芒果销售商吗?)
现在我们将要看看广度优先搜索是怎么回答第二类问题的:从节点A出发,前往节点B的哪条路径最短?(哪个芒果销售商与你关系最近?)
例如,朋友是一度关系,朋友的朋友是二度关系。
在你看来,一度关系胜于二度关系,二度关系大于三度关系,所以你先从一度关系中找有没有芒果销售商。
一度关系中没有再去二度关系找。广度优先搜索就是这样做的!广度优先搜索先检查你所有一度的关系,再检查你所有二度的关系。
因此你需要按添加顺序检查。有一种可实现这种目的的数据结构,那就是队列(queue)。
队列(queue)
队列的工作原理与现实生活中的队列完全相同。例如公交车的队列,排前面的先上,排后面的后上。队列类似于栈,不能随机的访问队列中的元素。
- 队列支持两种操作:入队(压入)、出队(弹出)。
- 队列是一种先进先出(First In First Out,FIFO)的数据结构,而栈是一种先进后出(Last In First Out,LIFO)的结构。
具体关于队列的原理信息,建议去看书,此处只是让人大脑里有个大概的概念,在此不再赘述!
具体关于队列的原理信息,建议去看书,此处只是让人大脑里有个大概的概念,在此不再赘述!
具体关于队列的原理信息,建议去看书,此处只是让人大脑里有个大概的概念,在此不再赘述!
大概了解的队列是怎么回事后,我们就开始实现算法了。
1. 实现图
首先通过代码实现图。我们需要实现类似于 " 你 → Bob" 这样的关系,回想我们以前学的数据结构,有什么能够表达 key→value 这样关系的数据结构?散列表!
记住,散列表让你能够将键映射到值。在这里你要将节点映射到其所有邻居。
表示这种映射关系的Python代码如下:
//python2.7
graph = {} //创建字典
graph["you"] = ["alice","bob","claire"]
graph["bob"] = ["anuj","peggy"]
graph["alice"] = ["peggy"]
graph["claire"] = ["thom","jonny"]
graph["anuj"] = []
graph["peggy"] = []
graph["thom"] = []
graph["jonny"] = []
顺便问一句:键——值对的添加顺序重要吗? 换而言之,如果你这样编写代码:
graph["claire"] = ["thom","jonny"]
graph["anuj"] = []
而不是这样编写代码:
graph["anuj"] = []
graph["claire"] = ["thom","jonny"]
回顾前一章,你就知道没有影响。散列表时无序的,因此添加键值对的顺序无关紧要。
下面用 java 写出上面的代码:
public class Demo {
public static void main(String[] args) {
HashMap<String, ArrayList<String>> graph = new HashMap<>();
//你的朋友
ArrayList<String> yourFriends = new ArrayList<>();
yourFriends.add("alice");
yourFriends.add("bob");
yourFriends.add("claire");
//Alice的朋友
ArrayList<String> AliceFirends = new ArrayList<>();
AliceFirends.add("peggy");
//Bob的朋友
ArrayList<String> BobFirends = new ArrayList<>();
BobFirends.add("anuj");
BobFirends.add("peggy");
//Claire的朋友
ArrayList<String> ClaireFirends = new ArrayList<>();
ClaireFirends.add("thom");
ClaireFirends.add("jonny");
graph.put("you", yourFriends);
graph.put("alice", AliceFirends);
graph.put("Bob", BobFirends);
graph.put("claire", ClaireFirends);
graph.put("anuj", null);
graph.put("peggy", null);
graph.put("thom",null);
graph.put("jonny", null);
}
}
淦,我好难啊,为什么python这么简单快乐???
java用键值对用HashMap类,朋友列表用ArrayList类,考虑到用String数组万一长度不够的问题。
OK,现在图已经建好了,我们进入下一步。
2. 实现算法
这里有一个问题,Peggy 既是 alice 的朋友又是 bob 的朋友,因此她将被加入队列两次:一次是在添加 alice 的朋友时,另一次是在添加bob的朋友时。因此,搜索队列将包含两个 peggy 。
但是你只需要检查 peggy 一次,检查两次就是做了无用功。因此,检查完一个人后,应将其标记为已检查,且不再检查他。
如果不这样做,就可能陷入无限循环。假设你的人际关系网如下:
这时候如果没有标记过是否已检查,搜索队列将在包含你和包含 peggy 之间反复横跳。
因此,检查一个人之前,要确认之前没检查过他,这很重要。
- python代码如下:
//python2.7
from collections import deque
def bfs(name): #广度优先搜索方法
search_queue = deque() #创建一个双端队列
search_queue += graph["you"] #将你的邻居都加入到搜索队列中
//graph["you"] = ["alice","bob","claire"]
searched = [] #此数组用于记录检查过的人
while search_queue: #只要队列不为空
person = search_queue.popleft() #取出队列第一个人
if person not in searched: #此人没被检查过
if person_is_seller(person): #检查这个人是不是芒果销售商
print person + "is a mango seller!" #此人是芒果销售商
return True
else:
search_queue += graph[person] #此人不是芒果销售商,将他的朋友都加入队列
searched.append(person) #将此人标记为已经检查过
return False
def person_is_seller(name): #判断是不是芒果销售商的方法
return name[-1] == 'm' #假设芒果销售商名字是以‘m’为结尾
- java代码非递归式如下:
/**
* @description 广度优先搜索非递归形式
* @author 海旋风
* @date 2019/10/8 20:09
* @param graph 图
* @param key 图起点
* @return boolean
*/
public static boolean bfs(HashMap<String, ArrayList<String>> graph, String key) {
//创建双端队列
Deque<String> deque = new LinkedList<>();
//创建标记已检查过的列表
ArrayList<String> searched = new ArrayList<>();
//获取“you”的朋友圈
ArrayList<String> list = graph.get(key);
System.out.println(key + "的朋友有:" + list);
if (list != null) {
//将“you”的朋友圈:alice, bob, claire 加入队列
for (String s : list) {
deque.offer(s);
}
}
System.out.println("队列内容:" + deque);
while (deque.size()>0){ //队列不为空
//弹出队列第一个元素
String pop = deque.pop();
System.out.println("弹出" + pop);
if (! searched.contains(pop)){ //判断此人是否被检查过
if (isSomeone(pop)) { //判断此人是不是芒果销售商
System.out.println(pop + "是芒果销售商");
return true;
} else {
//不是芒果销售销售商,把此人的朋友圈加入队列
ArrayList<String> list1 = graph.get(pop);
if (list1 != null) {
for (String s : list1) {
deque.offer(s);
}
}
searched.add(pop); //标记此人已经被检查过
System.out.println(pop+"弹出后的队列:"+deque);
}
}
}
return false;
}
//判断是不是芒果销售商
public static boolean isSomeone(String name) {
if (name.equals("lbwnb")) return true;
return false;
}
- java递归式代码如下:
/**
* @description
* @author 海旋风
* @date 2019/10/8 20:09
* @param graph 图
* @param key 图起点
* @return boolean
*/
static Deque<String> deque = new LinkedList<>(); //定义全局队列变量
static ArrayList<String> cheched = new ArrayList<>(); //标记已经检查过的人
public static boolean bfs(HashMap<String, ArrayList<String>> graph, String key) {
ArrayList<String> list = graph.get(key);
System.out.println(key + "的朋友有:" + list);
if (list != null) {
for (String s : list) {
deque.offer(s);
}
}
System.out.println("队列内容:" + deque);
//队列不为空时
if (deque.size()>0){
String pop = deque.pop();
System.out.println("弹出" + pop);
if (!cheched.contains(pop)){ //此人没被检查过
if (isSomeone(pop)) { //判断是不是芒果销售商
System.out.println(pop + "是芒果销售商");
deque.clear(); //清空队列,防止下次使用读到脏数据
cheched.clear(); //清空列表,防止下次使用读到脏数据
return true;
} else {
cheched.add(pop);//标记此人已被检查过
return bfs(graph, pop);
}
}else{
//检查过了直接取下一个。
return bfs(graph, deque.getFirst());
}
}
//队列为空了都没找到
return false;
}
public static boolean isSomeone(String name) {
if (name.equals("thom")) return true;
return false;
}
}
测试一下java代码
public static void main(String[] args) {
HashMap<String, ArrayList<String>> graph = new HashMap<>();
//你的朋友
ArrayList<String> yourFriends = new ArrayList<>();
yourFriends.add("alice");
yourFriends.add("bob");
yourFriends.add("claire");
//Alice的朋友
ArrayList<String> AliceFirends = new ArrayList<>();
AliceFirends.add("peggy");
//Bob的朋友
ArrayList<String> BobFirends = new ArrayList<>();
BobFirends.add("anuj");
BobFirends.add("peggy");
//Claire的朋友
ArrayList<String> ClaireFirends = new ArrayList<>();
ClaireFirends.add("thom");
ClaireFirends.add("jonny");
graph.put("you", yourFriends);
graph.put("alice", AliceFirends);
graph.put("bob", BobFirends);
graph.put("claire", ClaireFirends);
graph.put("anuj", null);
graph.put("peggy", null);
graph.put("thom", null);
graph.put("jonny", null);
boolean you = bfs(graph, "you");
System.out.println("bfs搜索结果:"+you);
}
假设thom是芒果销售商,结果打印如下:
you的朋友有:[alice, bob, claire]
队列内容:[alice, bob, claire]
弹出alice
alice的朋友有:[peggy]
队列内容:[bob, claire, peggy]
弹出bob
bob的朋友有:[anuj, peggy]
队列内容:[claire, peggy, anuj, peggy]
弹出claire
claire的朋友有:[thom, jonny]
队列内容:[peggy, anuj, peggy, thom, jonny]
弹出peggy
peggy的朋友有:null
队列内容:[anuj, peggy, thom, jonny]
弹出anuj
anuj的朋友有:null
队列内容:[peggy, thom, jonny]
弹出peggy
peggy的朋友有:null
队列内容:[thom, jonny]
弹出thom
thom是芒果销售商
bfs搜索结果:true
注意了:peggy 虽然会进入队列两次,但是只检查了一次!只检查了一次!只检查了一次!
广度优先搜索(breadth-first search BFS)的运行时间
运行时间至少为O(边数)【边是从一个人到另一个人的箭头或连接】
代码中还是使用了一个队列,其中包括要检查的每个人。将一个人添加到队列需要的时间是固定的,即O(1)。因此对每个人都这样做需要的总时间为O(人数)。
所以,广度优先搜索的运行时间为O(人数 + 边数),通常写作O(V + E),V为顶点数,E为边数。