我在您的问题中看到了两个重要的计算概念。第一个是二分查找,第二个是递归。由于这是家庭作业,因此这是对理解二分搜索、递归以及如何思考它们的贡献。
将二进制搜索视为将解决方案“空间”分成两半,保留解决方案所在的一半并连续执行此操作,以便该过程收敛于解决方案。这样做的关键概念是您需要设计一个具有以下属性的解决方案“空间”:
1) 可以细分,通常分成两半或至少两片
2)在细分后的两片中,有一种方法可以确定哪一半有解,这样该过程就可以只在一半上重复。
递归涉及调用自身的函数(OO 中的方法)。递归对于收敛到结论的过程非常有效。它要么永远递归,要么直到你用完一些资源,通常是内存,然后它会致命地停止。递归的两个关键概念是:
1)通过一些不变性收敛(更多关于下面的不变性)。
2) 终止条件(承认充分收敛的条件)。
现在,对于您的平方根例程。例程的要求是:
1) 整数输入。
2) 整数平方根近似,给出最接近实际平方根的下限整数。
3)使用递归。
4) 使用二分查找。
这有助于了解一些关于平方根的数学。初等微积分和解析几何概念也很有帮助。让我们做一些推理。
我们有一个任意的正整数 x。我们想要它的根 y。如果我们为 y 选择一些测试值,如果 y * y = x,我们可以查看它是否是 x 的根。如果 y 太大,则 y * y > x。如果 y 太小,则 y * y < x。我们也知道 0 <= root <= x 并且 0 和 1 的平方根通常是 0 和 1。因为我们正在寻找 y * y <= x 的最大整数(即底值),所以我们将有也要考虑到这一点。
这里有一些数学推理可以提供帮助。我们知道 x = y * y 其中 y 是 x 的平方根。这意味着:y = x/y。
嗯...如果 y 大到成为 x 的平方根会怎样?然后: x < y * y 和: x/y < y 这意味着 x/y 也太小而不能成为 x 的平方根。所以我们知道,对于 y 太大,x/y < x < y 的平方根。所以,让我们在 x/y 和 y 之间找到一个新的 y,比如 y1,作为新的测试值。x/y 和 y 的平均值就可以了。如果 y0 太大,y1 = (x/y0 + y0)/2 将给出比 y0 更接近 x 平方根的 y1。
这会收敛吗?好吧,在使用正实数的数学中,平均值总是高于该值,但每次迭代都越来越接近。这满足了我们依次将解“空间”分成两部分并知道要保留哪两部分的条件。在这种情况下,我们连续计算低于先前值和答案仍然存在的新值,允许我们丢弃高于新值的所有值。当我们达到不再存在高于答案的新值的条件时,我们停止。然而,使用计算机会产生实数的二进制近似值。对于整数,除法有截断。这可能对收敛产生有利或不利的影响。此外,您的答案应该是小于或等于平方根的最大整数。它'
由于整数除法转换,y1 = (x/y0 + y0)/2 将收敛,直到连续迭代达到整数根或(即小于该根的最大整数)的底值。这是理想的。如果我们从一个必须大于根的建议值开始,比如 x 本身,那么 yn * yn <= x 的 yn 的第一个值就是期望的结果。
简单的答案是,当我们从 y0 > y 开始时,第一个小于或等于 y 的新 yn,然后 y - yn < 1。也就是说,yn 现在是我们一直在寻找的底值我们现在有了一个完全满足所需答案条件的终止条件。
以下是基本的迭代和递归解决方案。解决方案不包含安全功能以确保不为 x 输入负值。一个主要的问题是避免除以零,以防有人想要找到 0 的平方根。由于这是一个简单的答案,递归和迭代方法都在除以零之前返回 0。递归和迭代解决方案都适用于寻找 0 和 1 的平方根的平凡情况。
在 Java 中,还有另一种分析总是需要用 int 和 long 算术来完成。一个主要问题是整数溢出,因为 Java 对 int 或 long 溢出没有任何作用。溢出会导致二进制补码值(在别处查找),这可能导致虚假结果,Java 不会抛出 int 或 long 溢出异常。
在这种情况下,很容易避免可能导致较大 x 值的内部溢出的算术运算。如果我们创建一个终止条件,例如 y0 * y0 < x,如果 x 大于 Integer.MAX_VALUE 的平方根,我们就有溢出的风险,因为中间值 y0 * y0 将立即超过最大 int 值。但是,我们可以将终止条件重新排列为 y0 < x / y0。我们的计算仍然存在问题: ((x / y0) + y0) / 2) 如果 x 和 y0 是 Integer.MAX_VALUE 因为它将尝试 Integer.MAX_VALUE + 1。但是,我们总是可以从小于的值开始x 保证 > y。x / 2 适用于所有 x > 1 的值。由于 x 为 0 或 1 的 x 的平方根就是 x,我们可以轻松地测试这些值并简单地返回正确且微不足道的值。您可以构造代码以防止使用值 < 0 或值 > Integer.MAX_VALUE。如果我们使用 long 而不是 int,同样可以应用。欢迎来到现实世界中的计算!
public static int intSqRootRecursive (int x) {
// square roots of 0 and 1 are trivial and x / 2 for
// the y0 parameter will cause a divide-by-zero exception
if (x == 0 || x == 1) {
return x;
}
// starting with x / 2 avoids overflow issues
return intSqRootRecursive (x, x / 2);
} // end intSqRootRecursive
private static int intSqRootRecursive(int x, int y0) {
// square roots of 0 and 1 are trivial
// y0 == 0 will cause a divide-by-zero exception
if (x == 0 || x == 1) {
return x;
} // end if
if (y0 > x / y0) {
int y1 = ((x / y0) + y0) / 2;
return intSqRootRecursive(x, y1);
} else {
return y0;
} // end if...else
} // end intSqRootRecursive
public static int intSqRootIterative(int x) {
// square roots of 0 and 1 are trivial and
// y == 0 will cause a divide-by-zero exception
if (x == 0 || x == 1) {
return x;
} // end if
int y;
// starting with y = x / 2 avoids overflow issues
for (y = x / 2; y > x / y; y = ((x / y) + y) / 2);
return y;
} // end intSqRootIterative
您可以测试递归解决方案以找出帧堆栈上会产生多少实例,但您会看到它收敛得非常快。有趣的是,迭代解决方案比递归解决方案更小更快,这通常不是这种情况,这就是为什么在可以预测堆栈资源足以满足递归深度的情况下使用递归的原因。