我的目标是编写一个递归程序来反转整数的数字。
由于这是行业中没有人可能希望实现的目标,因此这对于一门课程来说很有可能。特别是,即使有这个问题,任何明智的人都不会用递归来解决这个问题。所以这是一个教递归的糟糕问题,因为非递归解决方案是如此简单。
你不太了解递归的可能性也很大。从某种意义上说,这是一个很好的练习,因为它需要您使用一种称为累加器的特殊技术来实现递归解决方案。
此外,即使您修复了错误,您的解决方案也不会产生所需的数字。它打印它,这是完全不同的。我们希望能够产生结果,而不仅仅是显示它。
(然后提出了一个有趣的问题,即如何处理适合 int 但其反向不适合的数字,例如 1000000009。让我们暂时忽略这些问题。)
让我们从基础开始。每个递归方法都有相同的结构:
- 我们是否处于基本情况?如果是,则返回结果。
- 我们不在基本情况下。
- 将问题分解为更小的问题
- 递归解决每个问题
- 结合解决方案来解决更大的问题
- 返回结果。
让我们天真地将此模式应用于您的问题,看看会发生什么。
基本情况很简单:
什么是递归情况?假设我们有 157 个
- 将该数字除以 10 得到一个较小的数字,即 15
- 反转较小的数字 -- 51
- 将原始数字的低位数字附加为反向较小数字的高位数字,751 --- 等等,我们如何做到这一点?
不清楚如何执行最后一个操作。
我们需要变得更聪明。
这就是你要做的。 (我省略了拒绝负数的错误检查。)
static int ReverseHelper(int number, int accumulator) =>
number == 0 ?
accumulator :
ReverseHelper(number / 10, accumulator * 10 + number % 10);
public static int Reverse(int number) =>
ReverseHelper(number, 0);
让我们看一些案例。
假设number 是0。然后ReverseHelper 返回0,很好。零工作。
假设number 是3。然后我们调用ReverseHelper(3, 0),它调用ReverseHelper(0, 3),它返回3。一位数有效。
假设number 是35。我们调用ReverseHelper(35, 0),它调用ReverseHelper(3, 5),它调用ReverseHelper(0, 53),它返回53。两位数有效。
等等。
练习:有一种直接的方法来处理反转 int 的问题,其中反转不适合 int;这是什么?
你明白我希望为什么使用累加器来调用这种技术。随着递归的进行,所需的结果逐渐累积,然后在递归达到其基本情况时返回。
现在,请注意这项技术与您的原始程序有何关联。 您正在使用静态字段作为累加器。永远不要那样做!曾经!递归方法的正确性不应依赖于外部状态。 相反,将您将递归使用的值传递到对辅助方法的递归调用中。
仔细研究这项技术。这是编写递归程序的强大技术。在函数式语言中,这样的操作称为fold 或reduce;向那些寻求进一步的灵感。在 C# 中,它被称为 Aggregate,用于生成序列的汇总结果。
使用累加器的递归模式是:
- 我们是否处于基本情况?如果是,则返回累加器的结果。
- 我们不在基本情况下。
- 将问题分解为更小的问题
- 递归地解决每个问题,通过将有关解决方案的信息累积到递归调用的累加器中。
- 结合解决方案来解决更大的问题
- 返回结果。
为什么这项技术如此有价值?在 C# 中,这不太重要,但在 尾递归函数式语言中,编译器通常可以将使用累加器的递归方法优化为比非尾递归方法更有效的代码,因此还有待研究。
它在 C# 中很有价值,因为使用累加器的尾递归方法可以通过简单的程序转换过程轻松转换为非递归方法。
让我们看看如何。我们有:
static int ReverseHelper(int number, int accumulator) =>
number == 0 ?
accumulator :
ReverseHelper(number / 10, accumulator * 10 + number % 10);
让我们将其重写为一个语句:
static int ReverseHelper(int number, int accumulator)
{
if (number == 0)
return accumulator;
else
return ReverseHelper(number / 10, accumulator * 10 + number % 10);
}
}
现在让我们为所有子表达式创建解释变量:
static int ReverseHelper(int number, int accumulator)
{
if (number == 0)
return accumulator;
else
{
int newNumber = number / 10;
int newAccumulator = accumulator * 10 + number % 10;
return ReverseHelper(newNumber, newAccumulator);
}
}
到目前为止一切顺利。但是现在我们注意到我们可以把整个事情变成一个循环!
static int ReverseHelper(int number, int accumulator)
{
while (true)
{
if (number == 0)
return accumulator;
else
{
int newNumber = number / 10;
int newAccumulator = accumulator * 10 + number % 10;
number = newNumber;
accumulator = newAccumulator;
// And now the loop starts over!
}
}
}
当然,我们看到这现在具有问题的直接非递归解决方案的形式。
这应该让您深入了解递归程序的本质:
- 每个递归程序都可以转换为尾递归程序,但并不总是很容易看出如何。
- 每个尾递归程序都可以转换为非递归循环。
- 反之亦然!每个带有循环的程序都可以翻译成尾递归程序!
再次仔细研究这些简单的技巧。理解递归程序和非递归程序之间的关系是 CS 理论中的一个关键工具。