1.带虚拟头结点的链表反转
我的思考:
首先我们需要一个指针来遍历链表.
反转的过程就是把从头开始的当前结点(cur),变成头结点( new head),接到之前的节点上。
此时我们用一个虚拟节点(ans)指向头结点(head),来建成这一个新链表。
头结点(head)就是新链表的尾结点。
那么我们就只需要把旧链表的第二个结点之后的结点,插到虚拟节点(ans)后面就行,然后调整一下next指针域(cur.next)。
想到这边大体框架有了,但是在写循环体的时候发现一个问题,插入节点我首先处理单链表结点的后继指向问题,但是一旦更改目标结点(cur)的next指针,我们就找不到下一个要反转的结点,所以还需要在每一次反转中用一个局部变量(savenext)保存下一个结点的位置。
代码:
class Solution {
public ListNode reverseList(ListNode head) {
ListNode cur = head.next;
ListNode ans = new ListNode(-1);
ans.next = head;
while(cur!=null)
{
ListNode savenext = cur.next;
cur.next=ans.next;
ans.next=cur;
cur=savenext;
}
return ans.next;
}
}
但是因为我非常菜所以报错了,因为这里没有考虑一个问题那就是如果链表为空或者只有一个头结点怎么办?head.next就可能不存在,编译器不允许这样的情况出现。所以唯一的办法就是cur指向head,那这样的话,ans就需要在循环体中接上head。
正确代码:
class Solution {
public ListNode reverseList(ListNode head) {
ListNode cur = head;
ListNode ans = new ListNode(-1);
while(cur!=null)
{
ListNode savenext = cur.next;
cur.next=ans.next;
ans.next=cur;
cur=savenext;
}
return ans.next;
}
}
2.不带虚拟头结点的链表反转
我的思考:
从上述带虚拟头结点的链表反转中不难发现有一个很明显的特点:
创建了一个新的链表,然后把原来的搬运过去了。
现在要求我们不带虚拟头结点,如果不带虚拟头结点是不是就意味着我们不能创建一个新链表,而是直接在原来的链表上直接操作呢?好像可以试试。
假设我们当前要修改的结点指针为cur,首先要保存当前后继结savenext。
cur肯定是指向上一个结点,上个结点的位置也要保存,不然找不到,用pre表示。
那么当cur不为空的时候,我们就要将cur指向上一个结点,然后移动cur指针
问题来了,前后都有指针,cur一开始指向谁?head还是head.next?
savenext为空时怎么办?
head。为空时看看能不能指向null
因为cur表示需要修改指向的结点,第一个就需要,所以pre一开始只能指向null,因为如果也指向head就会形成一个死循环。
理论成立,尝试代码:
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while(cur!=null)
{
ListNode savenext = cur.next;
cur.next = pre;
pre = cur;
cur = savenext;
}
return cur;
}
}
haha,不出意料的报错了,还是太菜了。但是这一算法中我感觉到了几个比较重要的概念。
- 辅助指针,辅助内存的作用范围是局部的还是全局的,决定了定义的位置。比如说是循环体内部还是外部。
- 变量的初始值
- 循环的结束条件
- 返回值是哪一个
现在我们再来看一看错误,这个测试返回了一个null,说明cur最后指向null了,为什么呢?
因为循环体的最后 cur = savenext ,而savenext必定会在最后指向一个null值,可以说是考虑不周了。
再看一遍循环体,我们可以发现其实没有问题,还有一句pre = cur。
也就是说,在下一次开始之前,我们已经把pre指针移动到了,cur原来的位置,也就是已经都整理完的位置,cur的位置是下一个目标的位置,所以应该返回 pre 。
测试一下,过了。hhh
3.拓展
PS 这一部分内容我没有详细题目,所以直接以概念形式COPY出来
反转的递归实现
提到递归我直接就有一点想法。
很简单通过迭代,一直移动head指针,我们直接能够找到最后一个结点。
但是如果想要反转,不可避免我们需要对结点进行操作。
递归过程中,有一个和二叉树遍历相似的地方,那就是,操作在迭代中进行还是在回归中(自行意会)进行。
很显然,修改指针方向需要从尾向头改,所以是回归的时候进行,这意味着我们需要把修改结点操作放在内部那个函数的下方
比如这样一段伪代码:
public List reverse(List head){
回归条件;//肯定放在前面,不然都执行不到结束,只会执行到报错
reverse(head.next);
结点操作:把下一个结点指向这个结点,这个节点指针域不要了,指null
}
于是乎,有一个问题,结点操作是操作下一个结点,那么下一个结点为空,也就是这个结点指针为空的时候开始回归。
还有个问题,如果本来就是空的肯定也直接回归。
返回一个当前节点,也就是head
代码:
public ListNode reverseList(ListNode head){
if(head == null || head.next == null){
return head;
}
reverseList(head.next);
head.next.next=head;
head.next =null
}
ok,不出意外由于我长时间没有复习Java,报错了。
提示我最后没有返回值。(我看了一遍我真菜)
加上了return head;
致命问题来了,我返回的是当前结点,而不是反转链表的头指针。
头指针都没有。
哪里去放头指针呢,关键问题。
OK,修改单链表最重要的问题是什么?存放下一个结点,将迭代结点保存为新节点就能得到一个新节点的指针了,oi。
代码:
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode newhead = reverseList(head.next);
head.next.next=head;
head.next =null;
return newhead;
}
}