【数据结构与算法 - Swift实现】16 - 广度优先搜索和深度优先搜索

广度优先搜索深度优先搜索是在图的基础上来讨论的,它们都是图的顶点的遍历方式。下面我们一个个来研究一下。

两种遍历方式的讲解,我们都已下面这个图为例:

构建这个图的代码如下:

let graph = AdjacencyList<String>()

let a = graph.createVertex(value: "A")
let b = graph.createVertex(value: "B")
let c = graph.createVertex(value: "C")
let d = graph.createVertex(value: "D")
let e = graph.createVertex(value: "E")
let f = graph.createVertex(value: "F")
let g = graph.createVertex(value: "G")
let h = graph.createVertex(value: "H")

graph.addUndirectedEdge(between: a, and: b, weight: nil)
graph.addUndirectedEdge(between: a, and: c, weight: nil)
graph.addUndirectedEdge(between: b, and: d, weight: nil)
graph.addUndirectedEdge(between: c, and: e, weight: nil)
graph.addUndirectedEdge(between: c, and: f, weight: nil)
graph.addUndirectedEdge(between: d, and: e, weight: nil)
graph.addUndirectedEdge(between: e, and: f, weight: nil)
graph.addUndirectedEdge(between: d, and: g, weight: nil)
graph.addUndirectedEdge(between: e, and: h, weight: nil)
graph.addUndirectedEdge(between: f, and: h, weight: nil)

广度优先搜索

广度优先搜索可以用来解决这些问题:1)生成最小生成树;2)寻找顶点之间所有可能的路径;3)寻找顶点之间的最短路径。

我们把从一个起点开始的边的终点称为邻居,那么广度优先搜索的执行过程可以概括为:首先选择一个起点,然后遍历完这个起点的所有邻居之后再遍历这些邻居的邻居,以此类推。

我们将使用队列来存储要遍历的顶点,队列先进先出的特性可以保证一个顶点的邻居在邻居的邻居之前遍历。下面我们一步步来演示遍历的过程。

  1. 起点是 A,首先遍历的是 A,把 A 放到队列中,状态如下:
  1. 只要队列不是空的,我们就拿出一个元素,然后遍历下一个元素。这里我们拿出 A,然后把 A 的邻居加入队列中,状态如下:
  1. 把 B 拿出,然后再把 B 的邻居 D 加入队列中,邻居 A 已经遍历过了,不需要再加入。状态如下:
  1. 把 C 拿出,然后把邻居 E 和 F 加入队列中,邻居 A 已经遍历过了,不需要再加入。状态如下:
  1. 把 D 拿出,然后把邻居 G 加入队列中,邻居 B 已经遍历过了,邻居 E 已经在队列中,不需要再加入。状态如下:
  1. 把 E 拿出,然后把邻居 H 加入队列中,邻居 C 已经遍历过了,邻居 F 已经在队列中,不需要再加入。状态如下:
  1. 把 F 拿出,邻居 C 和 E 已经遍历过了,邻居 H 已经在队列中,不需要再加入。状态如下:
  1. 把 G 拿出,邻居 D 已经遍历过了,不需要再加入。状态如下:
  1. 最后把 G 拿出,邻居 E 和 F 已经遍历过了,不需要再加入。状态如下:

代码实现

代码实现如下:

extension Graph {
    func breathFirstSearch(from source: Vertex<Element>) -> [Vertex<Element>] {
        // 存储即将遍历的顶点
        var queue = Queue<Vertex<Element>>()
        // 存储已经入队的顶点
        var enqueued: Set<Vertex<Element>> = []
        // 存储已经遍历的顶点
        var visited: [Vertex<Element>] = []
        
        queue.enqueue(source)
        enqueued.insert(source)
        
        while let vertex = queue.dequeue() { // 取出队列的顶点
            visited.append(vertex)
            let neighborEdges = edges(from: vertex)
            for edge in neighborEdges
                where !enqueued.contains(edge.destination) {
                    // 把没有入队的邻居加入队列中
                    queue.enqueue(edge.destination)
                    enqueued.insert(edge.destination)
            }
        }
        
        return visited
    }
}

测试

let vertices = graph.breathFirstSearch(from: a)
vertices.forEach { print($0) }

// 结果
0: A
1: B
2: C
3: D
4: E
5: F
6: G
7: H

