学习笔记
学习书目:《算法图解》- Aditya Bhargava
文章目录
- 运行时间
图简介
今天是五一,假如我要从家出发去公园玩,现在可去公园的公交车路线如下:
现在,我想找一条换乘最少的线路,该使用什么样的算法呢?
我们先找出一步就能到达的地方,显而易见,一步能到达A、B;再找出两步能到达的地方,经过简单寻找,我们发现两步能到达E、D、C三个地;第三步呢?可以看出第三步可以到达公园和E。很好!此时我们就找到了换乘最少的路线:家–1-->路–>A–3路–>E–5路–>公园
这种问题被称为最短路径问题。我们要找出最短路径,这可能是前往朋友家的最短路径,也可能是国际象棋中把对方将死的最少步数。解决最短路径问题的算法被称为广度优先搜索。
我们解决这个问题时,用了两个步骤:
(1)使用图来建立问题模型
(2)使用广度优先搜索解决问题
图是啥
图模拟了一组连接,比如可像下图一样表示小黄欠我钱:
我们再看一幅更复杂的图:
可以看到这幅图由节点和边组成,一个节点可能与众多节点直接相连,这些节点被称为邻居。比如,我是小黄的邻居,但奶奶不是小黄的邻居;奶奶既是我的邻居,又是大白的邻居;但是,奶奶没有邻居,因为虽然有指向她的箭头,却没有从她出发的箭头。这种图叫做有向图,其中的关系是单向的。无向图则没有箭头,直接相连的节点互为邻居。
我们看到,下面两个图是等价的:
广度优先搜索
广度优先搜索是一种用于图的查找算法,可帮助回答两类问题。
第一类问题:从节点A出发,有前往节点B的路径吗?
第二类问题:从节点A出发,前往节点B的哪条路径最短?
假如我是一位作者,我想在我的Twitter的朋友列表里找一位编辑。我的想法很简单,先在朋友里找有没有编辑,没有的话就在朋友的朋友里寻找,再没有的话,就在朋友的朋友的朋友里寻找…以此类推
现在我有3个朋友:
我先创建一个朋友名单:
['Huang', 'Hei', 'Bai']
然后依次检查我的3个朋友是否是编辑。假如我没有朋友是编辑,那么我就必须在朋友的朋友中寻找:
比如,我发现Huang不是编辑,那我就把它的朋友Write和Tim加入我的朋友名单,并把Huang从名单中剔除:
['Hei', 'Bai', 'Write', 'Tim']
使用这种算法将搜遍我的整个人际关系网,直到找到编辑。这就是广度优先搜索算法。
寻找最短路径
由我在Twitter的朋友列表里找编辑的例子中,我们已经回答了广度优先搜索的第一个问题(从节点A出发,有前往节点B的路径吗?)现在,我们就要回答第二个问题,即哪位编辑是离我关系最近。比如,我的朋友和我是一度关系,我朋友的朋友和我是二度关系。在我看来,一度关系胜过二度关系。因此,我要现在一度关系中寻找编辑,没有的话,再从二度关系中寻找编辑,以此类推。
需要注意的是,我们必须把一度关系查找完,才能查找二度关系。比如,我必须先查找完Huang, Hei, Bai才能查找Write等二度关系。
以我们刚刚建立的朋友名单为例,一度关系在二度关系之前加入名单:
['Hei', 'Bai',' Write', 'Tim']
我们按顺序依次检查名单中的每个人,看看他是否是编辑。这将先在一度关系中查找,再在二度关系中查找,因此找到的是关系最近的编辑。
注意!只有按顺序查找才能找到与我关系最近的编辑。换句话说Bai先于Tim加入名单,就要先检查Bai。有一个可实现这种目的的数据结构,那就是队列。
队列
队列类似于栈,你不能随机地访问队列中的元素。队列只支持两种操作:入队和出队。如果我将A和B加入队列,先加入队列的A也将先出队,而后加入的B则会后出队。
队列是一种先进先出(First In First Out,FIFO)的数据结构,而栈是一种后进先出(Last In First Out,LIFO)的数据结构。
实现图
现在,我们将用散列表来表达这种我-->Huang
的关系,并用python代码来实现图:
graph = {}
graph['me'] = ['Huang', 'Hei', 'Bai']
graph['Huang'] = ['Write', 'Tim']
graph['Bai'] = ['Tim']
graph['Hei'] = ['Black', 'Ada']
graph['Write'] = []
graph['Tim'] = []
graph['Black'] = []
graph['Ada'] = []
我们看到Write、Tim、Black、Ada没有邻居,因为只有指向它们的箭头,却没有从它们出发的箭头。
实现算法
python代码:
from collections import deque
#判断谁是编辑
def person_is_edit(name):
return name[-1] == 'a'
#假设名字最后一个字母是a就是编辑
#查找某人的关系列表中谁是编辑
def search(name):
search_queue = deque()
search_queue += graph[name]
searched = []
#记录已经检查过的人,防止低效率和无限循环
while search_queue:
person = search_queue.popleft()
if person not in searched:
if person_is_edit(person):
print('I find you {}!'.format(person))
return True
else:
search_queue += graph[person]
searched.append(person)
return False
search('me')
控制台输出:
I find you Ada!
我们看到,我们在上面的python代码中加入了一个已搜索名单searched,这是为了防止循环和低效率。比如,Huang和Bai都有一个朋友Tim,但是我们只需要检查一次Tim,否则重复查询就是做了无用功。
并且如果出现下面这种情况,我们就会进入死循环:
所以,在检查一个人是否是编辑之前,确认此人是否被检查过,就十分重要了。
运行时间
如果我在我的关系网中搜寻编辑,那就意味着我沿着每条边前行,因此运行时间至少是O(边数)。这里,我们还使用了一个队列,其中包含要检查的每个人。将一个人添加到队列需要的时间是固定的,即为O(1),因此对每个人都这样做需要的总时间为O(人数)。所以,广度优先搜索的运行时间为O(人数 + 边数),这通常写作O(V + E),其中V 为顶点数,E 为边数。