学习从零到一的递归

我记得看到我的小侄子在数。 他会看着电梯上的按钮,然后数:

0, 1, 2, 3, 4, 5, 6, ...

它们是自然数。 这种通常的书写数字形式称为印度阿拉伯数字。 不过,在过去的欧洲,人们使用罗马数字:

, I, II, III, IIII, V, VI, ...

你能看到一些模式吗?

  • 没有零符号。 零不算什么。一个是一世,或者,之后没有任何内容一世。二是一世一世,或者,后跟一个一世。三是一世一世一世或两个一世。四是一世一世一世一世或三个一世。

假装空间不是问题,那么五个就可以了IIIII代替V,而六个可以是IIIIII代替六。

  • 五是一世一世一世一世一世,或四个后接一世。六是一世一世一世一世一世一世,或五个,后跟一世。And so on 。。。

当我看到我的侄子数电梯上的数字时,他知道它们是楼层号。 有一天我想告诉他,第一层是在地面上构建的,第二层是在第一层构建的,第三层是在第二层构建的,依此类推...,这就是递归。 (一天。)

那就是递归! 关于递归的最早经验可能是当我们开始计算数字时。 或者,至少,当我们意识到我们可以数不清双手,电梯按钮,数字之外的所有数字时,一直到无限。

写下所有“一世...其次是一世", not to mention impossible to write it for every one of the infinitely many natural numbers. 一世nstead, we can write the first case, which is nothing, and describe all the following cases in terms of ... what? We might want to say "the previous natural number", but even the concept of "the previous" is yet to be defined. So we can only say "a natural number", without referring to any specific one.

  • 零是自然数。 零不算什么。每个自然数是一个自然数,后跟一世。

这里有递归的通常定义:定义本身的引用或用法。 第一行称为“基本情况”,而第二行称为“递归步骤”。


我希望上面对递归的简单英语解释可以帮助您掌握这一强大的主意。 多年来,人们为递归开发了各种符号。 我想使用两种表示法,注意它们的区别和共同点,这样我们就可以熟悉熟悉的表示法,同时也可以针对不同的观点探索替代方法。

首先,让我们看一下Haskell表示法。

-- Haskell 1
data Nat = Zero | I Nat

zero  = Zero
one   = I Zero
two   = I (I Zero)
three = I (I (I Zero))

我们命名自然数类型纳特,给它一个基本案例零,然后进行递归步骤一世 纳特,由or运算符连接|(竖线)。 在递归步骤中,一世是构造函数,它需要纳特作为参数。

我们可以用零。 我们也可以通过零至一世至get one. We can pass 一世 零, which is one, again至一世至get two. And so on.

不幸的是,由于Haskell的语法,而不是遵循纳特通过一世, like we described in plain English, we have to lead通过一世。 希望我们能适应向前计数。

Haskell表示法很适合显示此类构造,但对于大多数程序员而言可能并不熟悉。 因此,我介绍了大致等效的Java版本。

// Java 1
class Nat {
    Nat prev;

    Nat() { this.prev = null; }
    Nat(Nat n) { this.prev = n; }
    static Nat Zero() { return new Nat(); }
    static Nat I(Nat n) { return new Nat(n); }
}

class Main {
    public static void main (String[] argv) {
        Nat zero  = Nat.Zero();
        Nat one   = Nat.I(Nat.Zero());
        Nat two   = Nat.I(Nat.I(Nat.Zero()));
        Nat three = Nat.I(Nat.I(Nat.I(Nat.Zero())));
    }
}

我们命名我们的自然数类纳特,并引用相同的类上一个, meaning that it refers to the 上一个ious number. We then have the two constructors, 零和一世,以重载的形式纳特。 为了清楚起见,我们使用适当名称的静态方法为重载的构造函数别名。

我们可以用零()。 我们也可以通过零()至一世()至get one. We can pass I(零()), which is one, again至一世()至get two. And so on.


在进一步处理递归结构之前纳特,让我们回到我们的童年,并尽力计算一下。 为了计数,我们写“增加”和“减少”。 “增量”的确是一世构造函数。 与此配对的“减量”将前一个数字取出。 让我们用Haskell编写它们。

-- Haskell 2
increment       = I
decrement Zero  = undefined
decrement (I n) = n

在递减我们需要考虑两种情况。 因为我们没有为我们定义负数纳特类型,我们没有定义结果递减 Zero要么。 对于任何纳特的形式ñ, 的结果递减很简单ñ。

// Java 2
class Nat {
    Nat increment() { return Nat.I(this); }
    boolean isZero() { return this.prev == null; }
    Nat decrement() throws Exception { 
        if (this.isZero()) {
            throw new Exception("Cannot decrement Zero");
        } else {
            return this.prev;
        }
    }
}

遵循Java约定,我们定义增量()和减量()作为实例方法。增量()别名一世()。 对于减量(),我们抛出一个异常来表明我们不能递减零()在这种情况下,否则我们返回上一页。


