递归和核心递归之间有什么区别?


55

这些有什么区别?

在Wikipedia上,几乎没有信息,也没有清晰的代码来解释这些术语。

有哪些非常简单的示例可以解释这些术语?

corecursion如何实现递归的对偶?

有没有经典的corecusive算法?


45
请参阅SOstackoverflow.com/questions/10138735/…的答案 (对不起,无法阻止自己)
高性能标记

7
@HighPerformanceMark,它没有解释什么是corecursion,我们需要另一个问题
Abyx 2012年

5
但是,严重的是,维基百科对这些术语的解释有什么问题?
Performance Performance Mark

5
维基百科上的corecursion解释很糟糕。我怀疑对于任何还不知道核心递归是什么的人是否有意义。
Marcin 2012年

9
@High Performance Mark:在我理解双关语之前,我点击链接三次以为出现了错误。哈哈
Giorgio

Answers:


24

有很多很好的方法可以解决这个问题。对我来说,最简单的方法是考虑“归纳”和“共归定义”之间的关系

集合的归纳定义是这样的。

集合“ Nat”定义为最小集合,以使“零”在Nat中,如果n在Nat中,“ Succ n”在Nat中。

对应于以下Ocaml

type nat = Zero | Succ of nat

关于此定义要注意的一件事是

omega = Succ(omega)

不是该集合的成员。为什么?假设是,现在考虑集合N与元素Nat具有所有相同的元素,只不过它没有omega。显然,零在N中,如果y在N中,则Succ(y)在N中,但是N小于Nat,这是一个矛盾。因此,欧米茄不在Nat中。

或者,对于计算机科学家来说可能更有用:

给定某个集合“ a”,将集合“ a的列表”定义为最小集合,以使“ Nil”在a的列表中,并且如果xs在a的列表中并且x在“ Cons x xs”中在清单a中。

对应于类似的东西

type 'a list = Nil | Cons of 'a * 'a list

这里的操作词是“最小的”。如果我们不说“最小的”,我们将没有任何办法告诉我们这套Nat是否包含香蕉!

再次,

zeros = Cons(Zero,zeros)

不是nat列表的有效定义,就像omega不是有效的Nat。

像这样以归纳方式定义数据,使我们可以使用递归来定义在其上起作用的函数

let rec plus a b = match a with
                   | Zero    -> b
                   | Succ(c) -> let r = plus c b in Succ(r)

然后我们可以使用归纳法(特别是结构归纳法)证明有关这一事实,例如“加零= a”

我们的证明通过对a的结构归纳来进行。
对于基本情况,将a设为零。 plus Zero Zero = match Zero with |Zero -> Zero | Succ(c) -> let r = plus c b in Succ(r)所以我们知道plus Zero Zero = Zero。坐一下a。假设的归纳假设plus a Zero = a。我们现在表明,plus (Succ(a)) Zero = Succ(a)这是显而易见的,因为plus (Succ(a)) Zero = match a with |Zero -> Zero | Succ(a) -> let r = plus a Zero in Succ(r) = let r = a in Succ(r) = Succ(a) 这样,通过归纳plus a Zero = a所有a的NAT

我们当然可以证明更多有趣的事情,但这是总的想法。

到目前为止,我们已经通过归纳定义数据作为“最小”集来处理数据。因此,现在我们要使用共同定义的协同数据,通过将其设为最大集合来获得协同数据

所以

让一个集合。集合“ a的流”定义为最大集合,这样对于a的流中的每个x,x都由有序对(头,尾)组成,使得头在a中,而尾在a的流中

在Haskell中,我们将其表示为

data Stream a = Stream a (Stream a) --"data" not "newtype"

实际上,在Haskell中,我们通常使用内置列表,该列表可以是有序对或空列表。

data [a] = [] | a:[a]

香蕉也不是这种类型的成员,因为它既不是有序对也不是空列表。但是,现在我们可以说

ones = 1:ones

这是一个完全有效的定义。而且,我们可以对此共同数据执行共同递归。实际上,一个函数既可以是联合递归的,也可以是递归的。虽然递归由具有包含数据的的函数定义,但是共递仅表示它具有作为数据的共域(也称为范围)。原始递归意味着始终在较小的数据上“自称”,直到到达一些最小的数据为止。原始共同递归总是在大于或等于以前的数据上“调用自身”。

ones = 1:ones

原本是共递归的。虽然函数map(在命令式语言中有点像“ foreach”)既是原始的递归(某种)又是原始的共递归。

map :: (a -> b) -> [a] -> [b]
map f []     = []
map f (x:xs) = (f x):map f xs

这同样适用于功能zipWith这需要一个函数和一对列表,并使用该函数一起将它们组合。

zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith f (a:as) (b:bs) = (f a b):zipWith f as bs
zipWith _ _ _           = [] --base case

