在解决LeetCode 1379题"找出克隆二叉树中相同的节点"时,我们需要理解题目本质:给定两棵完全相同的二叉树(原始树和克隆树),以及原始树中的一个目标节点,要求找到克隆树中对应的相同节点。这个问题的核心在于如何在树结构中精确定位节点。
二叉树是由节点组成的层次结构,每个节点最多有两个子节点(左子树和右子树)。在本题中,克隆树是原始树的完全复制品,意味着两棵树的结构完全一致,对应节点的值也相同,但它们在内存中是不同的对象实例。
关键理解点:虽然两棵树的节点对象不同(内存地址不同),但它们的结构关系和节点值是相同的。这就是我们能够通过递归遍历找到对应节点的理论基础。
递归是解决树形结构问题的天然工具。对于本题,递归的基本思路是:
cpp复制TreeNode* getTargetCopy(TreeNode* original, TreeNode* cloned, TreeNode* target) {
// 基本情况1:当前节点就是目标
if(original == target) {
return cloned;
}
// 基本情况2:当前节点为空
if(original == NULL) {
return NULL;
}
// 递归检查左子树
TreeNode* leftResult = getTargetCopy(original->left, cloned->left, target);
if(leftResult != NULL) {
return leftResult;
}
// 递归检查右子树
TreeNode* rightResult = getTargetCopy(original->right, cloned->right, target);
if(rightResult != NULL) {
return rightResult;
}
// 都没找到返回NULL
return NULL;
}
递归必须要有明确的终止条件,否则会导致无限递归。本题中有两个明确的终止条件:
在二叉树遍历中,常见的递归顺序有前序、中序和后序。本题采用的是前序遍历(根-左-右)的方式:
这种顺序在查找特定节点时效率较高,因为一旦找到目标就可以立即返回,不需要遍历整棵树。
对于递归感到困难是很正常的,因为它与我们日常的线性思维方式不同。培养递归思维可以尝试以下方法:
对于二叉树问题,可以总结出一个通用的递归模板:
cpp复制ReturnType traversal(TreeNode* root, ...其他参数...) {
// 1. 处理基本情况(空节点或满足特定条件)
if(root == NULL) {
return ...;
}
if(满足特定条件) {
return ...;
}
// 2. 递归处理左子树
LeftType leftResult = traversal(root->left, ...);
// 3. 递归处理右子树
RightType rightResult = traversal(root->right, ...);
// 4. 合并左右子树结果
return combine(leftResult, rightResult);
}
对于本题,这个模板的具体实现是:
原始代码可以进行一些优化,使其更简洁:
cpp复制TreeNode* getTargetCopy(TreeNode* original, TreeNode* cloned, TreeNode* target) {
if(!original) return NULL;
if(original == target) return cloned;
TreeNode* left = getTargetCopy(original->left, cloned->left, target);
if(left) return left;
return getTargetCopy(original->right, cloned->right, target);
}
优化点:
在编写递归算法时,必须考虑各种边界条件:
虽然递归是更自然的解法,但了解迭代解法也有助于深入理解问题。可以使用栈来实现深度优先搜索:
cpp复制TreeNode* getTargetCopyIterative(TreeNode* original, TreeNode* cloned, TreeNode* target) {
stack<pair<TreeNode*, TreeNode*>> stk;
stk.push({original, cloned});
while(!stk.empty()) {
auto [orig, clone] = stk.top();
stk.pop();
if(orig == target) {
return clone;
}
if(orig->right) {
stk.push({orig->right, clone->right});
}
if(orig->left) {
stk.push({orig->left, clone->left});
}
}
return NULL;
}
递归的优点:
递归的缺点:
迭代的优点:
迭代的缺点:
在实际面试中,通常可以先给出递归解法,然后根据面试官要求讨论迭代实现。
最坏情况下,我们需要遍历整棵树才能找到目标节点(或者确认目标不存在),因此时间复杂度是O(N),其中N是树中节点数量。
递归解法:
迭代解法:
如果题目允许预处理树结构,可以考虑:
但这些方法需要额外的空间,且不符合本题的限制条件(不能修改树结构)。
递归代码调试可能比较困难,可以采用以下技巧:
cpp复制void debugPrint(int depth, string msg) {
cout << string(depth*2, ' ') << msg << endl;
}
TreeNode* getTargetCopy(TreeNode* original, TreeNode* cloned, TreeNode* target, int depth=0) {
debugPrint(depth, "Enter: " + to_string(original? original->val : 0));
// ...函数体...
}
递归不仅适用于树形结构,还可以解决:
掌握递归思维是算法学习的重要里程碑,需要大量练习来培养这种思维方式。