我相信您采用的方法会奏效,但没有适当的时间限制。
让我们从分析这个算法的复杂性开始。请注意,这里需要考虑两个不同的参数。首先,BST 中的元素总数。如果你让 BST 更大或更小,算法完成所需的时间或多或少。我们称这个数字为 n。其次,您希望这些值相加的总数。我们称该值为 U。
那么让我们看看你当前的算法是做什么的。现在,它迭代一个循环 O(U) 次,在每次迭代时检查 BST 中是否存在两个值。每个 BST 查找需要时间 O(log n),因此您的算法所做的工作总量是 O(U log n)。但是,您的算法仅使用常量空间(即空间 O(1))。
不幸的是,这个运行时一点都不好。如果目标数字非常大(例如,1,000,000,000),那么您的算法将需要很长时间才能完成,因为 U 将非常大。
所以现在的问题是如何更有效地解决这个问题。幸运的是,我们可以使用一个非常可爱的算法来解决这个问题,它将利用 BST 的元素按排序顺序存储的事实。
让我们从解决一个与您提出的问题有点不同的类似问题开始。假设我没有给你一个 BST 和一个目标数,而是给你一个排序数组和一个目标数,然后问同样的问题:这个排序数组中是否有两个数相加等于目标数?例如,我可能会给你这个数组:
0 1 3 6 8 9 10 14 19
假设您想知道这个数组中的两个数字的总和是否为 16。您将如何做到这一点?
您可以尝试以前的方法:检查数组是否包含 0 和 16、1 和 15、2 和 14 等,直到找到一对或用完要检查的值。使用二分查找检查一个元素是否存在于排序数组中需要时间 O(log n),因此该算法仍然需要 O(U log n) 时间。(如果您知道这些值分布良好,那么您可以使用插值搜索来加速这一过程,这将使 O(U log log n) 运行时间符合预期,但大的 U 项仍然是一个问题)。
所以让我们考虑一种不同的方法。从根本上说,您正在做的事情需要您明确枚举总和为 U 的所有对。但是,它们中的大多数不会存在,事实上,大多数情况下,对中的任何元素都不存在. 通过使用以下算法,我们可以使事情变得更快:
- 对于 x 数组的每个元素,检查 U - x 是否在数组中。
- 如果是,则报告成功。
- 否则,如果不存在这样的对,则报告失败。
该算法将要求您查看数组中总共 O(n) 个元素,每次执行 O(log n) 个工作以找到匹配对。在这种最坏的情况下,这将花费 O(n log n) 时间并使用 O(1) 空间。如果 U 是一个巨大的数字,这比以前要好得多,因为根本不再依赖于 U!
但是,我们可以通过巧妙地观察问题的结构来进一步加快速度。假设我们正在查看数组中的数字 x 并进行二进制搜索以查找 U - x。如果我们找到它,我们就完成了。如果不是,我们将在数组中找到第一个大于 U - x 的数字。让我们称这个数字为 z。
所以现在假设我们想看看一个数字 y 是否可以是总和为 U 的对的一部分,此外,假设 y 大于 x。在这种情况下,我们知道
y + z
> x + z
> x + (U - x)
= U
这说明 y 和 z 之和大于U,所以它不可能是 U。这是什么意思?好吧,我们通过尝试对与 x 配对的元素进行二分搜索来找到z ,总数必须超过 U。换句话说,z 不能与大于 x 的任何东西配对。同样,任何大于 z 的东西都不能与大于 x 的东西配对,因为它必须总结为大于 U 的东西。
基于这一观察,我们可以尝试使用不同的算法。让我们像以前一样使用我们的数组,看看是否能找到总和为 16 的对:
0 1 3 6 8 9 10 14 19
让我们维护两个指针——一个“左侧”指针 lhs 和一个“右侧”指针 rhs:
0 1 3 6 8 9 10 14 19
^ ^
lhs rhs
当我们将这两个数字相加时,我们得到 19,它超过了 U。现在,我们相加的任何一对数字都必须使其较小的数字至少与 lhs 数字一样大,即 0。因此,如果我们尝试在这里总结任何使用数字 19 的对,我们知道总和会太大。因此,我们可以从考虑中排除任何包含 19 的对。这就剩下
0 1 3 6 8 9 10 14 19
^ ^
lhs rhs
现在,看看这个和(14),太小了。使用与之前类似的逻辑,我们可以有把握地说,剩余数组中使用 0 的任何总和最终给出的总和必须小于 16,因为总和中的第二个数字最多为 14。因此,我们可以排除0:
0 1 3 6 8 9 10 14 19
^ ^
lhs rhs
我们开始看到一种模式:
- 如果总和太小,则提前 lhs。
- 如果总和太大,则减少 rhs。
最终,我们要么找到总和为 16 的一对,要么 lhs 和 rhs 相互碰撞,此时我们保证没有一对总和为 16。
跟踪这个算法,我们得到
0 1 3 6 8 9 10 14 19
^ ^
lhs rhs (sum is 15, too small)
0 1 3 6 8 9 10 14 19
^ ^
lhs rhs (sum is 17, too big)
0 1 3 6 8 9 10 14 19
^ ^
lhs rhs (sum is 13, too small)
0 1 3 6 8 9 10 14 19
^ ^
lhs rhs (sum is 16, we're done!)
瞧!我们得到了答案。
那么这有多快呢?好吧,在每次迭代中,要么 lhs 下降,要么 rhs 上升,当它们相遇时算法停止。这意味着我们进行 O(n) 次总迭代。每次迭代最多进行恒定的工作,因此每次迭代最多需要 O(1) 次工作。这给出了 O(n) 的总时间复杂度。
空间呢?好吧,我们需要存储两个指针,每个指针占用 O(1) 空间,所以总的空间使用量是 O(1)。伟大的!
但这与您的问题有什么关系?联系是这样的:在每个时间点,这个算法只需要记住数组中的两个数字。然后它需要能够从一个元素前进到下一个元素或从一个元素前进到前一个元素。这就是它所要做的。
因此,假设您不使用排序数组,而是使用二叉搜索树。从两个指针开始,一个指向最小节点,一个指向最大节点。完成此设置后,您可以模拟上述算法,将“递增 lhs”和“递减 rhs”替换为“移动到 lhs 的有序后继”和“移动到 rhs 的有序前驱”。可以实现这些操作,以便它们使用总共 O(log n) 空间(假设树是平衡的),并且每种类型的 n 操作的任何序列总共需要 O(n) 时间(无论是否树是平衡的)。因此,如果您要使用修改后的上述算法来处理 BST,
实现细节有点棘手,我将把这部分作为练习留给你。如果您不熟悉按顺序排列后继和前任,网上有很多很好的资源。它们是 BST 上的标准算法,非常有用。我还将把数组算法正确性的正式证明留作练习,尽管它并不太难。
希望这可以帮助!