首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【算法】递归算法的深度实践:深度优先搜索(DFS)从原理到LeetCode实战

【算法】递归算法的深度实践:深度优先搜索(DFS)从原理到LeetCode实战

作者头像
蒙奇D索隆
发布2025-11-29 13:31:14
发布2025-11-29 13:31:14
7570
举报

导读

大家好,很高兴又和大家见面啦!!! 在前面的内容中,我们共同探索了汉诺塔的奥秘,体验了快速幂算法的高效,感受到了递归思维解决复杂问题的独特魅力。今天,我们将沿着递归这条主线继续前行,探索它在数据结构中的一个重要应用场景。 递归不仅仅是一种编程技巧,更是一种解决问题的思维方式。当我们掌握了递归的基本原理后,很自然地会想知道:这个强大的工具在树、图这样的复杂数据结构中能发挥怎样的作用? 这正是深度优先搜索DFS)要带给我们的答案——它完美展现了递归思维如何优雅地解决数据结构中的遍历与搜索问题。从基本概念的厘清到实际问题的解决,DFS都体现了递归思想的精髓。 现在,就让我们开始这次探索之旅,看看递归思维如何在数据结构中绽放光彩

一、基本概念

1.1 遍历

  • 基本定义:遍历​ (Traversal)是一种算法过程,指的是按照某种规则,系统地访问数据结构(如数组、链表、树、图)中的每一个节点(元素)一次且仅一次的过程。
  • 目的:其核心目的是“访问全部”,确保不重不漏。访问时执行什么操作(如打印、修改)是另一个层面的问题。
  • 基础性:遍历是更高级操作(如搜索、计算路径)的基础

1.2 深度优先遍历

  • 基本定义:深度优先遍历Depth-First Traversal, DFT)是遍历树或图的一种策略。
    • 核心思想尽可能深地探索分支路径,当一条路径走到尽头(即遇到叶子节点或已访问过的节点)时,再回溯到上一个分叉点,选择另一条未探索的路径继续深入
  • 核心要点:
    • 顺序:遵循“后进先出LIFO)”的原则,通常使用 这种数据结构(显式使用或通过 递归 调用隐式使用)来实现。
    • 结果:深度优先遍历会产生特定的序列
      • 在树结构中,有前序、中序、后序遍历。
      • 在图中,根据选择的路径不同,其 DFT 的遍历序列也有所不同

1.3 广度优先遍历

  • 基本定义:广度优先遍历Breadth-First Traversal, BFT)是遍历树或图的另一种策略。
    • 核心思想从起点开始,先访问所有相邻的节点,然后再访问这些相邻节点的相邻节点,以此类推,一层一层地向外扩展
  • 核心要点: 顺序:遵循“先进先出(FIFO)”的原则,必须使用 队列 这种数据结构来实现。 特性:它保证在访问第 n+1 层的节点之前,第 n 层的所有节点都已被访问。因此,它常被用于寻找最短路径(在边权为1的图中)。

1.4 搜索

  • 基本定义:搜索(** Search**)是一种算法过程,其目标是在一个数据集合或问题空间中,寻找一个满足特定条件的目标元素(解)。
  • 核心要点:
    • 目的性强:搜索 是目标导向的,一旦找到目标,过程就可以终止
    • 与遍历的关系:搜索通常是基于某种遍历策略实现。你可以把搜索理解为一种“带有提前终止条件的遍历”。

1.5 暴力搜索

  • 基本定义:暴力搜索(** Brute-Force Search**)又称穷举搜索,是一种最简单直接的搜索策略。它系统地枚举所有可能的候选解,并逐一检查其是否满足问题的条件
  • 核心要点:
    • 全面性:只要时间允许,它保证能找到解(如果解存在的话)。
    • 低效性:其时间复杂度通常非常高,是指数级的,因此只适用于问题规模很小的情况
    • 与遍历的关系:暴力搜索通常意味着对整个问题空间进行一种“无差别”的遍历。

