(* val rev : ’a list -> ’a list -> ’a list *)
let rec rev l r =
match l with
[] -> r
| (h::t) -> rev t (h::r)
有人可以告诉我这里递归发生了什么吗? 另外,为什么在代码中使用了两个参数l和r?
让我们说我要反转[1; 2; 3]该函数如何反转为3,2,1?
rev [1;2;3] []
答案 0 :(得分:2)
r
详细说明,让我们从没有r
(累加器)的函数开始:
let rec rev l =
match l with
[] -> l
| h :: t -> rev t @ [h]
这里效率低下的原因不仅仅是因为它不是尾递归(我不会详细介绍;请参阅下面的链接),还因为你必须在元素末尾附加一个元素[h]
每次通话的每个反向子列表。在Ocaml中,与前缀相比,附加到列表是低效的,因为列表只是一个单链接列表,您只能访问头部的指针。
将新元素添加到列表的头部只需要一个新创建的元素来创建指向前一个head元素的指针,并返回一个新创建的列表,并将该新元素作为头部。这只是O(1)操作。
然后,将一个元素附加到列表会产生O(N)复杂性,因为在为最后一个元素指向新元素创建新指针之前,必须遍历到列表的末尾。如果你有一个长度为N
的列表,其中N
是一个巨大的数字,那么O(N)非常糟糕。
使用累加器(r
),您正在“累积”或修改状态并进入下一个函数调用。这是尾递归的基础,但也避免了上述陷阱。要查看它的确实功能,请参阅下面的伪代码。
递归函数的作用
以下是递归操作继续进行时的可视化表示(第一个参数为l
,第二个参数为r
)。
(* pseudocode *)
(* while List.length l <> 0 *)
rev [1; 2; 3; 4] []
rev [2; 3; 4] (1 :: [])
rev [3; 4] (2 :: [1])
rev [4] (3 :: [2; 1])
rev [] (4 :: [3; 2; 1])
(* List.length l = 0 *)
return [4; 3; 2; 1]
在实践中,您可能希望创建一个内部“帮助器”函数来处理递归繁重,并将外部函数简单地保留为用户友好的单一API。
let rev l =
let rec helper l' r =
match l' with
[] -> r
| (h::t) -> helper t (h::r)
in helper l []
this article on recursion I wrote可能会帮助您更好地理解尾递归。
答案 1 :(得分:1)
此函数是递归的,因为在其内部调用了rev
。 r
参数在这里所谓的累加器 - 多亏了它,这个函数是尾递归。
它的细节如何运作?
rev [1;2;3] []
,l=[1;2;3]
和r=[]
。 L与h::t
匹配 - 这意味着列表的头部和尾部。 h=1
,t=[2;3]
。 rev
函数正文中的最后一次调用是rev [2;3] [1]
l
与之前匹配,因此使用rev
调用[3] [2;1]
rev [] [3;2;1]
。 L
与[]
匹配,并返回r
(=[3;2;1]
)。 请注意,您可以通过以下方式隐藏累加器:
let rev l =
let rec rev l r = match l with
[] -> r
| (h::t) -> rev t (h::r)
in
rev l []
然后你可以使用这个函数只传递一个参数rev [1;2;3]
。