如何在任意二叉树中找到两个节点的最低公共祖先?
如何在任意二叉树中找到两个节点的最低公共祖先?
这里的二叉树不一定是二叉搜索树。\n可以将结构定义如下:\nstruct node {\n int data;\n struct node *left;\n struct node *right;\n};\n我和一个朋友一起找到的最佳解决方案是这样的:\n考虑这棵二叉树:\n这棵二叉树:\n\n中序遍历结果为:8, 4, 9, 2, 5, 1, 6, 3, 7\n后序遍历结果为:8, 9, 4, 5, 2, 6, 7, 3, 1\n例如,如果我们想找到节点8和节点5的共同祖先,我们可以在中序遍历结果中找到8和5之间的所有节点,即[4, 9, 2]。然后我们检查这个列表中最后出现在后序遍历结果中的节点,即2。因此,8和5的共同祖先是2。\n我认为该算法的复杂度为O(n)(中序遍历和后序遍历都是O(n),其他步骤也是O(n),因为它们只是对数组进行简单迭代)。但是很有可能我理解错了。:-)\n但这是一种非常简陋的方法,我不确定它是否在某些情况下会失败。是否有其他(可能更优化的)解决方案?
如何在任意二叉树中找到两个节点的最低共同祖先?
在处理这个问题之前,我们先来了解一下问题的背景和解决方法。
在给出的JAVA代码中,有一个名为LCA的方法,它接受一个二叉树的根节点以及两个要查找最低共同祖先的节点。代码的实现逻辑如下:
1. 首先,检查根节点是否为空。如果为空,直接返回null。
2. 接下来,检查根节点是否是要查找的两个节点之一。如果是,那么根节点就是最低共同祖先,直接返回根节点。
3. 如果根节点不是要查找的节点之一,那么分别在左子树和右子树中递归调用LCA方法,查找两个节点的最低共同祖先。
4. 如果左子树和右子树中都找到了最低共同祖先,那么根节点就是最低共同祖先,直接返回根节点。
5. 如果只有左子树或者右子树中找到了最低共同祖先,那么返回找到的最低共同祖先。
6. 如果左子树和右子树中都没有找到最低共同祖先,那么返回null。
但是,这段代码在某个节点不存在于树中时会出现问题。如果要查找的某个节点不存在于树中,那么代码会返回null,而实际上我们期望返回的是存在的节点。
如果给定的树是一棵二叉搜索树(BST),我们可以对代码进行优化吗?
如果给定的树是一棵二叉搜索树,我们可以利用二叉搜索树的特性来优化代码。在二叉搜索树中,左子树的所有节点都小于根节点,右子树的所有节点都大于根节点。
优化后的代码如下所示:
public static Node LCA(Node root, Node a, Node b) { if (root == null) { return null; } // 如果根节点的值大于a和b的值,那么LCA一定在根节点的左子树中 if (root.value > a.value && root.value > b.value) { return LCA(root.left, a, b); } // 如果根节点的值小于a和b的值,那么LCA一定在根节点的右子树中 if (root.value < a.value && root.value < b.value) { return LCA(root.right, a, b); } // 否则,根节点就是LCA return root; }
这段代码的逻辑如下:
1. 首先,检查根节点是否为空。如果为空,直接返回null。
2. 如果根节点的值大于a和b的值,那么LCA一定在根节点的左子树中,递归调用LCA方法,在左子树中查找LCA。
3. 如果根节点的值小于a和b的值,那么LCA一定在根节点的右子树中,递归调用LCA方法,在右子树中查找LCA。
4. 如果以上两个条件都不满足,那么根节点就是LCA,直接返回根节点。
在这个优化后的代码中,我们利用了二叉搜索树的特性,通过比较节点的值来确定LCA所在的子树,从而减少了不必要的递归调用,提高了代码的效率。
在原始的代码中,有一句注释:"如果根节点是a或b之一,那么它就是LCA",但这并不一定是正确的。在这一点上,我们只知道不需要检查根节点的子节点来找到LCA。这是因为我们可以在后续的步骤中检查根节点的父节点,看看是否在两个分支上都找到了匹配(LCA是父节点),或者只在其中一个分支上找到了匹配(这种情况下,这个分支可能是LCA,或者可能有一个更大的祖先是LCA)。
如何在任意二叉树中找到两个节点的最低公共祖先?
如果没有父节点指针,那么Nick Johnson是正确的,O(n)的时间复杂度算法是你能做的最好的。对于这个算法的简单递归版本,请参考Kinding的帖子中的代码,它的运行时间为O(n)。
但是请记住,如果你的节点有父节点指针,那么可以使用改进的算法。对于要询问的两个节点,构造一个包含从根节点到该节点的路径的列表,从该节点开始,通过不断插入父节点来构造该列表。
所以对于你的例子中的节点8,你会得到(显示步骤): {4}, {2, 4}, {1, 2, 4}
对于你要询问的另一个节点,结果为(未显示步骤): {1, 2}
现在比较你制作的这两个列表,寻找它们第一个不同的元素,或者其中一个列表的最后一个元素,以先到者为准。
这个算法需要O(h)的时间,其中h是树的高度。在最坏的情况下,O(h)等于O(n),但如果树是平衡的,那么只有O(log(n))。它还需要O(h)的空间。可以使用仅使用常数空间的改进版本,代码显示在CEGRD的帖子中。
无论树的构造方式如何,如果这将是你在树上执行多次而不在中间更改的操作,还有其他算法可以使用,这些算法需要O(n)(线性)时间的准备工作,但是查找任何一对只需要O(1)(常数)时间。有关这些算法的参考资料,请参见维基百科上的最低公共祖先问题页面。(原始链接由Jason提供)
如果已经给出了父节点指针,那么这个方法可以解决问题。树中的节点就是我在问题中给出的结构-只有左/右子节点指针,没有父节点指针。如果没有可用的父节点指针,并且树不是二叉搜索树,而只是普通的二叉树,那么是否有O(log(n))的解决方案?
如果没有特定的方法来找到父节点和给定节点之间的路径,那么平均需要O(n)的时间来找到。这将使得实现O(log(n))的时间变得不可能。然而,如果在不改变树的情况下要多次执行此操作,那么O(n)的一次成本,O(1)的一次查找可能是你最好的选择。否则,如果可能的话,应该添加父节点指针。它可以加快许多潜在的算法,但我相当确定它不会改变任何现有算法的顺序。希望这对你有所帮助。
这种方法可以使用O(1)的内存完成--请参考Artelius(和其他人)在stackoverflow.com/questions/1594061/…上的解决方案。
确实,这可以限制基于列表的算法的内存复杂度为O(1)。显然,这意味着为了获取节点的深度,必须对树本身进行一次遍历,然后再对树进行(部分)第二次遍历以找到公共祖先。对于具有父节点指针的情况,O(h)的时间和O(1)的空间显然是最优的,并且不需要O(n)的预计算。O(h)的复杂度是如何确定的?这不是BST,所以要找出从根到叶节点的路径,我们必须遍历左右子树。所以这不能是O(log n)。
只有当树是平衡的时候,O(h)才等于O(log(n))。对于任何树,无论是二叉树还是其他类型的树,如果有父节点指针,你可以通过跟随父节点指针最多h次来确定从叶节点到根节点的路径,这需要O(h)的时间。这给出了从叶到根的路径。如果将路径存储为一个栈,那么迭代栈会给出从根到叶的路径。如果没有父节点指针,并且树没有特殊的结构,那么找到从根到叶的路径确实需要O(n)的时间。
在任何二叉树中找到两个节点的最低共同祖先的问题是一个常见的问题。为了解决这个问题,可以使用以下方法:
1. 从根节点开始向下遍历,如果找到任何一个节点的直接子节点是p或q,则该节点就是LCA。如果节点的值等于p或q,则返回该节点。否则,在p或q是彼此的直接子节点时,该方法会失败。
2. 如果在节点的右子树(或左子树)中找到了p,并且在其左子树(或右子树)中找到了q,则该节点就是LCA。
3. 如果上述两种情况都不满足,则递归地在左子树和右子树中寻找p和q的LCA。
下面是一个修复了代码错误的示例代码:
public Node findLCA(Node root, Node p, Node q) { if (root == null) { return null; } if (root == p || root == q) { return root; } else { Node l = findLCA(root.left, p, q); Node r = findLCA(root.right, p, q); if (l != null && r != null) { return root; } else if (l != null) { return l; } else { return r; } } }
上述代码在p或q是根节点的情况下返回正确的结果。但是,当根节点是p或q,并且另一个节点不在树中时,该代码会失败。为了解决这个问题,可以对代码进行包装,并添加两个标志位flag1和flag2。当找到一个节点时,将相应的标志位设置为true。然后在包装函数中检查两个标志位是否都设置为true,如果是,则返回该节点的值,否则返回null。
public Node findLCAWrapper(Node root, Node p, Node q) { boolean flag1 = false; boolean flag2 = false; Node lca = findLCA(root, p, q, flag1, flag2); if (flag1 && flag2) { return lca; } else { return null; } } public Node findLCA(Node root, Node p, Node q, boolean flag1, boolean flag2) { if (root == null) { return null; } if (root == p) { flag1 = true; } if (root == q) { flag2 = true; } Node l = findLCA(root.left, p, q, flag1, flag2); Node r = findLCA(root.right, p, q, flag1, flag2); if (l != null && r != null) { return root; } else if (l != null) { return l; } else { return r; } }
这种解决方法的时间复杂度为O(n),因为在最坏的情况下,每个节点只遍历一次。这是一种没有使用父节点指针的最有效的解决方法。
需要注意的是,上述代码只适用于二叉树,而不适用于二叉搜索树(BST)。此外,在找到p和q之后,代码仍然会继续搜索,这是因为p和q在树中可能不是唯一的,可能存在重复。因此,代码会继续搜索以确保找到最低共同祖先。
总之,通过递归遍历二叉树的节点,可以找到任何二叉树中两个节点的最低共同祖先。这种方法的时间复杂度为O(n),并且不需要使用父节点指针。