1.6 深度优先搜索

  • 基本定义:深度优先搜索Depth-First Search,DFS)是一种利用深度优先遍历策略来实现的搜索算法
    • 搜索过程它在搜索时,会沿着一条路径尽可能深地探索,直到找到目标或走到尽头,然后回溯
  • 核心要点:
    • 继承了深度优先遍历的所有特性(使用 栈/递归 )。
    • 常用于需要搜索整个解空间的问题,如拓扑排序、检测图中环、解决迷宫问题等。
  • 与深度优先遍历的区别:
    • 深度优先遍历强调过程,目的是访问所有节点。
    • 深度优先搜索强调目标,目的是找到特定节点或解,它可能在过程中途就提前结束。

1.7 广度优先搜索

  • 基本定义:广度优先搜索Breadth-First Search, BFS)是一种利用广度优先遍历策略来实现的搜索算法。
    • 搜索过程它在搜索时,会从起点开始,按距离起点的层次由近及远地进行探索
  • 核心要点:
    • 继承了广度优先遍历的所有特性(使用 队列 )。
    • 当图中的边没有权重(或权重相等)时,BFS 找到的路径一定是最短路径。因此它被广泛应用于最短路径问题、社交网络中查找最短关系链等
  • 与广度优先遍历的区别:
    • 广度优先遍历强调过程,目的是访问所有节点。
    • 广度优先搜索强调目标,目的是找到特定节点或解,它可能在过程中途就提前结束。

1.8 总结关系

这些概念可以看作一个清晰的层次结构:

  1. 遍历 VS 搜索:这是最高层的区分。遍历是手段(访问全部),搜索是目的(寻找特定目标)。搜索通常通过遍历来实现。
  2. 两种策略:深度优先和广度优先是两种核心的策略,它们既可以用于“遍历”,也可以用于“搜索”。
  3. 具体实现
    • 深度优先策略 + 遍历目的 = 深度优先遍历
    • 深度优先策略 + 搜索目的 = 深度优先搜索
    • 广度优先策略 + 遍历目的 = 广度优先遍历
    • 广度优先策略 + 搜索目的 = 广度优先搜索
  4. 暴力搜索是一个更上位的概念,它描述了“枚举所有可能”的思想,而 DFSBFS 是实现在图等结构上暴力搜索的两种具体、系统化的方法。

二、具体实现

深度优先搜索 就是递归算法在 数据结构 中的实际应用,常被用于 这两种数据结构中。这里我们以 为例来说明该算法的具体实现;

2.1 树中的深度优先搜索

深度优先搜索 的特性是 LIFO,该特性与树中的中序遍历一致,也就是说当我们在对树进行带有特定条件的中序遍历时,我们就是在对该棵树进行 深度优先搜索。 通常我们会以该算法的缩写——DFS 来表示该算法,因此在接下来的实现中,该算法的具体实现对应的函数名我们选择使用 DFS 或者 dfs

2.2 函数定义

函数的定义主要是定义函数的三要素——函数名、函数参数、函数的返回类型:

  • 函数名——当前我们实现的函数功能为:深度优先搜索,因此函数名我们选择 dfs
  • 函数参数——该还是主要用于对 进行 深度优先搜索,因此函数参数有两个:
    • root :树的根节点
    • x :需要在树中查找的特定值
  • 函数的返回类型——该函数是用于查找特定值是否存在于树中,因此函数的放回类型我们有多种选择:
    • bool —— 只用于简单的查找该值是否存在于树中
      • 存在则返回 true
      • 不存在则返回 false
    • TreeNodeType* —— 用于找到该值的具体位置
      • 存在则返回该值所在结点的地址
      • 不存在则返回 NULL

2.3 递归基

本身就是一种 递归型 的数据结构,因此在对该数据结构进行各种操作时,均可通过 递归 实现。 在 中,各种操作是否能够执行,取决于当前访问的树是否为空树,因此在 中,其递归基可以直接通过判断该树是否为空树

代码语言:javascript
复制
	if (root == NULL) {
		return NULL;
	}

2.4 递进关系

作为一种 递归型 的数据结构,其递进关系就是该棵树的子树,这里我们以 二叉树 为例:

代码语言:javascript
复制
	BTNode* p = dfs(root->lchild, x);
	if (p == NULL) {
		p = dfs(root->rchild, x);
	}
	return p;

2.5 组合优化

接下来我们将上述内容组合在一起,就得到了 二叉树 中的 dfs

