深度优先搜索(DFS)详解及C++实现

深度优先搜索(DFS)详解及C++实现

一、什么是深度优先搜索(DFS)?

深度优先搜索(Depth-First Search,简称DFS)是一种用于遍历或搜索树或图的算法。其核心思想是:尽可能深地搜索图的分支,当某条分支搜索到尽头无法继续前进时,回溯到上一个节点,再选择另一条未探索的分支继续搜索,直到所有节点都被访问完毕。

可以用一个生动的比喻理解DFS:想象你走进一个迷宫,每次遇到岔路时,随机选择一条路一直走,直到走到死胡同(无法继续前进),然后沿原路返回上一个岔路,选择另一条未走过的路继续探索,直到找到出口或遍历完整个迷宫。

DFS的实现通常依赖栈(Stack)这种数据结构(手动实现时),或者直接利用递归函数调用栈(更简洁,也是最常用的方式)。递归实现的本质是将每次的节点访问和回溯过程交给函数栈来管理,无需手动维护栈结构。

二、DFS的核心特性与适用场景

1. 核心特性

  • 不撞南墙不回头:优先深入探索当前分支,而非横向遍历同级节点;

  • 回溯思想:探索到尽头后,返回上一节点继续探索其他分支,需要记录节点访问状态(避免重复访问);

  • 空间复杂度:取决于递归深度或栈的大小,最坏情况下为O(n)(n为节点数);

  • 时间复杂度:遍历图时,时间复杂度为O(V+E)(V为顶点数,E为边数);遍历树时,为O(n)(树中边数为n-1)。

2. 适用场景

  • 图的遍历(连通分量查找、拓扑排序等);

  • 树的遍历(前序、中序、后序遍历,路径搜索等);

  • 迷宫问题(最短路径不适用,DFS不保证最短,需用BFS;但可用于判断是否存在路径);

  • 排列组合问题(如全排列、子集生成等);

  • 回溯法解决的经典问题(如N皇后、数独求解等)。

三、DFS的两种实现方式(C++)

DFS的实现分为递归实现非递归实现(手动栈)。递归实现代码简洁,易于理解;非递归实现则更灵活,可避免递归深度过大导致的栈溢出问题。下面以「无向图的遍历」和「树的前序遍历」为例,讲解两种实现方式。

1. 递归实现(最常用)

递归实现的核心逻辑:

  1. 访问当前节点,标记为已访问;

  2. 遍历当前节点的所有邻接节点;

  3. 对每个未访问的邻接节点,递归调用DFS函数。

案例1:无向图的DFS遍历

假设我们有如下无向图:

0 → 1 → 2

0 → 3 → 4

用邻接表存储图(邻接表是图的常用存储方式,适合稀疏图),然后通过递归DFS遍历所有节点。

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

// 邻接表存储图
vector<vector<int>> adj;
// 标记节点是否被访问
vector<bool> visited;

// 递归实现DFS
void dfs(int u) {
    // 标记当前节点为已访问
    visited[u] = true;
    // 访问当前节点(此处为打印节点值)
    cout << u << " ";
    
    // 遍历当前节点的所有邻接节点
    for (int v : adj[u]) {
        // 如果邻接节点未被访问,递归调用DFS
        if (!visited[v]) {
            dfs(v);
        }
    }
}

int main() {
    // 图的节点数
    int n = 5;
    // 初始化邻接表和访问标记数组
    adj.resize(n);
    visited.resize(n, false);
    
    // 构建无向图
    adj[0].push_back(1);
    adj[1].push_back(0);
    adj[1].push_back(2);
    adj[2].push_back(1);
    adj[0].push_back(3);
    adj[3].push_back(0);
    adj[3].push_back(4);
    adj[4].push_back(3);
    
    cout << "DFS遍历结果(递归):";
    // 从节点0开始遍历(若图不连通,需遍历所有未访问节点)
    dfs(0);
    cout << endl;
    
    return 0;
}
    

输出结果:

DFS遍历结果(递归):0 1 2 3 4

说明:从节点0出发,先深入探索1→2分支,回溯后再探索3→4分支,最终遍历所有节点。

案例2:二叉树的前序遍历(DFS)

二叉树的前序遍历(根→左→右)是DFS的典型应用,递归实现非常简洁。

cpp 复制代码
#include <iostream>
using namespace std;

// 二叉树节点定义
struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// 递归实现前序遍历(DFS)
void preorderDFS(TreeNode* root) {
    // 递归终止条件:节点为空
    if (root == nullptr) {
        return;
    }
    // 访问根节点
    cout << root->val << " ";
    // 递归遍历左子树
    preorderDFS(root->left);
    // 递归遍历右子树
    preorderDFS(root->right);
}

