1. 问题分析与解题思路
今天我们来解决LeetCode上的一道经典二叉树问题——863. 二叉树中所有距离为K的结点。这道题看似简单,但实际考察了我们对二叉树遍历、图论转换和广度优先搜索的综合运用能力。
题目给定一个二叉树(根节点root)、一个目标节点target和一个整数k,要求返回所有与target节点距离为k的节点列表。这里的距离指的是两个节点之间的最短路径上的边数。
1.1 常规思路的局限性
初看这道题,很多人的第一反应可能是直接从target节点出发进行BFS(广度优先搜索),遍历k层后收集结果。然而,这种方法存在一个致命缺陷:二叉树是有向的,我们只能从父节点访问子节点,而无法从子节点回溯到父节点。这意味着如果目标节点位于树的中部,我们无法通过简单的BFS访问到它的祖先节点。
1.2 图论转换的巧妙解法
为了解决这个问题,我们需要将二叉树转换为无向图。在无向图中,每个节点都可以自由地访问它的邻居节点,无论是子节点还是父节点。具体来说:
- 首先通过深度优先搜索(DFS)遍历整棵树
- 在遍历过程中,为每个节点建立邻接表,记录其左右子节点
- 同时,我们也要让子节点知道它们的父节点是谁
- 这样,我们就将二叉树转换成了一个无向图
这种转换的优点是:
- 保留了原始二叉树的所有连接关系
- 添加了从子节点到父节点的反向连接
- 使得我们可以从任意节点出发,自由地探索所有相邻节点
2. 代码实现详解
让我们仔细分析提供的C++解决方案,理解每个部分的实现细节。
2.1 数据结构准备
cpp复制vector<vector<int>> tr;
vector<bool> status;
这里定义了两个重要的数据结构:
tr:邻接表,用于存储转换后的无向图。tr[i]表示节点i的所有相邻节点。status:访问标记数组,记录哪些节点已经被访问过,防止重复访问。
2.2 构建邻接表的DFS实现
cpp复制void dfs(TreeNode* root) {
if(root == nullptr) return;
dfs(root->left);
dfs(root->right);
if(root->left != nullptr) {
tr[root->val].push_back(root->left->val);
tr[root->left->val].push_back(root->val);
}
if(root->right != nullptr) {
tr[root->val].push_back(root->right->val);
tr[root->right->val].push_back(root->val);
}
}
这个DFS函数完成了以下工作:
- 递归遍历左子树和右子树
- 对于每个非空左子节点,建立双向连接(父↔左子)
- 对于每个非空右子节点,建立双向连接(父↔右子)
注意:这里假设节点值都是唯一的,且不超过600(从后面的初始化可以看出)。在实际应用中,可能需要更通用的处理方式。
2.3 递归搜索距离为K的节点
cpp复制vector<int> ret;
void recursion(int start, int k) {
if(k == 0) {
ret.push_back(start);
return;
}
status[start] = true;
int next;
for(int i = 0; i < tr[start].size(); i++) {
next = tr[start][i];
if(status[next] == false) {
recursion(next, k-1);
}
}
}
这个递归函数实现了深度优先的K距离搜索:
- 当k减到0时,表示已经找到距离为K的节点,将其加入结果列表
- 标记当前节点为已访问
- 遍历所有相邻节点,对未访问的节点递归搜索(k-1)
2.4 主函数整合
cpp复制vector<int> distanceK(TreeNode* root, TreeNode* target, int k) {
tr.resize(600);
status.assign(600, false);
dfs(root);
recursion(target->val, k);
return ret;
}
主函数完成了以下步骤:
- 初始化邻接表和访问标记数组(大小设为600)
- 调用DFS构建邻接表
- 从target节点开始递归搜索
- 返回结果列表
3. 算法优化与改进
虽然上述解决方案能够正确解决问题,但仍有改进空间。让我们探讨几个优化方向。
3.1 空间复杂度优化
当前实现使用了固定大小的数组(600),这在以下情况下会有问题:
- 节点值超过600
- 节点值为负数
- 节点值不连续导致空间浪费
改进方案:
cpp复制unordered_map<int, vector<int>> tr;
unordered_map<int, bool> status;
使用哈希表可以动态适应各种节点值,更节省空间。
3.2 迭代式BFS实现
递归实现虽然简洁,但在极端情况下可能导致栈溢出。我们可以改用迭代式的BFS:
cpp复制vector<int> distanceKBFS(TreeNode* root, TreeNode* target, int k) {
// 构建邻接表(同前)
unordered_map<int, vector<int>> graph;
buildGraph(root, graph);
queue<pair<int, int>> q; // {node, distance}
unordered_set<int> visited;
vector<int> result;
q.push({target->val, 0});
visited.insert(target->val);
while (!q.empty()) {
auto [node, dist] = q.front();
q.pop();
if (dist == k) {
result.push_back(node);
continue;
}
for (int neighbor : graph[node]) {
if (visited.find(neighbor) == visited.end()) {
visited.insert(neighbor);
q.push({neighbor, dist + 1});
}
}
}
return result;
}
BFS的优势:
- 避免递归深度过大
- 更符合"层级遍历"的直观理解
- 当k较大时性能更稳定
3.3 父节点指针的替代方案
如果不希望显式构建邻接表,可以在遍历时记录父节点信息:
cpp复制unordered_map<TreeNode*, TreeNode*> parent;
void dfs(TreeNode* node, TreeNode* p) {
if (!node) return;
parent[node] = p;
dfs(node->left, node);
dfs(node->right, node);
}
然后在BFS中,可以从target出发,向三个方向探索(左、右、父)。
4. 复杂度分析与比较
让我们分析不同实现方式的时间和空间复杂度。
4.1 原始DFS递归方案
- 时间复杂度:O(N)
- 构建邻接表:O(N),每个节点访问一次
- 递归搜索:最坏O(N),每个节点最多访问一次
- 空间复杂度:O(N)
- 邻接表存储:O(N)
- 递归栈深度:最坏O(N)(退化成链表)
4.2 BFS迭代方案
- 时间复杂度:O(N)
- 与DFS相同,都是线性复杂度
- 空间复杂度:O(N)
- 队列和访问集合的空间需求
4.3 实际性能考量
在实际运行时:
- BFS通常比DFS有更稳定的性能
- 对于k较小的情况,BFS可能提前终止,效率更高
- DFS的递归实现代码更简洁,但可能有栈溢出风险
5. 常见问题与调试技巧
在实现这类算法时,经常会遇到一些典型问题。下面分享一些调试经验。
5.1 无限循环问题
当忘记标记已访问节点时,算法可能会在两个节点间来回跳转,导致无限循环。解决方法:
- 确保在访问节点后立即标记
- 在递归返回前不需要取消标记(与回溯问题不同)
5.2 节点值假设问题
原始代码假设:
- 节点值都是非负整数
- 节点值小于600
- 节点值唯一
如果这些假设不成立,解决方案:
- 使用哈希表代替数组
- 考虑节点地址而非节点值作为标识
5.3 边界条件测试
必须测试的特殊情况:
- 空树
- target就是root且k=0
- k大于树的高度
- 所有节点都在一侧的退化树
5.4 调试输出技巧
在调试时,可以添加临时输出:
cpp复制void printGraph() {
for (int i = 0; i < tr.size(); i++) {
if (!tr[i].empty()) {
cout << i << ": ";
for (int v : tr[i]) cout << v << " ";
cout << endl;
}
}
}
打印邻接表可以验证图转换是否正确。
6. 扩展思考与应用
这道题的解法可以扩展到更一般的图问题。理解这种转换思想对解决其他问题很有帮助。
6.1 类似问题
- 二叉树中两个节点的最近公共祖先(LCA)
- 二叉树中所有距离为K的叶子节点
- 在二叉树中找最长路径
6.2 实际应用场景
- 社交网络中查找特定距离的好友
- 计算机网络中的跳数限制查询
- 组织结构图中查找特定层级的关系
6.3 进一步挑战
尝试解决以下变种问题:
- 加权二叉树(边有权重)中的K距离节点
- 当存在多个target节点时如何高效查询
- 预处理树以支持多次K距离查询
在实际面试中,面试官可能会要求你逐步优化初始解法,因此理解每个步骤的优缺点非常重要。我建议在解决这类问题时,先从最直观的方法开始,然后逐步优化,同时清楚地表达你的思考过程。