LeetCode 206
给你单链表的头结点 head,请你反转链表,并返回反转后的链表。
示例1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
本题有两种方法,带头结点和不带头结点,我们都应该会,因为这两种方式都很重要,如果搞清楚,很多链表的算法题就不用做了。
1.建立虚拟头结点辅助反转
前面分析链表插入元素的时候,会发现如何处理头结点是个比较麻烦的问题,为此可以先建立一个虚拟的结点ans,并且令ans.next=head,这样可以很好的简化我们的操作。如下图所示,如果我们将链表{1->2->3->4->5}进行反转,我们首先建立虚拟结点ans,并令ans.next=node(1),接下来我们每次从旧的链表拆下来一个结点接到ans后面,然后将其他线调整好就可以了。
如上图所示,我们完成最后一步之后,只要返回 ans.next 就得到反转的链表了。
代码:
public static ListNode reverseList(ListNode head){
ListNode ans = new ListNode(-1);
ListNode cur = head;
while (cur != null){
ListNode next = cur.next; // 2 3 4 5 // 3 4 5 // 4 5 // 5 // null
cur.next = ans.next; // 1 // 2 1 // 3 2 1 // 4 3 2 1 // 5 4 3 2 1
ans.next = cur; // 1 // 2 1 // 3 2 1 // 4 3 2 1 // 5 4 3 2 1
cur = next; // 2 3 4 5 // 3 4 5 // 4 5 // 5 // null
}
return ans.next; // 5 4 3 2 1
}
建立虚拟结点是处理链表的经典方法之一,虚拟结点在很多工具的源码里都有使用,用来处理链表反转也比较好理解,因此我们必须掌握好。
2.直接操作链表实现反转
上面的方式虽然好理解应用也广,但是可能会被面试官禁止,为啥?原因是不借助虚拟结点的方式更难,更能考察面试者的能力。
我们观察一下反转前后的结构和指针位置:
我们再看一下执行期间的过程示意图,在图中,cur本来指向旧链表的首节点,pre表示已经调整好的新链表的表头,next是下一个要调整的。注意图中箭头方向,cur和pre是两个表的表头,移动过程中cur经过一次中间状态之后,又重新变成了第二个链表的表头。
public static ListNode reverseList2(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next; //① 2 3 4 5 //② 3 4 5 //③ 4 5 //④ 5 //⑤ null
curr.next = prev; //① 1 //② 2 1 //③ 3 2 1 //④ 4 3 2 1 //⑤ 5 4 3 2 1
prev = curr; //① 1 //② 2 1 //③ 3 2 1 //④ 4 3 2 1 //⑤ 5 4 3 2 1
curr = next; //① 2 3 4 5 //② 3 4 5 //③ 4 5 //④ 5 //⑤ null
}
return prev;
}
将上面这段代码在理解的基础上背下来,因为这个算法太重要。
3.扩展
通过递归来实现反转,链表反转还有第三种常见的方式,使用递归来实现,这里不做重点,感兴趣的同学可以研究一下:
public ListNode reverseList3(ListNode head){
if (head == null || head.next == null){
return head;
}
ListNode newHead = reverseList3(head.next);
head.next.next = head;
head.next = null;
return newHead;
}