理解Haskell中的递归

时间:2013-02-11 20:20:24

标签: haskell

我很难理解如何以递归方式思考问题,并使用Haskell解决问题。我花了几个小时阅读试图绕过递归。我经常从理解它的人那里得到的解释从来都不清楚,就像“你传递一个函数,函数的名称作为参数,函数将执行,解决一小部分问题并调用一次又一次地起作用,直到你击中基础案例“。

有人能够善待,并引导我完成这三个简单递归函数的思考过程吗?与其相关的功能,不如代码,如何以递归方式执行和解决问题。

非常感谢提前!

功能1

maximum' [] = error "maximum of empty list"
maximum' [x] = x
maximum' (x:rest) = max x(maximum' rest)

功能2

take' n _  
    | n <= 0   = []  
take' _ []     = []  
take' n (x:xs) = x : take' (n-1) xs  

功能3

reverse' [] = []  
reverse' (x:xs) = reverse' xs ++ [x]  

6 个答案:

答案 0 :(得分:18)

准则

当试图理解递归时,您可能会发现更容易思考算法对于给定输入的行为。很容易挂断执行路径的样子,所以请问自己这样的问题:

  • 如果我传递一个空列表会怎样?
  • 如果我传递一个包含一个项目的列表会怎样?
  • 如果我传递包含许多项目的列表会怎样?

或者,对于数字的递归:

  • 如果我传递负数会怎样?
  • 如果我通过0会怎样?
  • 如果我传递的数字大于0会怎样?

递归算法的结构通常只是覆盖上述情况的问题。因此,让我们看看您的算法如何表现出来以了解这种方法:

最大'

maximum []     = error
maximum [1]    = 1
maximum [1, 2] = 2

正如您所看到的,唯一有趣的行为是#3。其他人只是确保算法终止。看看定义,

maximum' (x:rest) = max x (maximum' rest)

使用[1, 2]调用此选项会扩展为:

