目录
图简介
图是什么
广度优先搜索
查找最短路径
队列
实现图
实现算法
运行时间
小结
示例代码
C++
Python
C#
Java
JS
广度优先搜索让你能够找出两样东西之间的最短距离,不过最短距离的含义有很多!使用广度优先搜索可以:
编写国际跳棋AI,计算最少走多少步就可获胜;
编写拼写检查器,计算最少编辑多少个地方就可将错拼的单词改成正确的单词,如将
READED改为READER需要编辑一个地方;
根据你的人际关系网络找到关系最近的医生。
还有其他前往金门大桥的路线,但它们更远(需要四步)。这个算法发现,前往金门大桥的
最短路径需要三步。这种问题被称为最短路径问题(shorterst-path problem)。你经常要找出最短
路径,这可能是前往朋友家的最短路径,也可能是国际象棋中把对方将死的最少步数。解决最短
路径问题的算法被称为广度优先搜索。
要确定如何从双子峰前往金门大桥,需要两个步骤。
(1) 使用图来建立问题模型。
(2) 使用广度优先搜索解决问题。
就这么简单!图由节点和边组成。一个节点可能与众多节点直接相连,这些节点被称为邻居。
在前面的欠钱图中, Rama是Alex的邻居。 Adit不是Alex的邻居,因为他们不直接相连。但Adit既
是Rama的邻居,又是Tom的邻居。
广度优先搜索是一种用于图的查找算法,可帮助回答两类问题。
第一类问题:从节点A出发,有前往节点B的路径吗?
第二类问题:从节点A出发,前往节点B的哪条路径最短?
这样一来,你不仅在朋友中查找,还在朋友的朋友中查找。别忘了,你的目标是在你的人际
关系网中找到一位芒果销售商。因此,如果Alice不是芒果销售商,就将其朋友也加入到名单中。
这意味着你将在她的朋友、朋友的朋友等中查找。使用这种算法将搜遍你的整个人际关系网,直
到找到芒果销售商。这就是广度优先搜索算法。
在你看来,一度关系胜过二度关系,二度关系胜过三度关系,以此类推。因此,你应先在一
度关系中搜索,确定其中没有芒果销售商后,才在二度关系中搜索。广度优先搜索就是这样做的!
在广度优先搜索的执行过程中,搜索范围从起点开始逐渐向外延伸,即先检查一度关系,再检查
二度关系。顺便问一句:将先检查Claire还是Anuj呢? Claire是一度关系,而Anuj是二度关系,因
此将先检查Claire,后检查Anuj。
你按顺序依次检查名单中的每个人,看看他是否是芒果销售商。这将先在一度关系中查找,
再在二度关系中查找,因此找到的是关系最近的芒果销售商。广度优先搜索不仅查找从A到B的
路径,而且找到的是最短的路径。
注意,只有按添加顺序查找时,才能实现这样的目的。换句话说,如果Claire先于Anuj加入
名单,就需要先检查Claire,再检查Anuj。如果Claire和Anuj都是芒果销售商,而你先检查Anuj
再检查Claire,结果将如何呢?找到的芒果销售商并非是与你关系最近的,因为Anuj是你朋友的
朋友,而Claire是你的朋友。因此,你需要按添加顺序进行检查。有一个可实现这种目的的数据
结构,那就是队列(queue) 。
队列的工作原理与现实生活中的队列完全相同。
假设你与朋友一起在公交车站排队,如果你排在他前
面,你将先上车。队列的工作原理与此相同。队列类
似于栈,你不能随机地访问队列中的元素。队列只支
持两种操作: 入队和出队。
首先,需要使用代码来实现图。图由多个节点组成。
每个节点都与邻近节点相连,如果表示类似于“你→Bob”
这样的关系呢?好在你知道的一种结构让你能够表示这种关
系,它就是散列表!
记住,散列表让你能够将键映射到值。在这里,你要将节
点映射到其所有邻居。
Anuj、 Peggy、 Thom和Jonny都没有邻居,这是因为虽然有指向他们的箭头,但没有从他们
出发指向其他人的箭头。这被称为有向图(directed graph) ,其中的关系是单向的。因此, Anuj
是Bob的邻居,但Bob不是Anuj的邻居。 无向图(undirected graph)没有箭头,直接相连的节点互
为邻居。例如,下面两个图是等价的。
graph = {}
graph["you"] = ["alice", "bob", "claire"]
print(graph["you"])
for items in graph["you"]:
print(items)
先概述一下这种算法的工作原理。
这个算法将不断执行,直到满足以下条件之一:
找到一位芒果销售商;
队列变成空的,这意味着你的人际关系网中没有芒果销售商。
Peggy既是Alice的朋友又是Bob的朋友,因此她将被加入队列两次:一次是在添加Alice的朋
友时,另一次是在添加Bob的朋友时。因此,搜索队列将包含两个Peggy。但你只需检查Peggy一次,看她是不是芒果销售商。如果你检查两次,就做了无用功。因此,检查完一个人后,应将其标记为已检查,且不再检查他。
如果不这样做,就可能会导致无限循环。假设你的人际关系网类似于下面这样。
如果你在你的整个人际关系网中搜索芒果销售商,就意味着你将沿每条边前行(记住,边是
从一个人到另一个人的箭头或连接),因此运行时间至少为O(边数)。
你还使用了一个队列,其中包含要检查的每个人。将一个人添加到队列需要的时间是固定的,
即为O(1),因此对每个人都这样做需要的总时间为O(人数)。所以,广度优先搜索的运行时间为
O(人数 + 边数),这通常写作O(V + E),其中V为顶点(vertice)数, E为边数。
广度优先搜索指出是否有从A到B的路径。
如果有,广度优先搜索将找出最短路径。
面临类似于寻找最短路径的问题时,可尝试使用图来建立模型,再使用广度优先搜索来
解决问题。
有向图中的边为箭头,箭头的方向指定了关系的方向,例如, rama→adit表示rama欠adit钱。
无向图中的边不带箭头,其中的关系是双向的,例如, ross - rachel表示“ross与rachel约
会,而rachel也与ross约会”。
队列是先进先出(FIFO)的。
栈是后进先出(LIFO)的。
你需要按加入顺序检查搜索列表中的人,否则找到的就不是最短路径,因此搜索列表必
须是队列。
对于检查过的人,务必不要再去检查,否则可能导致无限循环
#include
#include
C++11
#include
#include
#include
#include
#include
#include
using std::cout;
using std::endl;
bool is_seller(const std::string& name) {
return name.back() == 'm';
}
template
bool search(const T& name, const std::unordered_map>& graph) {
std::queue search_queue;
std::unordered_set searched;
// add all friends to search queue
for (auto friend_name : graph.find(name)->second) {
search_queue.push(friend_name);
}
while (!search_queue.empty()) {
T person = search_queue.front();
// T& person = search_queue.front();
search_queue.pop();
// only search this person if you haven't already searched them.
if (searched.find(person) == searched.end()) {
if (is_seller(person)) {
cout << person << " is a mango seller!" << endl;
return true;
}
std::vector friend_list = graph.find(person)->second;
// add all friends of a person to search queue
for (T friend_name : friend_list) {
search_queue.push(friend_name);
}
// mark this person as searched
searched.insert(person);
}
}
return false;
}
int main() {
std::unordered_map> graph;
graph.insert({ "you",{ "alice", "bob", "claire" } });
graph.insert({ "bob",{ "anuj", "peggy" } });
graph.insert({ "alice",{ "peggy" } });
graph.insert({ "claire",{ "thom", "jonny" } });
graph.insert({ "anuj",{} });
graph.insert({ "peggy",{} });
graph.insert({ "thom",{} });
graph.insert({ "jonny",{} });
std::string name = "you";
bool result = search(name, graph);
cout << "Found mango seller: " << result << endl;
}
from collections import deque
def person_is_seller(name):
return name[-1] == 'm'
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"] = []
def search(name):
search_queue = deque()
search_queue += graph[name]
# This array is how you keep track of which people you've searched before.
searched = []
while search_queue:
person = search_queue.popleft()
# Only search this person if you haven't already searched them.
if person not in searched:
if person_is_seller(person):
print person + " is a mango seller!"
return True
else:
search_queue += graph[person]
# Marks this person as searched
searched.append(person)
return False
search("you")
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleApplication
{
public class Program
{
private static Dictionary _graph = new Dictionary();
public static void Main(string[] args)
{
_graph.Add("you", new[] { "alice", "bob", "claire" });
_graph.Add("bob", new[] { "anuj", "peggy" });
_graph.Add("alice", new[] { "peggy" });
_graph.Add("claire", new[] { "thom", "jonny" });
_graph.Add("anuj", Array.Empty());
_graph.Add("peggy", Array.Empty());
_graph.Add("thom", Array.Empty());
_graph.Add("jonny", Array.Empty());
Search("you");
}
private static bool Search(string name)
{
var searchQueue = new Queue(_graph[name]);
var searched = new List();
while (searchQueue.Any())
{
var person = searchQueue.Dequeue();
if (!searched.Contains(person))
{
if (PersonIsSeller(person))
{
Console.WriteLine($"{person} is a mango seller");
return true;
}
else
{
searchQueue = new Queue(searchQueue.Concat(_graph[person]));
searched.Add(person);
}
}
}
return false;
}
private static bool PersonIsSeller(string name)
{
return name.EndsWith("m");
}
}
}
import java.util.*;
public class BreadthFirstSearch {
private static Map> graph = new HashMap<>();
private static boolean search(String name) {
Queue searchQueue = new ArrayDeque<>(graph.get(name));
// This list is how you keep track of which people you've searched before.
List searched = new ArrayList<>();
while (!searchQueue.isEmpty()) {
String person = searchQueue.poll();
// Only search this person if you haven't already searched them
if (!searched.contains(person)) {
if (person_is_seller(person)) {
System.out.println(person + " is a mango seller!");
} else {
searchQueue.addAll(graph.get(person));
// Marks this person as searched
searched.add(person);
}
}
}
return false;
}
private static boolean person_is_seller(String name) {
return name.endsWith("m");
}
public static void main(String[] args) {
graph.put("you", Arrays.asList("alice", "bob", "claire"));
graph.put("bob", Arrays.asList("anuj", "peggy"));
graph.put("alice", Arrays.asList("peggy"));
graph.put("claire", Arrays.asList("thom", "jonny"));
graph.put("anuj", Collections.emptyList());
graph.put("peggy", Collections.emptyList());
graph.put("thom", Collections.emptyList());
graph.put("jonny", Collections.emptyList());
search("you");
}
}
'use strict';
function person_is_seller(name) {
return name[name.length-1] === 'm';
}
const 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"] = [];
function search(name) {
let search_queue = [];
search_queue = search_queue.concat(graph[name]);
// This array is how you keep track of which people you've searched before.
const searched = [];
while (search_queue.length) {
let person = search_queue.shift();
// Only search this person if you haven't already searched them
if (searched.indexOf(person) === -1) {
if (person_is_seller(person)) {
console.log(person + ' is a mango seller!');
return true;
} else {
search_queue = search_queue.concat(graph[person]);
// Marks this person as searched
searched.push(person);
}
}
}
return false;
}
search('you'); // thom is a mango seller!