
大家好,很高兴又和大家见面啦!!! 在前面的内容中,我们共同探索了汉诺塔的奥秘,体验了快速幂算法的高效,感受到了递归思维解决复杂问题的独特魅力。今天,我们将沿着递归这条主线继续前行,探索它在数据结构中的一个重要应用场景。 递归不仅仅是一种编程技巧,更是一种解决问题的思维方式。当我们掌握了递归的基本原理后,很自然地会想知道:这个强大的工具在树、图这样的复杂数据结构中能发挥怎样的作用? 这正是深度优先搜索(DFS)要带给我们的答案——它完美展现了递归思维如何优雅地解决数据结构中的遍历与搜索问题。从基本概念的厘清到实际问题的解决,DFS都体现了递归思想的精髓。 现在,就让我们开始这次探索之旅,看看递归思维如何在数据结构中绽放光彩。
这些概念可以看作一个清晰的层次结构:
深度优先搜索 就是递归算法在 数据结构 中的实际应用,常被用于 树 与 图 这两种数据结构中。这里我们以 树 为例来说明该算法的具体实现;
深度优先搜索 的特性是 LIFO,该特性与树中的中序遍历一致,也就是说当我们在对树进行带有特定条件的中序遍历时,我们就是在对该棵树进行 深度优先搜索。 通常我们会以该算法的缩写——DFS 来表示该算法,因此在接下来的实现中,该算法的具体实现对应的函数名我们选择使用 DFS 或者 dfs
函数的定义主要是定义函数的三要素——函数名、函数参数、函数的返回类型:
dfs;
root :树的根节点x :需要在树中查找的特定值bool —— 只用于简单的查找该值是否存在于树中 truefalseTreeNodeType* —— 用于找到该值的具体位置 NULL树 本身就是一种 递归型 的数据结构,因此在对该数据结构进行各种操作时,均可通过 递归 实现。 在 树 中,各种操作是否能够执行,取决于当前访问的树是否为空树,因此在 树 中,其递归基可以直接通过判断该树是否为空树:
if (root == NULL) {
return NULL;
}树 作为一种 递归型 的数据结构,其递进关系就是该棵树的子树,这里我们以 二叉树 为例:
BTNode* p = dfs(root->lchild, x);
if (p == NULL) {
p = dfs(root->rchild, x);
}
return p;接下来我们将上述内容组合在一起,就得到了 二叉树 中的 dfs:
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;
}题目标签:树、深度优先搜索、二叉树、二叉搜索树 题目难度:中等 题目描述: 给定一个二叉搜索树的根节点 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
提示:
树中的节点数为 n 。 1 \leq k \leq n \leq 10^4 0 \leq Node.val \leq 10^4
进阶:如果二叉搜索树经常被修改(插入 / 删除操作)并且你需要频繁地查找第 k 小的值,你将如何优化算法?
常规的思路我们可以通过一个数组,将树中的元素从小到大依次进行存储,之后再返回下标为 k -1 的元素即可; 在该思路中,数组的大小取决于树中的结点数量,而该数量我们可以通过中序遍历的方式获取:
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 中,树中的各结点满足 左子树 < 根节点 < 右子树
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 ,此时我们可以开始进行计数;
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 小的元素。
该思路的实现是对该 BST 进行一次 dfs ,因此我们直接以 DFS 作为其函数名; 函数的参数有三个:
root —— 遍历树的根结点k —— 计数变量ans —— 返回值变量该函数只需要对 BST 进行一次 dfs ,因此函数不需要任何返回值,即,该函数的返回值为 void
在该算法中,控制算法的因素有两个:
root —— 当树为空树时,结束遍历k —— 当找到目标 k 时,结束遍历这里需要注意的是,若我们直接使用题目所给的变量 k ,那么则可以通过 k 的自减,来对各个结点进行编号,此时,我们需要寻找的目标值就变成了 k == 0 的值; 这里我们还是以上面介绍的 BST 为例,当 k == 6 时,那么树中的各个结点对应的编号为:
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 也是控制该算法结束的一个条件:
if (root == NULL || k == 0) {
return;
}在 二叉树 中,其 dfs 实质上就是对该树进行 中序遍历,而 中序遍历 的顺序为:左子树 \rightarrow 根结点 \rightarrow 右子树,即:
DFS(root->left, k, ans);
*k -= 1;
if (*k == 0) {
*ans = root->val;
}
DFS(root->right, k, ans);在对根结点的访问中,我们只需要对该结点进行一次编号即可,而这里对 k 值的判断,实际上做的就是记录 k == 0 时,结点中存储的数值。 当然我们也可以将访问根结点的操作给封装成函数 visited ,这里我就不进行展开了,感兴趣的朋友可以关注一下 【数据结构】专栏,其中介绍 二叉树 的遍历操作时,有进行详细介绍。
接下来我们只需要将前面的内容进行整合即可:
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;
}这里需要注意,因为我们需要改变 k 与 ans 的值,因此我们需要在传参时,传入这两个变量的地址,并且在 DFS 中,通过指针来修改其值;
下面我们就在 leetcode 中对该算法进行测试:

可以看到,此时我们很好的通过 DFS 算法解决了该问题;
通过本文的系统学习,我们共同完成了对深度优先搜索(DFS)算法的完整探索。从理论概念到代码实现,再到实战应用,相信您已经对 DFS 建立了全面的认识。 核心要点回顾
知识拓展建议
DFS 不仅是重要的算法工具,更体现了"深度探索,回溯选择"的思维方式。掌握 DFS 将为学习更复杂的算法奠定坚实基础。 互动与分享
感谢您的耐心阅读! 关注博主,不错过更多技术干货。我们下一篇再见!