链表反转的姿势

158 阅读4分钟

众所周知,算法作为互联网民工的大厂敲门砖,不会不行,会了其实也没什么用。本文以个人理解尝试用轻松自然的方式帮助大家理解这个算法题。
本文代码以java为例,阅读时间约20分钟
首先,存在一个链表,如下图所示

图片

现在要对这个链表进行反转,也就是变为

图片

直观地想,反转是交换链表节点的位置,比如1先和2交换,再和3交换,一直换到5为止。可至少要有两个对象,这种交换的行为才能发生。但通常这类题只给一个头节点,怎么办呢?别忘了,这个是链表,我们可以把头节点后面的对象用一个对象引用去指代。目前就具备了交换的条件。

那我们怎么进行交换呢?上面说的1和2交换、2和3交换,类似于冒泡排序,因为以数组的下标作为索引交换时,操作是比较方便的,对于链表我们不这样做。那怎么交换呢,想一想我们的目的,即要把1-->2变成2-->1,就是指向关系变化嘛,我们把原有的指向关系破坏掉不就行了。

交换1和2时,首先1要变成末尾,也就是1要指向null,但这件事我们不能第一步做,因为我们先要知道另一个交换对象2在哪里,2需要利用1去链表上找到。所以先找到2,然后破坏1的指向关系,把1指向null,这时等于空间中存在了两个链表,一个是只有1的链表,一个是以2最为头节点的链表。这时候有人会说,我要反转链表,你整两个链表干啥?你会不会?图片图片图片   别急,这就来了,现在我们把对1的操作给2重复下,先用一个对象引用通过2指向3,然后再把2指向1。这时候空间中的两个链表是2-->1 和以3开头的长链表

就是这样,从结果上看2和1交换了,剩下的就是重复过程了。

class ListNode{   
    int val;   
    public ListNode(int val){     
      this.val = val;   
    }
}
public ListNode reverseList(ListNode head){     
  //newHead是反转中的新链表头节点,初始值为null,逐渐变为1,2...     
  ListNode newhead = null;     
  while(head != null){          
     //确定要破坏的节点          
     ListNode cur = head;          
     //破坏之前先找到后续          
     head = head.next;          
     //开始破坏关系          
     cur.next = newHead;          
     //保持newHead为新链表的头          
     newHead = cur;     
   }     
   return newHead;     
}

放松下,喝杯咖啡,卡布奇诺最好不过了图片图片图片图片图片

对了上面的方法叫头插法,也就是2会插在1之前,3会插在2之前,还有一种方法是尾插法,这时候有人会说,学这种算法就是eat shit,为什么还要学好几种吃法,我深表同意,暂且先按下牢骚,看看这个方法。尾插法我们实际上是分成两个链表去操作的,而头插法只需要使用一个链表,具体是引入一个冗余节点,每次执行固定插入,在冗余节点后的第一个位置将next引用指向的节点插入.

图片

如图所示,dummy.next始终指向的是dummy节点后的第一个节点,next引用指向的是head所指向节点的的后续节点。

图片

上图展示了一次插入过程,因为插入也是交换,肯定要破坏原有的连接关系,但是不能丢失节点数据
步骤1:next要交换,必须先把next的后续节点数据保存起来,我们这里先交给head.next(为什么是head.next,因为head.next指向的节点,已经由next指向了,不会丢失)

步骤2,3:next要插入到dummy.next,这个简单,next节点的后续保存dummy.next原来的值,再把dummy.next指向next,纯粹的链表插入操作。

至此我们完成了一次插入,后续以此类推

public ListNode reverseList(ListNode head){    
    ListNode dummy = new ListNode(-1);    
    dummy.next = head;    
    while(head != null && head.next != null){        
        ListNode next = head.next;        
        head.next = next.next;        
        next.next = dummy.next;        
        dummy.next = next;    
    }    
    return dummy.next;
}

看完上面的,你会了反转链表。缺德的来了,你还需要反转部分链表,看图

图片

看到没,对比初始链表,这里只把2-->3-->4变为了4-->3-->2。你可能会想到,把链表切为三段,第一段不变,反转2到4,第三段不变,最后把这三段拼起来。没错你可以这样做,但这个切分操作遍历一次链表,反转又需要遍历一次。可以这样做,但没必要。实际部分反转你只需要想明白一件事,什么时候开始反转,什么时候结束反转。

想想。。。

你一定会觉得这很简单,因为题目会告诉你反转区间。实际上就这么简单。

public ListNode reverseBetween(ListNode head,int start,int end){    
        ListNode dummy = new ListNode(-1);   
        dummy.next = head;    
        //dummy节点为了利用头插法,指向要反转节点的前一个    
        //pre是为了遍历    
        ListNode pre = dummy;    
        //遍历到指定位置    
        for(int i=0;i<start-1;i++){       
            pre = pre.next;    
        }    
        //这里就是头插法的实现    
        ListNode preHead = pre;    
        ListNode cur = pre.next;    
        for(int i=0;i<end-start;i++){       
            ListNode next = cur.next;      
            cur.next = next.next;       
            next.next = preHead.next;      
            preHead.next = next;    
        }    
        return dummy.next;
}

没想到你会看到这里,嘿嘿,恭喜你浪费了你人生中的20分钟图片图片图片图片