功能语言的经典示例是斐波那契数列

fib 0 = 0
fib 1 = 1
fib n = (fib (n-1)) + (fib (n-2))

原本是递归的,但可以更优雅地表示为无限列表

fibs = 0:1:zipWith (+) fibs (tail fibs)
fib' n = fibs !! n --the !! is haskell syntax for index at

归纳/共归的一个有趣的例子是证明这两个定义可以计算相同的事物。这留给读者练习。


1
@ user1131997谢谢。我打算将一些代码转换为Java,请继续关注
Philip JF

@PhilipJF:我觉得很愚蠢,但是我不明白为什么“ ... N显然是零,如果y在N中,Succ(y)在N ...”。如果y是满足Succ(y)= omega的某物会发生什么?(因为您不使用零和Succ的任何属性,所以我可以替换Succ =平方根和零= 2)
Ta Thanh Dinh 2014年

...然后我看到的ω= 1
钽清亭

目的是证明欧米茄不存在。我们这样做是矛盾的。如果omega处于自然状态,则集合N = nat-{omega}将满足定律。那是因为nat符合法律。如果y在N中,则1. y不是欧米茄,而2. y在nat中。从2中我们可以知道Succ(y)的自然值,到1 y则不是ΩSucc(y)并不是Ω。因此,N中的Succ(y)也包括零。但是,N比nat小。这是一个矛盾。因此,nat不包括欧米茄。
菲利普·JF 2014年

因为ocaml具有值递归功能,所以这一切都是谎言。我确实应该使用SML,它是唯一支持归纳推理的“主流”式语言。
菲利普·JF 2014年

10

基本上,corecursion是递归累加器式的,其结果是从起始案例开始的,而常规的递归则是从基础案例返回的结果。

(现在讲Haskell)。这就是为什么foldr(有严格的组合功能)表示递归,foldl'(严格的梳子。F。)/ scanl/ until/ iterate/ unfoldr/等表达corecursion。Corecursion无处不在。foldr用非严格的梳子。F。表示尾递归模的缺点

和Haskell的守卫递归只是尾递归模利弊

这是递归:

fib n | n==0 = 0
      | n==1 = 1
      | n>1  = fib (n-1) + fib (n-2)

fib n = snd $ g n
  where
    g n | n==0 = (1,0)
        | n>0  = let { (b,a) = g (n-1) } in (b+a,b)

fib n = snd $ foldr (\_ (b,a) -> (b+a,b)) (1,0) [n,n-1..1]

(读$为“ of”)。这是corecursion:

fib n = g (0,1) 0 n where
  g n (a,b) i | i==n      = a 
              | otherwise = g n (b,a+b) (i+1)

fib n = fst.snd $ until ((==n).fst) (\(i,(a,b)) -> (i+1,(b,a+b))) (0,(0,1))
      = fst $ foldl (\(a,b) _ -> (b,a+b)) (0,1) [1..n]
      = fst $ last $ scanl (\(a,b) _ -> (b,a+b)) (0,1) [1..n]
      = fst (fibs!!n)  where  fibs = scanl (\(a,b) _ -> (b,a+b)) (0,1) [1..]
      = fst (fibs!!n)  where  fibs = iterate (\(a,b) -> (b,a+b)) (0,1)
      = (fibs!!n)  where  fibs = unfoldr (\(a,b) -> Just (a, (b,a+b))) (0,1)
      = (fibs!!n)  where  fibs = 0:1:map (\(a,b)->a+b) (zip fibs $ tail fibs)
      = (fibs!!n)  where  fibs = 0:1:zipWith (+) fibs (tail fibs)
      = (fibs!!n)  where  fibs = 0:scanl (+) 1 fibs
      = .....

折叠:http : //en.wikipedia.org/wiki/Fold_(高级功能)


4

Vitomir Kovanovic的博客上进行检查。我发现了这一点:

懒惰求值是编程语言中具有lisp,haskell,python等功能编程功能的一项非常不错的功能。它要求将变量值的求值延迟到该变量的实际使用。

这意味着,例如,如果您要创建一百万个这样的元素的列表,(defn x (range 1000000))则实际上并没有创建它,而是仅在第一次使用该变量时(例如,当您想要第10个元素时)指定该列表。该列表解释器仅创建该列表的前10个元素。因此,第一次运行(占用10倍)实际上创建了这些元素,并且对同一函数的所有后续调用都在使用已经存在的元素。

这非常有用,因为您可以创建无限列表而不会出现内存不足的错误。当然,如果您的程序正在处理大量数据,则在使用这些无限列表时可能会达到内存限制。

另一方面,corecursion对递归是双重的。这是什么意思 就像递归函数以自身形式表示一样,核心递归变量也以自身形式表示。

最好在示例中表达出来。

假设我们要列出所有素数...


1
博客站点已死。
Jason Hu
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.