我很难理解如何以递归方式思考问题,并使用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]
答案 0 :(得分:18)
当试图理解递归时,您可能会发现更容易思考算法对于给定输入的行为。很容易挂断执行路径的样子,所以请问自己这样的问题:
或者,对于数字的递归:
递归算法的结构通常只是覆盖上述情况的问题。因此,让我们看看您的算法如何表现出来以了解这种方法:
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]
此函数引入了对整数参数和列表的递归,因此有两种基本情况。
如果我们采取0或更少的项目会怎样?我们不需要任何物品,所以只需返回空列表。
take' n _ | n <= 0 = []
take' -1 [1] = []
take' 0 [1] = []
如果我们传递空列表会怎样?没有更多的项目,所以停止递归。
take' _ [] = []
take' 1 [] = []
take -1 [] = []
算法的核心在于走下列表,拉开输入列表并减少要采用的项目数,直到上述任何一种基本情况停止进行。
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]
中看到这个函数的行为。
apply_rec [1,2,3] = (2 * 1) : (apply_rec [2,3])
apply_rec [2,3] = 2 : ((2 * 2) : (apply_rec [3]))
apply_rec [3] = 2 : (4 : ((2 * 3) : (apply_rec []))
apply_rec [] = 2 : (4 : (6 : [])))
产生[2,4,6]
。
可能,因为你不太了解递归,最好的事情是从你提出的简单例子开始。并且通过自己学习,即使需要数小时。
答案 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,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]
我希望能有所帮助。归根结底,练习是完美的,所以继续尝试递归地解决一些问题,您会成功的。