《算法图解》第六章笔记

本章讲述了数据结构——图、队列,新算法——广度优先搜索(breadth-first search BFS)

  • 图是由节点 (node) 和边 (edge) 组成的。
  • 图是用于模拟不同东西是如何相连的。
  • 图分为有向图和无向图,有向图方向确定了关系的方向,无向图确定了关系的双向。

假设你和朋友打牌,需要模拟谁欠谁的钱。可以像下面这样指出 Alex 欠 Rama 的钱。
在这里插入图片描述
完整的欠钱图可能类似于下面这样。
在这里插入图片描述
在我们生活中图的作用也非常大,这种数据结构可以直观的表示出节点与节点的关系。

例如我自己在回家或者出去旅游时就会用图来描述数据
在这里插入图片描述
在这里插入图片描述
上面都是有向图,下面讲一下无向图。

无向图就是没有箭头的边构成的图,无向图和有向图有等价关系,如下:
在这里插入图片描述
树是一种特殊的图。 不信可以动手画画哦。

上述的都只是为了让人能简单直观的知道图是长什么样的(节点+边),有什么作用(通过模拟连接来描述节点之间的关系),方便于引入广度优先搜索算法。还有很多原理性的东西建议去找《数据结构》之类的书深入了解一下。

数据结构-图的基本概念和存储结构——百度文库

这里就不再赘述。

广度优先搜索(breadth-first search BFS)

广度优先搜索主要回答或者说解决两类问题:

  1. 从节点A出发,有前往节点的B的路径吗?
  2. 从节点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 之间反复横跳。

在这里插入图片描述
因此,检查一个人之前,要确认之前没检查过他,这很重要。

  1. 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’为结尾 
  1. 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;
    }
  1. 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为边数。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值