我真的希望您到目前为止没有遇到任何问题。 因为如果有的话,当我们不知道如何打印任何内容时,很难调试这个小混乱纳特出来。 让我们打印我们的纳特用罗马数字!

-- Haskell 3
toRomanNumerals Zero  = ""
toRomanNumerals (I n) = (toRomanNumerals n) ++ "I"

按照我们简单的英语描述,零什么都没有,也就是一个空字符串。 所有其他纳特是它的前一个罗马数字,后跟一个附加数字“一世”。 惊喜,惊喜,这是一个递归函数!

但还要注意它与递减。 在左侧,它们是相同的(当然名称除外)! 但是,我们不提取先前的数字,而是应用罗马数字再次适用于罗马数字再次使用之前的编号罗马数字到之前的号码之前的号码...不,这不是很清楚。 让我们来看一个例子。

   toRomanNumerals (I (I (I Zero)))
=> (toRomanNumerals   (I (I Zero)))                 ++ "I"
=> ((toRomanNumerals     (I Zero))          ++ "I") ++ "I"
=> (((toRomanNumerals       Zero)   ++ "I") ++ "I") ++ "I"
=> ((""                             ++ "I") ++ "I") ++ "I"
=> ("I"                                     ++ "I") ++ "I"
=> "II"                                             ++ "I"
=> "III"

我已经将所有内容都整理好了,希望您能看到步骤之间的对应关系。 这类似于您在高中逐步简化代数表达式的方式。 称其为简化,减少或评估。 请注意,当我们迈出第一步时,罗马数字向内移动括号,(我(我(我为零)))像洋葱一样被剥皮一层++“我”,等待内部先简化。

自己尝试更多示例! 练习得越多,您就会越了解。 做出变化!

Java等效项如下所示:

// Java 3
class Nat {
    static String toRomanNumerals(Nat n) {
        if (n.isZero()) {
            return "";
        } else {
            return toRomanNumerals(n.prev) + "I";
        }
    }
}

我们再次看到与递减,这次采用if语句的形式。

为了方便起见,我们可以添加必要的仪式以进行集成罗马数字语言的常用打印工具。

-- Haskell 4
instance Show Nat where
    show = toRomanNumerals

main = do
    print zero
    print one
    print two
    print three
// Java 4
class Nat {
    public String toString() {
        return Nat.toRomanNumerals(this);
    }
}

class Main {
    public static void main (String[] argv) {
        System.out.println(zero);
        System.out.println(one);
        System.out.println(two);
        System.out.println(three);
    }
}

这仅仅是个开始。 在下一篇文章中,让我们探讨如何在此模型上定义加号和减号运算。 然后,我们将看到这与经典数据结构之一的链表如何相关。


  1. Personally, I had much pain learning recursion, because the professor only introduced it with Fibonacci sequence and went on straight to the tree traversals. That is when recursion becomes almost absolutely necessary, but not when recursion is the easiest to understand. I hope you find this post helpful in your understanding of recursion.

  2. Hindu-Arabic numerals were revolutionary for simplifying arithmetic. In exchange, their recursive structure is more complicated.

  3. IIII is the additive notation, and is more suitable for the story I am telling. The also common subtractive notation is written as IV. According to Wikipedia.

  4. For maths nerds, and I beg mathematicians for forgiveness of this abusing of mathematical analogy: Countable infinity is defined in terms of natural numbers. Natural numbers can be defined in terms of induction, roughly recursion, by [Peano axiom]. Therefore, countable infinity can be defined in terms of recursion. That is one semantic by which recursion may represent infinite loop in computation. Or, without tail call optimization, stack overflow.

  5. You may try the code yourself in the Haskell REPL and Java REPL by repl.it.

  6. When running Haskell code in repl.it, if you see the error Variable not in scope: main, put main = return () at the end. After running the Haskell code, you can still type any name after the output and press Enter to have its value printed out, or type :t <name> to see its type.

  7. When running Java code, please append the code within the same block together between the curly braces. For example, combine code pieces class Nat { a; } and class Nat { b; } into class Nat { a; b; } before running.

  8. When running code piece Haskell 3, you may be confused why it doesn't print an empty line at the beginning like Java 3 does. Both of them print a line break to begin with, but the Haskell environment has an additional prompt symbol preceding the printed line break.

  9. For basics about Haskell, try Haskell. For basics about Java, learn Java online.

  10. If you have realized that the Java code is only one data field away from a linked list, you are right! It is also the case for Haskell.

  11. This article may be regarded as a primitive form of literary programming.


如果您觉得您在这篇文章中有所收获,我很高兴! 请随时发表评论或联系。

If you feel that you've got enough that you'd like to donate, please donate to Wikimedia Foundation, to which I owe infinitely.

from: https://dev.to//louy2/learn-recursion-from-zero-to-one-2a64

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值