代码语言:javascript
复制
BTNode* dfs(BTree root, ElemType x) {
	if (root == NULL) {
		return NULL;
	}
	BTNode* p = dfs(root->lchild, x);
	if (p == NULL) {
		p = dfs(root->rchild, x);
	}
	return p;
}

三、230. 二叉搜索树中第 K 小的元素

3.1 题目介绍

题目标签:树、深度优先搜索、二叉树、二叉搜索树 题目难度:中等 题目描述: 给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(从 1 开始计数)。

示例 1: 输入:root = [3, 1, 4, null, 2], k = 1 输出:1

示例 2: 输入:root = [5, 3, 6, 2, 4, null, null, 1], k = 3 输出:3

提示

树中的节点数为 n1 \leq k \leq n \leq 10^4 0 \leq Node.val \leq 10^4

进阶:如果二叉搜索树经常被修改(插入 / 删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?

3.2 解题思路

常规的思路我们可以通过一个数组,将树中的元素从小到大依次进行存储,之后再返回下标为 k -1 的元素即可; 在该思路中,数组的大小取决于树中的结点数量,而该数量我们可以通过中序遍历的方式获取:

代码语言:javascript
复制
int Get_Size(TN* root) {
	if (root == NULL) {
		return 0;
	}
	int left = Get_KSize(root->left);
	int right = Get_KSize(root->right);
	return left + right + 1;
}

但是该思路在经常被修改的二叉搜索树中,则不再适用,因为当二叉树被修改时,其对应的数组大小也将被频繁修改,所以会大大降低算法的效率; 这题我们将会采取一种更优的算法思路——直接通过 dfs 实现查找; 在 BST 中,树中的各结点满足 左子树 < 根节点 < 右子树

代码语言:javascript
复制
flowchart TB
a((20))
b((10))
c((30))
a--->b
a--->c

d((6))
e((17))
b--->d
b--->e

f((4))
g((7))
d--->f
d--->g

h((13))
i((19))
e--->h
e--->i

我们需要找到该 BST 中第 k 个最小的数,这是我们需要通过 dfs 从左子树中开始查找; 通过 dfs 我们找到的该树中最小值为树中的最左侧结点 4 ,此时我们可以开始进行计数;

代码语言:javascript
复制
flowchart TB
a((20<br>count = 8))
b((10<br>count = 4))
c((30<br>count = 9))
a--->b
a--->c

d((6<br>count = 2))
e((17<br>count = 6))
b--->d
b--->e

f((4<br>count = 1))
g((7<br>count = 3))
d--->f
d--->g

h((13<br>count = 5))
i((19<br>count = 7))
e--->h
e--->i

若我们只是对该 BST 进行遍历操作,那么每个结点对应的计数如上图所示。可以看到,每个结点对应的计数值,正是该结点按由小到大顺序排列时的位置; 因此我们的思路就是对该二叉树进行一次 dfs ,找到 count == k 的结点,该结点即为我们需要获取的第 k 小的元素。

3.3 代码编写

3.3.1 函数定义

该思路的实现是对该 BST 进行一次 dfs ,因此我们直接以 DFS 作为其函数名; 函数的参数有三个:

  • root —— 遍历树的根结点
  • k —— 计数变量
  • ans —— 返回值变量

该函数只需要对 BST 进行一次 dfs ,因此函数不需要任何返回值,即,该函数的返回值为 void

3.3.2 递归基

在该算法中,控制算法的因素有两个:

  • root —— 当树为空树时,结束遍历
  • k —— 当找到目标 k 时,结束遍历

这里需要注意的是,若我们直接使用题目所给的变量 k ,那么则可以通过 k 的自减,来对各个结点进行编号,此时,我们需要寻找的目标值就变成了 k == 0 的值; 这里我们还是以上面介绍的 BST 为例,当 k == 6 时,那么树中的各个结点对应的编号为:

代码语言:javascript
复制
flowchart TB
a((20<br>count = -2))
b((10<br>count = 2))
c((30<br>count = -3))
a--->b
a--->c

d((6<br>count = 4))
e((17<br>count = 0))
b--->d
b--->e

f((4<br>count = 5))
g((7<br>count = 3))
d--->f
d--->g

h((13<br>count = 1))
i((19<br>count = -1))
e--->h
e--->i

每完成一次结点的访问,就需要对 k 值进行一次自减操作,因此,每个结点对应的 k 值与上图中展示的 count 值一致,结点的对应编号会从 k - 1 开始进行递减编号; 所以当找到编号为 0 的结点是,该节点中存储的值就是我们需要寻找的目标值; 因此 k == 0 也是控制该算法结束的一个条件:

代码语言:javascript
复制
	if (root == NULL || k == 0) {
		return;
	}
3.3.3 递进关系

在 二叉树 中,其 dfs 实质上就是对该树进行 中序遍历,而 中序遍历 的顺序为:左子树 \rightarrow 根结点 \rightarrow 右子树,即:

代码语言:javascript
复制
	DFS(root->left, k, ans);
	*k -= 1;
	if (*k == 0) {
		*ans = root->val;
	}
	DFS(root->right, k, ans);

在对根结点的访问中,我们只需要对该结点进行一次编号即可,而这里对 k 值的判断,实际上做的就是记录 k == 0 时,结点中存储的数值。 当然我们也可以将访问根结点的操作给封装成函数 visited ,这里我就不进行展开了,感兴趣的朋友可以关注一下 【数据结构】专栏,其中介绍 二叉树 的遍历操作时,有进行详细介绍。

3.3.4 组合优化

接下来我们只需要将前面的内容进行整合即可:

代码语言:javascript
复制
void DFS(TN* root, int* k, int* ans) {
	if (root == NULL || k == 0) {
		return;
	}
	DFS(root->left, k, ans);
	*k -= 1;
	if (*k == 0) {
		*ans = root->val;
	}
	DFS(root->right, k, ans);
}
int kthSmallest(struct TreeNode* root, int k) {
	int ans = 0;
	DFS(root, &k, &ans);
	return ans;
}

这里需要注意,因为我们需要改变 kans 的值,因此我们需要在传参时,传入这两个变量的地址,并且在 DFS 中,通过指针来修改其值;

3.4 代码测试

下面我们就在 leetcode 中对该算法进行测试:

可以看到,此时我们很好的通过 DFS 算法解决了该问题;

结语

通过本文的系统学习,我们共同完成了对深度优先搜索DFS)算法的完整探索。从理论概念到代码实现,再到实战应用,相信您已经对 DFS 建立了全面的认识。 核心要点回顾

  • 概念层面:清晰区分了遍历与搜索的本质差异,明确了 DFSBFS 的适用场景
  • 实现层面:深入理解了 DFS 的递归实现机制,掌握了在树结构中的具体应用
  • 实战层面:通过LeetCode 230题的分析,展示了 DFS 解决实际问题的完整流程

