反转链表(206)
这是一道经典的面试题,它在leetcode里属于easy级别. 刷题多的人都知道,链表或数组类的题要下意识的反应出两个信息: 双指针,哨兵. 双指针的思想跟快速排序有点类似, 用两个指针在链表或数组中进行各种骚操作.
不用哨兵
这道题也是用了哨兵和双指针. 其实一开始我有点小倔强, 想着不用哨兵只用双指针看能不能写出来. 虽然写出来,但也费了一番功夫,因为被里面的一个小坑给绊住了. 下面说一下我自己给自己刨的坑
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null || head.next==null) return head;
ListNode cur=head;
ListNode next=cur.next;
while(next!=null){
ListNode tmpNext=next.next;
next.next=cur;
cur=next;
next=tmpNext;
}
return cur;
}
}
上面的代码不复杂: 先声明两个指针cur和next. 然后在循环中先把next.next保存下来,因为我们要保证循环能正常进行. 保存下来后我们就可以将next.next指向cur,这表示将下一个节点的next指向了前面节点. 比如我们有如下链表:
1 --> 2 --> 3 --> 4 -->5
| |
cur next
执行完上面两行后,此时就变成了如下状态:
1 <-- 2 3 --> 4 -->5
| |
cur next
大家发现此时2节点的next已经指向了1,这正是我们希望的结果.但此时2,3节点已经缘尽了, 那怎么往下循环呢? 没关系,因为我们通过ListNode tmpNext=next.next;已经把3节点保存下来了,所以可以通过以下方式正常循环下去:
cur=next;
next=tmpNext;
但此时的坑也已刨好了, 运行上面的代码,leetcode会提示链表中有循环, 链表出现循环后你在遍历时就会进入无限循环,就像JDK1.7种的hashcode,在并发行情况下扩容可能出现的问题.
为什么会这样呢? 细心的读者可能已经发现了, 将2节点指向1节点这块没问题,但是1节点还保留着对2节点的引用. 那怎么办? 简单,将1节点的next置空不就行了吗? 于是想都不想改成如下代码:
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null || head.next==null) return head;
ListNode cur=head;
ListNode next=cur.next;
while(next!=null){
cur.next=null;
ListNode tmpNext=next.next;
next.next=cur;
cur=next;
next=tmpNext;
}
return cur;
}
}
在循环开始时将cur.next置空,其他不变. 然后满心欢喜地提交代码, 结果leetcode告诉你出错了:
input: [1,2,3,4,5]
Output [5,4]
Expected [5,4,3,2,1]
结果只有5,4 剩余的1,2,3跑哪了? 为什么会这样? 我怎么这么笨, 这么简单的题都做不出来, OMG! 我相信刷题的人都有过这种感觉, 遇到一个简单的做了两遍没做出来就开始怀疑人生了! 那怎么办呢? take a deep breath then try again
上面问题出在cur.next=null的位置放的不对, 它会导致下次循环时把已经指向正确位置的cur.next置为空, 这样在循环结束时,只保留了最后两个元素, 倒数第二个之前的节点全部丢失.
那怎么调整呢? 很简单,把cur.next=null 放到循环外面,只执行一次就行. 修改代码如下:
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null || head.next==null) return head;
ListNode cur=head;
ListNode next=cur.next;
cur.next=null;
while(next!=null){
ListNode tmpNext=next.next;
next.next=cur;
cur=next;
next=tmpNext;
}
return cur;
}
}
从上面可以看出, 不使用哨兵也可以实现. 但是引入了复杂性,这样一不小心就会出错. 那作为对比我们看看使用哨兵的代码
使用哨兵
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre=null;
ListNode cur=head;
while(cur != null){
final ListNode next=cur.next;
cur.next=pre;
pre=cur;
cur=next;
}
return pre;
}
}
我们将pre作为哨兵引入,整个代码一下清爽不少! 简单跟无哨兵代码比较一下:
- 不引入哨兵时,
cur指向第一个节点,next指向第二个节点,但是你不知道这个链表会有几节点,所以需要判断一下head是否为空 - 不引入哨兵时,首次循环前需要将
cur.next置空,否则会链表内部出现循环. 而引入哨兵后就无需区分是否首次循环, 所有节点步调一致. 因为哨兵节点本来就没有指向下一个节点的指针,无需额外的置空操作. 所以不要耍一下小聪明,更不要小看前人的智慧.
总结
我们分别使用了无哨兵和有哨兵的方式来实现了链表反转, 并分析了引入哨兵所带来的优势.当然还有其他方法,比如引入一个栈,利用栈的特性将链表反转,但这无疑增加了空间复杂度.我们追求的是极致代码,你懂的!
刷题是一个痛并快乐的事情, 尤其刚开始真的是痛并痛着. 这时一定要沉住气,戒骄戒躁, 一定要根据自己的节奏坚持下去, 这样肯定会有收获.