递归是一种通过实践获得的思维方式。所以祝贺你的尝试。这是不正确的,但不要气馁。
这里有两种解决问题的方法。
您的目标是将其细分为一个较小的版本,再加上一个(希望计算简单且快速)增量步骤,该步骤将较小版本的解决方案变为完整的解决方案。这就是递归思维的精髓。
第一次尝试:将列表视为头部元素加上“列表的其余部分”。即,
L = empty or
= h . R
其中h 是头元素R 是列表的其余部分,点. 将新元素加入列表。反转此列表包括反转R,然后在末尾附加h:
rev(L) = empty if L is empty
= rev(R) . h otherwise
这是一个递归解决方案,因为我们可以递归调用 reverse 函数来解决反转 R 的稍微小一点的问题,然后添加一点工作来追加 h,这样我们就得到了完整的解决方案。
这个公式的问题是附加h 比你想要的要贵。因为我们有一个只有一个头指针的单链表,所以很耗时:遍历整个链表。但它会正常工作。在 C 语言中是:
NODE *rev(NODE *head) {
return head ? append(head, rev(head->next)) : NULL;
}
NODE *append(NODE *node, NODE *lst) {
node->next = NULL;
if (lst) {
NODE *p;
for (p = lst; p->next; p = p->next) /* skip */ ;
p->next = node;
return lst;
}
return node;
}
那么如何摆脱糟糕的表现呢?通常情况下,问题的不同递归公式具有不同的效率。所以经常会涉及到一些试验和错误。
下一次尝试:考虑将列表划分为两个子列表的计算:L = H T,因此 rev(L) = rev(T) + rev(H)。这里加上+ 是列表连接。关键是如果我知道rev(H) 并且想在其头部添加一个新元素,那么要添加的元素是T 中的first 元素。如果这看起来很模糊,令 H = [a, b, c] 和 T = [d, e]。那么如果我已经知道 rev(H) = [c, b, a] 并且想在头部添加下一个元素,我想要 d,它是 T 的第一个元素。在我们的小符号中,你可以写下这个观察就这样:
rev(H + (d . T)) = rev(T) + ( d . rev(H) )
所以这看起来非常好。在这两种情况下(获取 T 的头部并将其移动到 rev(H) 的头部),我只对列表的头部感兴趣,这是非常有效的访问。
当然,如果 T 为空,则 rev(H) = rev(L)。这就是答案!
把它写成递归过程。
NODE *rev(NODE *t, NODE *rev_h) {
if (t) { // if t has some elements
NODE *tail = t->next; // save the tail of T
t->next = rev_h; // prepend the head to rev(H)
return rev(tail, t); // recur to solve the rest of the problem
}
return rev_h; // otherwise T is empty, so the answer is rev(H)
}
一开始,我们对 rev(H) 一无所知,所以 T 是整个列表:
NODE *reversed_list = rev(list, NULL);
接下来要注意的是这个函数是尾递归的:递归调用在函数返回之前执行。这很好!这意味着我们可以轻松地将其重写为循环:
NODE *rev(NODE *t, NODE *rev_h) {
recur:
if (t) { // if t has some elements
NODE *tail = t->next; // save the tail of T
t->next = rev_h; // prepend the head to rev(H)
rev_h = t; // "simulate" the recursive call
t = tail; // by setting both args
goto recur; // and going back to the start
}
return rev_h; // otherwise T is empty, so the answer is rev(H)
}
您始终可以使用尾递归调用来进行这种转换。您应该认真思考为什么会这样。
现在goto 很容易被重写为while 循环,我们可以将rev_h 设为初始化为NULL 的局部变量,因为这就是初始调用所做的全部:
NODE *rev(NODE *t) {
NODE *rev_h = NULL;
while (t) { // while t has some elements
NODE *tail = t->next; // save the tail of T
t->next = rev_h; // prepend the head to rev(H)
rev_h = t; // "simulate" the recursive call
t = tail; // by setting both args
}
return rev_h; // otherwise T is empty, so the answer is rev(H)
}
一个只需要少量固定空间的就地链表逆向器!
然后看!我们从来不用画有趣的方框图和箭头图,也不必考虑指针。它“刚刚发生”是通过仔细推理如何将问题细分为更小的自身实例,即递归的本质。这也是一种很好的方式,可以看出循环只是一种特殊的递归。很酷,不是吗?