五一刷刷题(一)

326 阅读4分钟

反转链表(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;
        
    }
}

上面的代码不复杂: 先声明两个指针curnext. 然后在循环中先把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作为哨兵引入,整个代码一下清爽不少! 简单跟无哨兵代码比较一下:

  1. 不引入哨兵时,cur指向第一个节点,next指向第二个节点,但是你不知道这个链表会有几节点,所以需要判断一下head是否为空
  2. 不引入哨兵时,首次循环前需要将cur.next置空,否则会链表内部出现循环. 而引入哨兵后就无需区分是否首次循环, 所有节点步调一致. 因为哨兵节点本来就没有指向下一个节点的指针,无需额外的置空操作. 所以不要耍一下小聪明,更不要小看前人的智慧.

总结

我们分别使用了无哨兵和有哨兵的方式来实现了链表反转, 并分析了引入哨兵所带来的优势.当然还有其他方法,比如引入一个栈,利用栈的特性将链表反转,但这无疑增加了空间复杂度.我们追求的是极致代码,你懂的!

刷题是一个痛并快乐的事情, 尤其刚开始真的是痛并痛着. 这时一定要沉住气,戒骄戒躁, 一定要根据自己的节奏坚持下去, 这样肯定会有收获.