- 之前很少接触递归,最近在学习《算法4》一书。该书课后习题要求使用递归和迭代方式解决问题,本人觉得迭代方式更加符合正常的逻辑思维。应要求不得不编写递归方式,不过在专研递归时,让我产生新的思路,让我对递归产生一种又爱又恨的感觉。
- 本书的104页练习题1.3.30
编写一个函数,接受一条链表的首节点作为参数,将链表反转并返回结果链表的首结点
- 首先来看迭代版本的思路,将原链表看做两个链表,一个是正序链表一个是反序链表。首先搞清楚怎样的顺序才算作反转。请看下图
- 由于使用的链表为单向链表没有
pre
,所以不能从尾部开始,依次将second.next = first;
,无法保存前一个结点的信息。所以出现如图所示的思路,将整个链表分作两部分。reverse
始终指向指向反序链表的首部,first
和second
分别指向正序链表的尾部。正序的first
结点指向reverse
,reverse
结点向前移动(reverse
始终指向反序链表的首结点),正序部分的first
,second
结点后移一位。代码如下
public Node reverse(Node x){
Node first = x;
Node reverse = null;
while(first != null){
//记录等待反转操作链表首节点的下一个结点
Node second = first.next;
//reverse为反转链表的首节点
//将要插入反转链表的结点指向当前反转链表首结点
first.next = reverse;
//首节点插入之后reverse结点向前移动
reverse = first;
//等待反转操作的链表首节点
first = second;
}
return reverse;
}
- 回到主题—递归。受到迭代版思路的影响,还是第一个想到将整个链表分成两部分(正序和反序),花费了一天时间,想破脑袋也没想通代码的写法。首先考虑我们要返回的是下一个结点还是返回second.next = first的关系。如果要返回下一个结点,那我们没有必要使用递归,直接使用first.next返回即可。如果返回两个结点之间的关系,就我目前水平不能实现。给出课本自带的实现代码
public Node reverse_recursive(Node first){
if(first == null) return null;
if(first.next == null) return first;
Node second = first.next;
Node rest = reverse_recursive(second);
second.next = first;
first.next = null;
return rest;
}
- 如果是按照迭代版思路,那么一定会卡在寻找反序首节点和前两个结点与第三个结点之间没有关联等问题,这也有由于本人对递归根本不熟悉的原因。
- 在使用IDEA进行调试递归代码时,发现每个迭代过程都有自己的递归栈。如下图
- 上图栈**[6]**中保存最后一个结点,我们可能会产生一个思路
[6].next = [5],[5].next = [4] ...
。这是一个不错的思路,但是如何编写代码。结合上面给出的课本参考代码,发现rest
始终作为反序链表首节点。真正的子问题是second.next = first; first.next = null;
可以很容易明白原本结点first
指向结点second
,现在改为结点second
指向结点first
。但是第二个结点如何与第三个结点建立关系的?观察递归栈,递归栈中保存着first,second
等局部变量。举个例子,第5层栈中保存的second
变量就是第6层first
变量。那么层与层之间的关联不就建立起来了嘛。下面是示意图 - 细细品味上面那段代码,越发的觉得其实现算法的精妙。它巧妙地运用了递归栈保存变量信息,并且每层的变量之间不会产生影响。迭代的思路也很清奇,它进行递归并不会产生一个新的结果(rest始终为反序的首节点),只是为了让
first
和second
结点之间的指向反转,并利用递归栈性质建立两个结点与第三个结点之间的关系。 - 哈哈,刚刚接触到算法,可能是本人少见多怪吧。