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