美文网首页
关于BFS算法【结点组织方式】的一点思考 2020-08-14(

关于BFS算法【结点组织方式】的一点思考 2020-08-14(

作者: 9_SooHyun | 来源:发表于2020-08-14 18:40 被阅读0次

1.最常见的BFS算法结构

class Solution:
    def bfs(self, root):
        if not root:
            return  
            
        queue = list()
        queue.append(root)
        # 核心代码开始
        while queue:
            l = len(queue)
            for i in range(l):
                out = queue.pop(0)
                for child in out.children:
                    queue.append(child)

首先要明确,BFS的核心是【滚动组织最新一层的节点】

上面是我们最熟悉的BFS结构,通过维护一个队列,不断弹出结点并把下一层结点加入队列来实现整个BFS,每一层结点的组织方式在这里是【队列】

也许这种结构太习以为常了,于是算法思维就没能再打破一些条条框框,总是想着BFS就是搞个队列然后实现

今天刚好碰到两个BFS的问题,对【结点组织方式】有了一点思考

2.先说思考的结论

BFS中,结点是分层的,对每一层结点我们需要用一种数据结构把它们组织起来,最常见的就是上面的队列了。而队列的本质是什么,线性表。用线性表去组织每一层结点,非常自然。但是,线性表除了队列这样的顺序表,还有链表,链表同样可以拿来组织每层的结点。再进一步想,线性表可以组织每层结点,非线性的数据结构可以吗?当然是可以的,集合、哈希表都可以用来组织每一层的结点

线性结构和非线性结构组织每层结点的区别主要在于,线性结构可以天然保存结点的先后顺序,而当我们并不关心同层结点间的顺序而恰好有一些查找结点的需求,那么集合、哈希表这样的结构就派上用场了

每层结点组织方式:

  • 线性
    • 队列、数组
    • 链表
  • 非线性
    • set、hashtable

3.实战感受

3.1 链表组织每层结点

117. 填充每个节点的下一个右侧节点指针 II

给定一个二叉树
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL
初始状态下,所有 next 指针都被设置为 NULL
你只能使用常量级额外空间

分析:
这题用常规的层次遍历很好做,用队列组织每一层结点,然后按顺序调整next指针。但是要求空间复杂度O(1),这就要想办法改造层次遍历了。根据题目要求,每层结点通过next指针相连形成链表,那我们就可以天然借助链表这个结构去访问和组织每一层的结点。核心就是,队列组织每层结点 -> 链表组织每层结点,但本质也还是一样的

"""
# Definition for a Node.
class Node:
    def __init__(self, val: int = 0, left: 'Node' = None, right: 'Node' = None, next: 'Node' = None):
        self.val = val
        self.left = left
        self.right = right
        self.next = next
"""

class Solution:
    def connect(self, root: 'Node') -> 'Node':
        cur = root
        while cur: # 从每层头节点cur开始遍历当前层,并链接下一层
            pointer = Node() # 用pointer走一遍来链接下一层
            most_left = pointer
            # 疯狂链接下一层
            while cur:
                if cur.left:
                    pointer.next = cur.left
                    pointer = pointer.next
                if cur.right:
                    pointer.next = cur.right
                    pointer = pointer.next
                cur = cur.next
            
            # cur指向链接好的下一层的最左节点
            cur = most_left.next
        return root

3.2 集合组织每层结点

127. 单词接龙

给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:
每次转换只能改变一个字母。
转换过程中的中间单词必须是字典中的单词。

说明:
如果不存在这样的转换序列,返回 0。
所有单词具有相同的长度。
所有单词只由小写字母组成。
字典中不存在重复的单词。
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例 1:
输入:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]
输出: 5
解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog",
返回它的长度 5

分析:
每个单词看成一个节点,相差只有一个字母的单词都认为邻接,就抽象成一个无向图结构的问题。问题变成,无向图中寻找到从起点到终点的最短路径距离,后面就是BFS

因此,问题的关键实际上在于建图

  • 1.建图。差距只有一个字母的单词之间形成无向边,使用hashtable邻接表把所有差距为1个字母的单词以list形式聚合起来,表示它们之间是邻接关系
如{'*it':['hit'], 'h*t':['hit','hot'], ...}
  • 2.注意到起点和终点都给出了,因此可以优化成双向的BFS
  • 3.双向BFS的结点组织方式-set。使用双向BFS,往往是图结点的相遇问题。两点各自逐层向外扩张,如同两个石头泛起的涟漪,当涟漪最外层相遇时,就对应最短距离。我们需要记录的是双方的最外层结点,并且判断两者是否存在交集,而层内结点的顺序并不重要。因此,这里使用set来组织每层结点,这样一来,判断A层某个结点是否在B层中,只需要O(1)的时间
class Solution(object):
    ###### 基于集合的双向BFS模式 ######
    def ladderLength(self, beginWord, endWord, wordList):
        if endWord not in wordList:
            return 0

        # 基于wordList建图,使用【同类表】形式。如{'*it':['hit'], 'h*t':['hit','hot'], ...}
        # 建立了图之后就是常规的bfs问题
        size, graph_dic = len(beginWord), defaultdict(list)
        for w in wordList:
            for _ in range(size):
                graph_dic[w[:_]+"*"+w[_+1:]].append(w)
        
        # 两个set的双向BFS
        s, s1 = set(), set()
        s.add(beginWord)
        s1.add(endWord)
        # 标记哪些单词已经访问
        mark_dic = defaultdict(bool)  # bool 的默认值是false,因此所有不在list里的是false
        mark_dic[beginWord] = True
        mark_dic[endWord] = True
        steps = 1
        while s and s1:
            # strick: 总是选择元素少的一方进行bfs扩散
            if len(s) > len(s1):
                s, s1 = s1, s
            
            # 用temp记录本轮s集合的bfs扩散结果
            temp = set()
            l = len(s)
            for _ in range(l):
                cur_word = s.pop() # 随机pop一个
                for i in range(size):               # 找邻居
                    for neighbour in graph_dic[cur_word[:i]+"*"+cur_word[i+1:]]:
                        # 如果从起点、终点扩散的两军前锋相遇,判断元素是否在集合中比判断是否在数组中更快
                        if neighbour in s1: 
                            return steps + 1
                        if not mark_dic[neighbour]:
                            mark_dic[neighbour] = True
                            temp.add(neighbour)  
            steps += 1
            s = s1
            s1 = temp
        return 0

相关文章

  • 关于BFS算法【结点组织方式】的一点思考 2020-08-14(

    1.最常见的BFS算法结构 首先要明确,BFS的核心是【滚动组织最新一层的节点】 上面是我们最熟悉的BFS结构,通...

  • LeetCode指北-BFS

    BFS就是广度优先算法,BFS相对DFS来说不太直观。BFS中,我们会搜索r的“孙子节点”之前先访问结点r的所有相...

  • 走迷宫(BFS例题)

    BFS例题---走迷宫 BFS对应数据结构queue BFS算法时间复杂度O(n^2) BFS算法不走走过的点 的...

  • G - 7 UVA - 11624

    所用算法:BFS

  • H - 8 POJ - 3984

    所用算法:BFS

  • [LeetCode打卡] BFS #958 Check Comp

    难度:?? 考察点:BFS 解法:BFS把每一个结点的两个子结点放入队列中,当队列第一个值为None的时候循环结束...

  • ORID27

    [127] Word Ladder解题报告 BFS算法 用什么算法?这道题需要用 BFS为什么用这个算法(那些条件...

  • LeetCode 第207题:课程表

    1、前言 2、思路 使用拓扑排序的方法,拓扑排序其实是使用的 BFS 算法,简而言之使用 BFS 算法解题。算法流...

  • BFS

    [TOC] BFS 和 DFS BFS广度有限搜索和DFS深度优先搜索算法是特别常用的两种算法 DFS 算法就是回...

  • LeetCode 773. 滑动谜题

    1、题目 2、分析 直接套用BFS的算法框架就可以。要注意“邻居”数组的定义方式和遍历方式 3、代码

网友评论

      本文标题:关于BFS算法【结点组织方式】的一点思考 2020-08-14(

      本文链接:https://www.haomeiwen.com/subject/pptndktx.html