Shaula Blog

美丽的东西都是肤浅的

Swift 中的 DFS 与 BFS 算法

最近在开发过程中需要实现树形结构的选中状态联动(包括全选、部分选和未选状态),这里使用到了树的遍历算法。深度优先搜索(DFS)和广度优先搜索(BFS)是两种常用的图/树遍历算法,它们的主要区别在于访问节点的顺序:

  • 深度优先搜索(DFS):优先深入一条路径,直到访问到底再回溯。
  • 广度优先搜索 (BFS):逐层访问节点,优先处理同一层的所有节点。

代码实现

首先,我们定义一个节点的泛型协议:

protocol TreeNode: Identifiable {
    var id: String { get }
    var label: String { get }
    var children: [T]? { get }
    var parent: T? { get set }
}

深度优先搜索 (DFS)

深度优先遍历通常包括三种基本访问顺序:

  • 前序遍历:遵循"根 → 左子树 → 右子树"的顺序,优先访问当前节点,再处理子树。此方法常用于复制树结构,其遍历序列的首元素必为根节点。
  • 中序遍历:按"左子树 → 根 → 右子树"的顺序访问,适用于二叉搜索树生成升序序列。中序遍历序列可结合前序或后序序列唯一确定二叉树的结构。
  • 后序遍历:采用"左子树 → 右子树 → 根"的顺序,适用于需要优先处理子节点的场景,如表示式求值等。

DFS 算法利用栈(Stack)的相关概念:

  • 递归:隐式使用调用栈。
  • 非递归:显式用数组模拟栈(使用 appendremoveLast)。

以下是前序遍历的非递归与递归实现的 DFS 代码示例:

非递归实现

func dfs<T: TreeNode>(_ root: T) -> Set<T> {
    var stack: [T] = [root]
    var result = Set<T>()
    while !stack.isEmpty {
        let current = stack.removeLast()
        
        // 防止重复访问和循环
        if result.contains(current) { continue }
        result.insert(current)
        
        // 逆序压栈确保顺序一致
        for child in current.children?.reversed() ?? [] {
            if var item = child as? T {
                item.parent = current
                stack.append(item)
            }
        }
    }
    return result
}

递归实现

func dfsRecursive<T: TreeNode>(_ root: T, visited: inout Set<T>) {
    guard !visited.contains(root) else { return }
    visited.insert(root)
    for child in root.children ?? [] {
        if var item = child as? T {
            item.parent = root
            dfsRecursive(item, visited: &visited)
        }
    }
}

广度优先搜索 (BFS)

BFS 算法利用队列(Queue)的相关概念。可以使用数组模拟队列(使用 appendremoveFirst),但需要注意 removeFirst() 在处理大型数据集时的性能问题。为此,可以使用两个数组交替存储层级,从而避免 removeFirst() 的 O(n) 开销,以下是优化后的版本:

func bfs<T: TreeNode>(_ root: T) -> Set<T> {
    var currentLevel: [T] = [root]
    var result = Set<T>()
    
    while !currentLevel.isEmpty {
        // 使用两个数组交替存储层级,避免 removeFirst() 的 O(n) 开销
        var nextLevel: [T] = []
        for node in currentLevel {
            result.insert(node)
            for child in node.children ?? [] {
                if var item = child as? T, !result.contains(item) {
                    item.parent = node
                    nextLevel.append(item)
                }
            }
        }
        currentLevel = nextLevel
    }
    return result
}

总结

DFS(深度优先搜索)和 BFS(广度优先搜索)不仅是遍历树的标准算法,更是遍历图(Graph)的基础和核心算法。树可以视作一种特殊类型的图(无环、连通的有向图或更严格的根树定义),因此针对树的遍历算法同样可以推广到更一般的图结构上。在进行图遍历以计算最短路径或最少步数时,建议优先选择广度优先搜索 (BFS);而在需要枚举所有解或进行回溯时,则应首选深度优先搜索 (DFS)。同时,由于图的结构更为灵活,节点之间可能存在多条边,且没有层次关系,一个节点可以有多个父节点和子节点,因此之前定义的节点泛型协议需要进行相应修改。