int main() {
    // 构建一棵二叉树:
    //       1
    //        \
    //         2
    //        /
    //       3
    TreeNode* root = new TreeNode(1);
    root->right = new TreeNode(2);
    root->right->left = new TreeNode(3);
    
    cout << "二叉树前序遍历(DFS):";
    preorderDFS(root);
    cout << endl;
    
    // 释放内存(简化处理,实际应手动遍历释放)
    delete root->right->left;
    delete root->right;
    delete root;
    
    return 0;
}
    

输出结果:

二叉树前序遍历(DFS):1 2 3

2. 非递归实现(手动栈)

当递归深度过大时(如遍历深度为1e4的树),会导致栈溢出(C++默认递归栈大小有限),此时需要用手动栈实现DFS。核心逻辑与递归一致,只是将递归调用栈替换为手动维护的栈。

非递归实现步骤:

  1. 将起始节点压入栈,标记为已访问;

  2. 弹出栈顶节点,访问该节点;

  3. 将该节点的所有未访问邻接节点压入栈(注意:为保证遍历顺序与递归一致,需逆序压入,因为栈是先进后出);

  4. 重复步骤2-3,直到栈为空。

案例1:无向图的DFS遍历(非递归)

cpp 复制代码
#include <iostream>
#include <vector>
#include <stack>
using namespace std;

vector<vector<int>> adj;
vector<bool> visited;

// 非递归实现DFS(手动栈)
void dfsNonRecursive(int start) {
    stack<int> st;
    // 压入起始节点,标记为已访问
    st.push(start);
    visited[start] = true;
    
    while (!st.empty()) {
        // 弹出栈顶节点
        int u = st.top();
        st.pop();
        // 访问当前节点
        cout << u << " ";
        
        // 遍历邻接节点(逆序压入,保证遍历顺序与递归一致)
        for (auto it = adj[u].rbegin(); it != adj[u].rend(); ++it) {
            int v = *it;
            if (!visited[v]) {
                visited[v] = true;
                st.push(v);
            }
        }
    }
}

int main() {
    int n = 5;
    adj.resize(n);
    visited.resize(n, false);
    
    // 构建无向图(同递归案例)
    adj[0].push_back(1);
    adj[1].push_back(0);
    adj[1].push_back(2);
    adj[2].push_back(1);
    adj[0].push_back(3);
    adj[3].push_back(0);
    adj[3].push_back(4);
    adj[4].push_back(3);
    
    cout << "DFS遍历结果(非递归):";
    dfsNonRecursive(0);
    cout << endl;
    
    return 0;
}
    

输出结果:

DFS遍历结果(非递归):0 1 2 3 4

说明:这里对邻接节点逆序遍历(rbegin()和rend()),是因为栈是"先进后出"的。如果直接正序压入,遍历顺序会变成0→3→4→1→2,虽然也是DFS,但与递归实现的顺序不一致(不影响遍历完整性,仅影响顺序)。

案例2:二叉树的前序遍历(非递归DFS)

cpp 复制代码
#include <iostream>
#include <stack>
using namespace std;

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// 非递归实现前序遍历(DFS)
void preorderDFSNonRecursive(TreeNode* root) {
    if (root == nullptr) {
        return;
    }
    stack<TreeNode*> st;
    // 压入根节点
    st.push(root);
    
    while (!st.empty()) {
        // 弹出栈顶节点,访问
        TreeNode* node = st.top();
        st.pop();
        cout << node->val << " ";
        
        // 注意:先压右子树,再压左子树(栈先进后出,保证左子树先访问)
        if (node->right != nullptr) {
            st.push(node->right);
        }
        if (node->left != nullptr) {
            st.push(node->left);
        }
    }
}

int main() {
    // 构建与递归案例相同的二叉树
    TreeNode* root = new TreeNode(1);
    root->right = new TreeNode(2);
    root->right->left = new TreeNode(3);
    
    cout << "二叉树前序遍历(非递归DFS):";
    preorderDFSNonRecursive(root);
    cout << endl;
    
    // 释放内存
    delete root->right->left;
    delete root->right;
    delete root;
    
    return 0;
}
    

输出结果:

二叉树前序遍历(非递归DFS):1 2 3

四、DFS的经典应用:回溯法求解N皇后问题

DFS的核心是"探索-回溯",而回溯法是DFS的一种延伸应用,常用于解决"选择-验证-回溯-再选择"的组合优化问题。N皇后问题是回溯法的经典案例:在n×n的棋盘上放置n个皇后,使得任意两个皇后不处于同一行、同一列或同一斜线上。

cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
using namespace std;

vector<vector<string>> result;  // 存储所有合法解