如果要使用 AdjacencyMatrix,添加边的时候 weight 必须要有值。

性能分析

在遍历过程中,每个顶点入队一次,时间复杂度为 O(V),在遍历过程中,我们还遍历了所有的边,时间复杂度为 O(E),所以总体的事件复杂度为 O(V + E)

深度优先搜索

深优先搜索可以用来解决这些问题:1)拓扑排序;2)检测是有循环;3)寻找路径;4)在图中寻找连通的组件。

深度优先搜索的执行过程可以概括为:从一个顶点开始,尽可能的往下搜寻,直到到达底部为止;然后在往回走,搜寻另外一个分支,直到遍历完整个图。

我们将使用栈来存储要遍历的顶点,队列后进先出的特性可以让我们在一个分支走到底部时往回走。每次 push 一个顶点,就意味着往更深一层。下面我们一步步来演示遍历的过程。

  1. 以 A 为起点,首先把 A push 到栈中,状态如下:
  1. 只要栈不为空,就查看栈顶的顶点,然后栈顶顶点的第一个邻居 push 到栈上,也即是 B 入栈,状态如下:
  1. 现在栈顶顶点为 B,第一个邻居为 D,所以把 D push 到栈中,状态如下:
  1. 现在栈顶顶点为 D,第一个邻居为 E,所以把 E 入栈,状态如下:
  1. 现在栈顶顶点为 E,第一个邻居为 C,所以把 C 入栈,状态如下:
  1. 现在栈顶顶点为 C,它的两个邻居 A 和 E,都已在栈中,所以把第三个邻居 F 入栈,状态如下:
  1. 现在栈顶顶点为 F,邻居 E 已经在栈中,所以把 H 入栈,状态如下:
  1. 现在栈顶顶点为 H,他已经没有其他邻居,到达了底部,所以把它从栈顶中取出,状态如下:
  1. 接下来的三个栈顶顶点 F、C 和 E,都没有其他邻居,都要被取出。取出后状态如下:
  1. 栈顶顶点 D 还有另外一个邻居 G 没有遍历,所以把 G 入栈,状态如下:
  1. 栈顶顶点 G,没有其他邻居,把 G 取出。状态如下:
  1. 接下来的三个栈顶顶点 D、B 和 A,都没有其他邻居,都要被取出。取出后遍历完成。

代码实现

extension Graph {
    func depthFirstSearch(from source: Vertex<Element>) -> [Vertex<Element>] {
        // 存储即将遍历的顶点
        var stack = Stack<Vertex<Element>>()
        // 存储已经入栈的顶点
        var pushed: Set<Vertex<Element>> = []
        // 存储已经遍历过的顶点
        var visited: [Vertex<Element>] = []
        
        stack.push(source)
        pushed.insert(source)
        visited.append(source)
        
        outer: while let vertex = stack.top { // 判断栈顶的顶点是否为空
            let neighborEdges = edges(from: vertex)
            guard !neighborEdges.isEmpty else {
                // 如果当前顶点没有邻居,把栈顶顶点取出
                stack.pop()
                continue
            }
            for edge in neighborEdges {
                if !pushed.contains(edge.destination) { // 当前顶点的邻居没遍历过
                    stack.push(edge.destination)
                    pushed.insert(edge.destination)
                    visited.append(edge.destination)
                    // 继续执行外部的 while 循环,尝试往更深的一层继续遍历
                    continue outer
                }
            }
            // 没有其他没有遍历过的邻居,把栈顶顶点取出
            stack.pop()
        }
        
        return visited
    }
}

测试

let vertices = graph.depthFirstSearch(from: a)
vertices.forEach { print($0) }

// 结果
0: A
1: B
3: D
4: E
2: C
5: F
7: H
6: G

性能分析

在遍历过程中,每个顶点都会被遍历一次,时间复杂度为 O(V),在遍历过程中,我们还遍历了所有的邻居,以确定是否还要继续往更深一层继续遍历,时间复杂度为 O(E),所以总体的事件复杂度为 O(V + E)。空间复杂度为 O(V),因为我们要存储图中的所有顶点。

完整代码 >>

参考资料

Data Structures and Algorithms in Swift --- raywenderlich.com,如果想看原版书籍,请点击链接购买。

下一篇文章:【数据结构与算法 - Swift实现】17 - 迪克斯特拉 (Dijkstra) 算法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342