Cairo 之旅 VI:掌握递归

作者:Darington Nnam
原文:Journey through Cairo VI — Mastering Recursion
翻译:Louis Wang
校对:「StarkNet 中文社区」

欢迎来到我们的系列文章「Cairo之旅」第六讲。上一讲中我们学习了隐式参数,今天我们要聊 Cairo 中很重要的一章:递归。

像往常一样,如果你是中途加入的,建议从头开始看我们的文章。

P.S:教程中的语法代码都是在 Cairo v0.9.0 版本下使用的

数组是神秘的

在开始使用 Cairo 时,我听到的一个令人震惊的事情是我不能写循环。没有 for 循环,没有 while 循环等等,只有递归。主要原因是 Cairo 的内存是不可改变的,也就是说,一旦你写了一个内存单元的值,这个单元在将来就不会再改变。

这是很令人沮丧的,但值得庆幸的是,在 Cairo 1.0 中会看到这方面的更新。

什么是递归?

在计算机科学中,递归是一种使用函数或算法的编程技术,它可以一次或多次调用自己,直到满足一个指定的条件。

更简单地说,如果一个函数反复调用自己,该函数就是一个递归函数。注意,该函数必须有一个停止递归的基础条件,否则程序可能会出现堆栈溢出。

递归是相当复杂的,我花了一些时间才完全理解,所以如果你在第一次阅读时没有理解也不要自责。我们将通过一些 Starklings 的练习来更好地解释递归。

recursion01.cairo

在这个练习中,我们要写一个斐波那契数的递归实现,返回第 n 个斐波那契数。要解决这个练习,我们首先要了解什么是斐波纳契数。

斐波那契数列是一个数列,其中每个数字 ( 斐波那契数 ) 都是前面两个数字之和。该系列的前 13 个数字是:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144

从上面的定义中,我们可以看出,要得到第 n 个斐波那契数,我们需要把第 n-1 个的数字和第 n-2 个的数字相加。(因为每个数字都是前面两个数字之和)。

用数学的形式表达:

nth number = (nth - 1) + (nth - 2)

要求第五个斐波那契数,就是:

Fib(5) = Fib(4) + Fib(3)

由前面可知,第三第四个分别是 2,1。所以第五个就是:

Fib(5) = 2 + 1 = 3

我们可以很容易地推导出这一点,那是因为我们只求了第五,但想象一下要求得到第一百个斐波那契数字,我们不能手动做这件事,我们需要写一个程序来做这件事。

如果我们使用 Javascript、Python、Solidity 等,我们可以很容易地使用 for 循环来完成这个任务,但由于 Cairo 还不支持 for 循环,我们要使用递归,具体步骤如下:

首先我们需要注意,由于我们正在编写一个计算机程序,我们的第一个斐波那契数字的索引是0,而不是 1。

我们要设置基本条件以避免堆栈溢出。由于我们已经知道第一个和第二个斐波那契数总是 0 和 1,所以我们要检查我们的 n,如果是 0 就自动返回 0,如果是 1 就返回 1:

if n == 0:
  return (0)
end
if n == 1:
  return (1)
end

由于第 n 个斐波那契项是第 n-1 个和第 n-2 个的数字之和,我们需要用递归来获得这些数字*(调用斐波那契函数,将 n-1 和 n-2 项作为新的 n 传递给它)*:

let (x) = fibonacci(n - 1)
let (y) = fibonacci(n - 2)

返回这两个数的和:

return (x + y)

完整代码如下:

func fibonacci(n : felt) -> (result : felt):
  alloc_locals
  if n == 0:
    return (0)
  end
  if n == 1:
    return (1)
  end
  let (local x) = fibonacci(n - 1)
  let (local y) = fibonacci(n - 2)
  return (x + y)
end

这里我们引入了一些新的东西,如 alloc_locals 和 local 关键字,这是撤销引用,我们将在本系列的下一讲深入研究。

现在检查一下我们的测试是否通过:

没问题。

array01.cairo

递归在数组中有着巨大的应用,与循环类似。在这个练习中,我们要通过 contains 函数进行循环,如果 haystack 包含 needle,则返回 1,否则返回 0。

为了解决这个练习,我们将遵循斐波那契练习的步骤。

建立基本条件

首先需要检查给定的数组长度(haystack_len)是否为 0,如果是,我们将自动返回 0,因为这意味着数组是空的。

if haystack_len == 0:
  return (0)
end

接下来我们检查数组的第一个元素是不是 needle,是的话返回 1

if haystack[0] == needle:
  return (1)
end

用递归循环 contains 函数

let (contained) = contains(needle, haystack + 1, haystack_len - 1)
return (contained)

我们这里用递归循环 contains 函数,每次都将被检查的数组索引增加 1,并将数组长度减少 1。

这背后的逻辑是,每次我们通过 contains 函数进行递归,把数组的输入长度不断减少,直到我们到达数组的最后一个元素。这里有一个场景可以帮助你更好地理解这个问题:

如果我们有一个数组,x 和 needle=3。

x = [0, 1, 2, 3, 4, 5]

检查基本条件:

如果数组长度为 0,我们会返回 0,但事实并非如此,所以我们转而检查 x[0]= needle,但事实并非如此,因为 x[0]=0,但 needle=3。

接下来开始递归:

调用 contains 函数,这次传递的是 needle,将数组索引向上推 1,变成 x+1 而不是 x,然后将数组长度也减少 1,因为我们刚刚从数组中剔除了第一个元素。

因此,当第一次调用 contains 函数时,看起来会是这样的:

contains(3, [1, 2, 3, 4, 5], 5)

继续下去直到数组长度为 0。完整代码:

func contains(needle : felt, haystack : felt*, haystack_len : felt) -> (result : felt):
  if haystack_len == 0:
    return (0)
  end
  if haystack[0] == needle:
    return (1)
  end
  let (contained) = contains(needle, haystack + 1, haystack_len - 1)
  return (contained)
end

看看能否通过测试:

成功!

最后

开始这个系列以来我们已经取得了重大的进展,很高兴你能走到这一步!

为了缩短本文的篇幅,我们只尝试了 Starklings 中递归部分的 7 个练习中的 2 个。希望你尝试剩下的 5 个练习,这将有助于提高你对递归的理解。

像往常一样,如果你觉得这篇文章有收获,不妨与他人分享。

Subscribe to Starknet 中文
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.