// 检查当前位置(row, col)是否可以放置皇后
bool isValid(int n, int row, int col, vector<string>& board) {
    // 检查同一列是否有皇后
    for (int i = 0; i < row; ++i) {
        if (board[i][col] == 'Q') {
            return false;
        }
    }
    // 检查左上到右下的斜线(左上方向)
    for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; --i, --j) {
        if (board[i][j] == 'Q') {
            return false;
        }
    }
    // 检查右上到左下的斜线(右上方向)
    for (int i = row - 1, j = col + 1; i >= 0 && j < n; --i, ++j) {
        if (board[i][j] == 'Q') {
            return false;
        }
    }
    return true;
}

// DFS回溯函数:当前处理第row行
void backtrack(int n, int row, vector<string>& board) {
    // 递归终止条件:所有行都处理完毕(找到一个合法解)
    if (row == n) {
        result.push_back(board);
        return;
    }
    // 遍历当前行的每一列,尝试放置皇后
    for (int col = 0; col < n; ++col) {
        if (isValid(n, row, col, board)) {
            // 选择:在当前位置放置皇后
            board[row][col] = 'Q';
            // 探索:处理下一行
            backtrack(n, row + 1, board);
            // 回溯:撤销选择,恢复原状
            board[row][col] = '.';
        }
    }
}

// 求解N皇后问题
vector<vector<string>> solveNQueens(int n) {
    result.clear();
    // 初始化棋盘:n行n列,全部为'.'
    vector<string> board(n, string(n, '.'));
    backtrack(n, 0, board);
    return result;
}

// 打印所有解
void printResult(const vector<vector<string>&> result) {
    for (auto& solution : result) {
        cout << "=====================" << endl;
        for (auto& row : solution) {
            cout << row << endl;
        }
    }
}

int main() {
    int n = 4;  // 求解4皇后问题
    vector<vector<string>> solutions = solveNQueens(n);
    cout << n << "皇后问题共有 " << solutions.size() << " 个解:" << endl;
    printResult(solutions);
    return 0;
}
    

输出结果(4皇后问题的2个解):

4皇后问题共有 2 个解:

.Q...

...Q
Q...
...Q.

...Q.

Q...

...Q

.Q...

说明:该代码通过DFS回溯,逐行尝试放置皇后,每放置一个皇后就验证合法性,若合法则继续探索下一行,若不合法则回溯到上一行重新选择列。最终遍历出所有合法的放置方案。

五、DFS的常见注意事项

  • 避免重复访问:必须使用visited数组(或其他标记方式)标记已访问的节点,否则会陷入无限循环(尤其是图中有环的情况);

  • 递归深度控制:递归实现时,若问题规模过大(如n=1e4),会导致栈溢出,此时需改用非递归实现;

  • 回溯的撤销操作:回溯法中,每次选择后必须撤销选择(如N皇后问题中恢复board[row][col]为'.'),否则会影响后续探索;

  • 邻接表的构建:图的DFS遍历中,邻接表比邻接矩阵更高效(空间复杂度O(V+E) vs O(V²)),适合稀疏图;

  • 多连通分量处理:若图不连通,需遍历所有节点,对每个未访问的节点调用DFS(如:for(int i=0; i<n; i++) if(!visited[i]) dfs(i);)。

六、总结

DFS是一种基于"深度优先、回溯探索"的遍历算法,核心依赖栈(递归栈或手动栈)实现。其代码简洁、思想直观,广泛应用于图遍历、树遍历、排列组合、回溯优化等问题。

在C++实现中,递归版本适合小规模问题,代码易写;非递归版本适合大规模问题,避免栈溢出。学习DFS的关键是理解"探索-回溯"的思想,以及如何通过标记和撤销操作控制遍历过程。

相关推荐
石像鬼₧魂石2 小时前
SQLMap 注入完整操作流程(新手通关手册)
linux·人工智能·windows·学习
victory04312 小时前
SFT RLHF PPO DPO GRPO学习规划
学习
郝学胜-神的一滴2 小时前
Linux线程错误调试指南:从原理到实践
linux·服务器·开发语言·c++·程序人生
小徐不会敲代码~2 小时前
Vue3 学习 5
前端·学习·vue
iconball2 小时前
个人用云计算学习笔记 --32 Docker和docker swarm
运维·笔记·学习·docker·容器·云计算
秋深枫叶红2 小时前
嵌入式第四十篇——网络编程
linux·网络·学习
星马梦缘2 小时前
计算机组成原理 2024级焚决
计算机·cache·计算机组成原理·组合逻辑
KingRumn2 小时前
玩转DBus命令行工具之gdbus使用
linux·算法
二狗哈2 小时前
Cesium快速入门34:3dTile高级样式设置
前端·javascript·算法·3d·webgl·cesium·地图可视化