Java中的深度优先搜索

2025/04/09

1. 概述

在本教程中,我们将探索Java中的深度优先搜索。

深度优先搜索(DFS)是一种用于树和图数据结构的遍历算法。深度优先搜索会深入每个分支,然后再探索另一个分支

在接下来的部分中,我们将首先了解树的实现,然后了解图的实现。

要了解如何在Java中实现这些结构,请查看我们之前关于二叉树的教程。

2. 树深度优先搜索

使用DFS遍历树有3种不同的顺序:

  1. 前序遍历
  2. 中序遍历
  3. 后序遍历

2.1 前序遍历

在前序遍历中,我们首先遍历根,然后遍历左子树和右子树

我们可以使用递归简单地实现前序遍历:

  • 访问当前节点
  • 遍历左子树
  • 遍历右子树
public void traversePreOrder(Node node) {
    if (node != null) {
        visit(node.value);
        traversePreOrder(node.left);
        traversePreOrder(node.right);
    }
}

我们还可以实现不使用递归的前序遍历。

为了实现迭代前序遍历,我们需要一个栈,遵循以下步骤:

  • 将根推入栈

  • 当栈不为空时

    • 弹出当前节点
    • 访问当前节点
    • 将右子节点推入栈,然后将左子节点推入栈
public void traversePreOrderWithoutRecursion() {
    Stack<Node> stack = new Stack<Node>();
    Node current = root;
    stack.push(root);
    while(!stack.isEmpty()) {
        current = stack.pop();
        visit(current.value);
        
        if(current.right != null) {
            stack.push(current.right);
        }    
        if(current.left != null) {
            stack.push(current.left);
        }
    }        
}

2.2 中序遍历

对于中序遍历,我们首先遍历左子树,然后遍历根,最后遍历右子树

二叉搜索树的中序遍历意味着按照节点值的升序遍历节点。

我们可以用递归的方式简单地实现中序遍历:

public void traverseInOrder(Node node) {
    if (node != null) {
        traverseInOrder(node.left);
        visit(node.value);
        traverseInOrder(node.right);
    }
}

我们也可以实现不使用递归的中序遍历:

  • 用根初始化当前节点

  • 当当前节点不为空或栈不为空时

    • 继续将左子节点推入栈,直到到达当前节点的最左子节点
    • 弹出并访问栈最左边的节点
    • 将当前节点设置为弹出节点的右子节点
public void traverseInOrderWithoutRecursion() {
    Stack stack = new Stack<>();
    Node current = root;

    while (current != null || !stack.isEmpty()) {
        while (current != null) {
            stack.push(current);
            current = current.left;
        }

        Node top = stack.pop();
        visit(top.value);
        current = top.right;
    }
}

2.3 后序遍历

最后,在后序遍历中,我们在遍历根之前先遍历左子树和右子树

我们可以遵循之前的递归解决方案:

public void traversePostOrder(Node node) {
    if (node != null) {
        traversePostOrder(node.left);
        traversePostOrder(node.right);
        visit(node.value);
    }
}

或者,我们也可以实现不使用递归的后序遍历:

  • 将根节点压入栈

  • 当栈不为空时

    • 检查我们是否已经遍历了左子树和右子树
    • 如果不是则将右子节点和左子节点推入栈
public void traversePostOrderWithoutRecursion() {
    Stack<Node> stack = new Stack<Node>();
    Node prev = root;
    Node current = root;
    stack.push(root);

    while (!stack.isEmpty()) {
        current = stack.peek();
        boolean hasChild = (current.left != null || current.right != null);
        boolean isPrevLastChild = (prev == current.right || (prev == current.left && current.right == null));

        if (!hasChild || isPrevLastChild) {
            current = stack.pop();
            visit(current.value);
            prev = current;
        } else {
            if (current.right != null) {
                stack.push(current.right);
            }
            if (current.left != null) {
                stack.push(current.left);
            }
        }
    }   
}

3. 图深度优先搜索

图和树之间的主要区别在于图可能包含循环

因此,为了避免循环搜索,我们在访问每个节点时会对其进行标记。

我们将看到图DFS的两种实现:使用递归的和不使用递归。

3.1 使用递归的图DFS

首先,让我们从简单的递归开始:

  • 我们将从给定节点开始
  • 将当前节点标记为已访问
  • 访问当前节点
  • 遍历未访问的相邻顶点
public boolean[] dfs(int start) {
    boolean[] isVisited = new boolean[adjVertices.size()];
    return dfsRecursive(start, isVisited);
}

private boolean[] dfsRecursive(int current, boolean[] isVisited) {
    isVisited[current] = true;
    visit(current);
    for (int dest : adjVertices.get(current)) {
        if (!isVisited[dest])
            dfsRecursive(dest, isVisited);
    }
    return isVisited;
}

3.2 无递归的图DFS

我们也可以不使用递归来实现图DFS,我们只需使用栈:

  • 我们将从给定节点开始

  • 将起始节点压入栈

  • 当栈不为空时

    • 将当前节点标记为已访问
    • 访问当前节点
    • 推入未访问的相邻顶点
public void dfsWithoutRecursion(int start) {
    Stack<Integer> stack = new Stack<Integer>();
    boolean[] isVisited = new boolean[adjVertices.size()];
    stack.push(start);
    while (!stack.isEmpty()) {
        int current = stack.pop();
        if(!isVisited[current]){
            isVisited[current] = true;
            visit(current);
            for (int dest : adjVertices.get(current)) {
                if (!isVisited[dest])
                    stack.push(dest);
            }
        }
    }
    return isVisited;
}

3.3 拓扑排序

图深度优先搜索有很多应用,其中一个著名的应用是拓扑排序。

有向图的拓扑排序是对其顶点的线性排序,以便对于每个边,源节点都位于目标节点之前

为了进行拓扑排序,我们需要对刚刚实现的DFS进行一个简单的补充:

  • 我们需要将访问过的顶点保存在栈中,因为拓扑排序是按相反顺序访问顶点
  • 遍历完所有邻居后,我们才将访问的节点推送到栈
public List<Integer> topologicalSort(int start) {
    LinkedList<Integer> result = new LinkedList<Integer>();
    boolean[] isVisited = new boolean[adjVertices.size()];
    topologicalSortRecursive(start, isVisited, result);
    return result;
}

private void topologicalSortRecursive(int current, boolean[] isVisited, LinkedList<Integer> result) {
    isVisited[current] = true;
    for (int dest : adjVertices.get(current)) {
        if (!isVisited[dest])
            topologicalSortRecursive(dest, isVisited, result);
    }
    result.addFirst(current);
}

4. 总结

在本文中,我们讨论了树和图数据结构的深度优先搜索。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章