【LeetCode難題解題思路(Java版)】25. k個一組翻轉鏈表

首先看一下題目描述:

給出一個鏈表,每 k 個節點一組進行翻轉,並返回翻轉後的鏈表。

k 是一個正整數,它的值小於或等於鏈表的長度。如果節點總數不是 k 的整數倍,那麼將最後剩餘節點保持原有順序。

示例 :

給定這個鏈表:1->2->3->4->5

當 k = 2 時,應當返回: 2->1->4->3->5

當 k = 3 時,應當返回: 3->2->1->4->5

說明 :

你的算法只能使用常數的額外空間。
你不能只是單純的改變節點內部的值,而是需要實際的進行節點交換。

再來看一下輸入輸出的要求:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        
    }
}

注意到的關鍵點:
1.常數的額外空間,不是說只能使用基本類型的額外空間,而是讓你用盡量少的對象空間
2.一定要做結點交換,其實就是重新安排指針。
3.給出的是個單向鏈表,所以在設計遞進單元,指針重定向的時候,要注意指針位置的保存。不然可能會丟掉指針導致遞進失敗。(這個問題在大四刷數據結構題的時候吃了不少虧。)
遞進單元設計
先不考慮其他干擾因素,只考慮對長度爲k的鏈表的反轉問題。我想到的是兩種方法:
第一種,頭插法。其思想見圖。
單鏈表頭插法反轉
這種方法,相當於建立另外一個鏈表–L的頭,然後,將要處理的列表遞進處理,一個一個的插入新鏈表,最後返回新L.next即可。
然後再考慮一下多個這樣的處理單元(n個結點,k個爲一個單元)合併的事情,每次處理完這樣一個單元后,要拿到兩個結點纔可以進行合併,第一就是L,這個是本來就有的,通過L.next可以獲取到單元鏈表的頭,第二就是最後一個結點,其實最後一個結點就是最開始插入的那個結點,所以每次處理的時候,要把它保存一下。
第二種,直接指針反轉。
直接反轉
因爲是單鏈表,爲了防止指針結點丟失,我們至少需要三個指針來進行操作,以及遞進。首先紅色的指針代表當前結點,綠色的代表上個結點,藍色的代表下一個結點,那麼在每個操作單元要做的事情就是:

next=cur.next;//首先獲取到遞進的下一項防止結點丟失
cur.next=prev;//處理當前結點的指針,它本來應該指向下一個的,現在讓它指向前一個
prev=cur;//前進
cur=next;//注意這裏,不是cur=cur.next,因爲cur的next已經重新指向前一位了,它的下一位已經丟失,所以第一步先把它的下一位用next保存下來了

當cur處理完最後一個結點的時候(第k個),prev指向最後一個結點,next指向下一個結點。
進行鏈表拼接的時候,也需要兩個結點,首先頭結點就是prev,而尾結點同樣是處理的第一個結點,那麼也需要記錄一下第一個結點。

碼代碼
第一種思想的實現:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode list=new ListNode(0);//永遠表示頭插法要插入的頭結點
        ListNode return_list=list;//最終要返回的鏈表頭
        ListNode Start=new ListNode(0);//每次處理反轉時,開始的位置
        ListNode End=new ListNode(0);//每次處理反轉時,結束的位置
        int count=0;//計數
        while(head!=null){//對要處理的鏈表按k個一組遞進處理
            count=0;
            Start=head;
            End=Start;//開始結束指針的初始化
            while(End!=null&&count<k){
                End=End.next;
                count++;
            }//首先判定一下當前組是否有k個
            if(count==k){//有k個則進行處理
                list.next=head;//因爲遇到list.next=null的問題,好像不能tmp=list.next這樣去賦值,所以就手動給它先加一個結點
                head=head.next;//手動遞進一位
                for(int i=1;i<k;i++){//因爲已經加了一個結點,所以i從1開始,
                    ListNode tmp=list.next;
                    list.next=head;
                    head=head.next;
                    list.next.next=tmp;//頭插法
                }
                Start.next=null;//在這裏必須清空頭數字的指針,使其變成真正的尾部
                list=Start;//Start裏保存的就是尾部,而list要永遠是頭插法要插入的頭,所以要讓list定位到Start開始下一輪處理,這裏可以這麼理解,list一次往後跳k個結點,每次處理完之後,都跳到鏈表的最後,以便下一輪在其後面插入結點
               
            }else{//如果不足k個,直接鏈接即可
                list.next=Start;
                break;
            }
            
        }
       return return_list.next; //因爲list是往後跳的,但是return_list是保存着list的最開始的位置的,所以返回的時候要返回return_list;
    }
}

第二種的實現:
PS:參考了評論區的遞歸的思想。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        
        ListNode prev=null;
        ListNode cur=head;
        ListNode S=head;//和前一個思想一樣,先用它遞進看一下是不是有k個結點
        ListNode next=null;
        int count=0;
        int i=0;
        while(S!=null&&count<k){
            S=S.next;
            count++;
        }
        if(count==k){
            while(i<k&&cur!=null){//遞歸的末端1
                next=cur.next;
                cur.next=prev;
                prev=cur;
                cur=next;
                i++;//這裏學會了用prev來保存前一變量,
            }
            if(next!=null){//遞歸的傳遞端
                head.next=reverseKGroup(next,k);//在這裏優化成了遞歸
            }
            return prev;//最終返回
            
        }else{
            return head;//遞歸的末端2
        }
        
    }
}

複雜度總結:
首先可以看到關鍵處理過程是一遍掃描,之前又有一次預掃描,所以時間複雜度應該是O(2n)。
最後
點擊提交,看下成績~
成績

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章