maximum [1, 2]    ~ max 1 (maximum' [2])
                  ~ max 1 2

maximum'通过返回一个数字来工作,这种情况下知道如何使用max递归处理。让我们再看一个案例:

maximum [0, 1, 2] ~ max 0 (maximum' [1, 2]) 
                  ~ max 0 (max 1 2)
                  ~ max 0 2

您可以看到,对于此输入,第一行中对maximum'的递归调用与前一个示例完全相同。

反向“

reverse []     = []
reverse [1]    = [1]
reverse [1, 2] = [2, 1]

通过取得给定列表的头部并在最后粘贴它来反向工作。对于一个空列表,这不涉及任何工作,因此这是基本情况。所以给定定义:

reverse' (x:xs) = reverse' xs ++ [x] 

让我们做一些替代。鉴于[x]等同于x:[],您可以看到实际上有两个要处理的值:

reverse' [1]    ~ reverse' [] ++ 1
                ~ []          ++ 1
                ~ [1]

够容易。对于一个双元素列表:

reverse' [0, 1] ~ reverse' [1] ++ 0
                ~ [] ++ [1] ++ 0
                ~ [1, 0]

采取“

此函数引入了对整数参数和列表的递归,因此有两种基本情况。

  1. 如果我们采取0或更少的项目会怎样?我们不需要任何物品,所以只需返回空列表。

    take' n _   | n <= 0    = [] 
    
    take' -1 [1]  = []
    take' 0  [1]  = []
    
  2. 如果我们传递空列表会怎样?没有更多的项目,所以停止递归。

    take' _ []    = []
    
    take' 1 []    = []
    take  -1 []   = []
    
  3. 算法的核心在于走下列表,拉开输入列表并减少要采用的项目数,直到上述任何一种基本情况停止进行。

    take' n (x:xs) = x : take' (n-1) xs
    

    因此,在首先满足数字基本情况的情况下,我们在到达列表末尾之前停止。

    take' 1 [9, 8]  ~ 9 : take (1-1) [8]
                    ~ 9 : take 0     [8]
                    ~ 9 : []
                    ~ [9]
    

    在首先满足列表基本情况的情况下,我们在计数器达到0之前用完项目,然后返回我们能够做的事情。

    take' 3 [9, 8]  ~ 9 : take (3-1) [8]
                    ~ 9 : take 2     [8]
                    ~ 9 : 8 : take 1 []
                    ~ 9 : 8 : []
                    ~ [9, 8]
    

答案 1 :(得分:2)

递归它是将给定函数应用于给定集合的策略。您将该函数应用于集合的第一个元素,而不是将该过程重复到集合的其余元素。

让我们举一个例子,你想要将列表中的所有整数加倍。首先,您认为我想要应用的功能是什么?答案 - &gt; 2*,现在你必须递归地应用这个函数。让我们称之为apply_rec,所以你有:

apply_rec (x:xs) = (2*x)

但这只会改变第一个元素,你想要改变集合上的所有元素。因此,您必须将apply_rec应用于其余元素。因此:

apply_rec (x:xs) = (2*x) : (apply_rec xs)

但你现在有问题。 apply_rec什么时候结束?到达列表末尾时结束。换句话说[],所以你必须定义这个新的前提。

apply_rec [] = []
apply_rec (x:xs) = (2*x) : (apply_rec xs)

当您到达最后,您不想应用任何功能,因此您定义了此行为。因此,当结束时,函数apply_rec应该“返回”[]

让我们在set = [1,2,3]中看到这个函数的行为。

  1. apply_rec [1,2,3] = (2 * 1) : (apply_rec [2,3])
  2. apply_rec [2,3] = 2 : ((2 * 2) : (apply_rec [3]))
  3. apply_rec [3] = 2 : (4 : ((2 * 3) : (apply_rec []))
  4. apply_rec [] = 2 : (4 : (6 : [])))
  5. 产生[2,4,6]

    可能,因为你不太了解递归,最好的事情是从你提出的简单例子开始。并且通过自己学习,即使需要数小时。

    还要看看learn recursionHaskell Tutorial 3 - recursion

答案 2 :(得分:2)

你问的是“思维过程”,大概是程序员而不是电脑,对吗?所以这是我的两分钱:

考虑用递归编写一些函数g的方法是, 想象 你已经编写了该函数 即可。就是这样。

这意味着你可以在需要的时候使用它,而且它“会做”它应该做的任何。所以,只需写下 是什么制定必须遵守的法律 ,写下您对它的了解。说出关于它的某事

现在,只说g x = g x没有说什么。当然这是真的,但这是一个毫无意义的重言式。如果我们说g x = g (x+2)它不再是重言式,反正无意义。我们需要说一些更明智的事情。例如,

g :: Integer -> Bool
g x | x<=0 = False
g 1 = True
g 2 = True

这里我们说某事。此外,

g x = x == y+z  where
          y = head [y | y<-[x-1,x-2..], g y]    -- biggest y<x that g y
          z = head [z | z<-[y-1,y-2..], g z]    -- biggest z<y that g z

我们是否已经说过关于x的所有话题?无论我们是否做过,我们都会说任何 x。这就结束了我们的递归定义 - 一旦所有可能性都耗尽,我们就完成了

终止 呢?我们希望从我们的函数中获得一些结果,我们希望它完成它的工作。这意味着,当我们使用它来计算x时,我们需要 确保我们使用 递归地使用已定义的y “before” x,即 “更接近”我们所拥有的最简单的案例之一

在这里,我们做到了。现在我们可以惊叹于我们的手工作品,

filter g [0..]

最后一件事是,为了理解定义,不要试图回溯其步骤。只需阅读方程本身。如果我们看到g的上述定义,我们将其简单地理解为:g是一个数字的布尔函数,对于1和2,True,并且对于任何x > 2,它是前两个g数字的总和。

答案 3 :(得分:0)

看功能3:

reverse' [] = []  
reverse' (x:xs) = reverse' xs ++ [x] 

让我们说你反向'[1,2,3]然后......

1. reverse' [1,2,3] = reverse' [2,3] ++ [1]

   reverse' [2,3] = reverse' [3] ++ [2] ... so replacing in equation 1, we get:

2. reverse' [1,2,3] = reverse' [3] ++ [2] ++ [1]

   reverse' [3] = [3] and there is no xs ...

  ** UPDATE ** There *is* an xs!  The xs of [3] is [], the empty list. 

   We can confirm that in GHCi like this:

     Prelude> let (x:xs) = [3]
     Prelude> xs
     []

   So, actually, reverse' [3] = reverse' [] ++ [3]

   Replacing in equation 2, we get:

3. reverse' [1,2,3] = reverse' [] ++ [3] ++ [2] ++ [1]

   Which brings us to the base case: reverse' [] = []
   Replacing in equation 3, we get:

4. reverse' [1,2,3] = [] ++ [3] ++ [2] ++ [1], which collapses to:

5. reverse' [1,2,3] = [3,2,1], which, hopefully, is what you intended!

也许你可以尝试与其他两个做类似的事情。选择小参数。
成功!

答案 4 :(得分:0)

也许您提出问题的方式并不是好的,我的意思是,这不是通过研究现有递归函数的实现,您将了解如何复制它。我更喜欢为您提供另一种方式,它可以作为一个有条理的过程来查看,它可以帮助您编写递归调用的标准框架,然后促进对它们的推理。

你的所有例子都是关于列表的,然后当你使用list时的第一个东西是详尽的,我的意思是使用模式匹配。

rec_fun [] = -- something here, surely the base case
rec_fun (x:xs) = -- another thing here, surely the general case  

现在,基本情况不能包含递归,否则你肯定会以无限循环结束,然后基本情况应该返回一个值,掌握这个值的最好方法是查看函数的类型注释。

例如:

reverse :: [a] -> [a]

可以鼓励您将基本情况视为类型[a]的值,将[]视为反向

maximum :: [a] -> a 

可以鼓励您将基本情况视为最大类型a的值

现在对于递归部分,正如所说的,函数应该包括她自己的调用。

rec_fun (x:xs) = fun x rec_fun xs 

有趣地表示使用另一个负责实现递归调用链接的函数。为了帮助您的直觉,我们可以将其作为操作员呈现。

rec_fun (x:xs) = x `fun` rec_fun xs 

现在考虑(再次)你的函数的类型注释(或者更简单的基本情况),你应该能够推断出这个运算符的本质。反过来,因为它应该返回一个列表,操作符肯定是串联(++)等等。

如果你将所有这些东西放在一起,那么最终得到所需的实现应该不会那么困难。

当然,和任何其他算法一样,你总是需要思考一点,没有神奇的食谱,你必须思考。例如,当您知道列表尾部的最大值时,列表的最大值是多少?

答案 5 :(得分:0)

我也总是发现很难递归思考。反复浏览http://learnyouahaskell.com/递归一章,然后尝试重新实现他的重新实现为我巩固了它。同样,通常,通过仔细地学习Mostly Adequate Guide并练习currying和composition来学习功能编程,这使我专注于解决问题的核心,然后以其他方式应用它。

返回递归...基本上是考虑递归解决方案时要经过的步骤:

  1. 递归必须停止,因此请考虑一个或多个基本情况。在这种情况下,不再需要对该函数进行进一步的调用。
  2. 考虑最简单的非基本情况(递归情况),并考虑如何以导致基本情况的方式再次调用该函数...函数不会一直调用自身。关键在于最简单的非基础案例。这将帮助您解决问题。

因此,例如,如果必须反转列表,则基本情况将是一个空列表或一个元素的列表。转到递归情况时,不要考虑[1,2,3,4]。相反,请考虑最简单的情况([1,2])以及如何解决该问题。答案很简单:抓住尾巴并附加头部即可得到相反的结果。

我不是haskell专家...我刚刚开始学习自己。我从这个可行的开始。


reverse' l
  | lenL == 1 || lenL == 0 = l
  where lenL = length l
reverse' xs ++ [x]

警卫检查它是1还是0长度的列表,如果是则返回原始列表。

当列表的长度不为0或1时,递归的情况就会发生,并且得到尾部的反面,并追加头部。直到列表长度为1或0,然后您得到答案为止。

然后我意识到您不需要检查单例列表,因为一个元素列表的尾部是一个空列表,我去过了,这就是learnyouahaskell中的答案:


reverse' :: [a] -> [a]
reverse' [] = []
reverse' (x:xs) = reverse' xs ++ [x]

我希望能有所帮助。归根结底,练习是完美的,所以继续尝试递归地解决一些问题,您会成功的。