知识拓展建议

  • 基础巩固:尝试用 DFS 解决二叉树的其他问题,如路径总和、最大深度计算等
  • 进阶提升:探索 DFS 在图结构中的应用,如连通分量识别、拓扑排序等
  • 优化思考:研究剪枝策略在 DFS 中的应用,提升算法效率

DFS 不仅是重要的算法工具,更体现了"深度探索,回溯选择"的思维方式。掌握 DFS 将为学习更复杂的算法奠定坚实基础。 互动与分享

  • 点赞👍 - 您的认可是我持续创作的最大动力
  • 收藏⭐ - 方便随时回顾这些重要的基础概念
  • 转发↗️ - 分享给更多可能需要的朋友
  • 评论💬 - 欢迎留下您的宝贵意见或想讨论的话题

感谢您的耐心阅读! 关注博主,不错过更多技术干货。我们下一篇再见!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 导读
  • 一、基本概念
    • 1.1 遍历
    • 1.2 深度优先遍历
    • 1.3 广度优先遍历
    • 1.4 搜索
    • 1.5 暴力搜索
    • 1.6 深度优先搜索
    • 1.7 广度优先搜索
    • 1.8 总结关系
  • 二、具体实现
    • 2.1 树中的深度优先搜索
    • 2.2 函数定义
    • 2.3 递归基
    • 2.4 递进关系
    • 2.5 组合优化
  • 三、230. 二叉搜索树中第 K 小的元素
    • 3.1 题目介绍
    • 3.2 解题思路
    • 3.3 代码编写
      • 3.3.1 函数定义
      • 3.3.2 递归基
      • 3.3.3 递进关系
      • 3.3.4 组合优化
    • 3.4 代码测试
  • 结语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档