有如下連結串列:
要求對連結串列進行反轉,反轉後的連結串列如下:
題目解析
反轉連結串列,就是將連結串列中每一個節點的 next 引用指向其前驅節點。連結串列預設自帶一個引用,這個引用指向了頭節點,記為 n1。首先嚐試將 n1 的 next 引用進行反轉:
可以發現,① 的 next 引用指向了空,由於 ① 切斷了指向 ② 的引用,導致 n1 無法移動到 ② 和 ③,此時可以再引入一個引用,記為 n2,n2 指向 ②:
對 ② 進行反轉:
這時候 ③ 丟失了,是否可以複用現有的引用來訪問到 ③ 呢,答案是不行的。 ② 的前驅節點需要通過 n1 來訪問,此時需再引入一個新的引用 n3,來指向 ③:
對 ③ 進行反轉:
這時候三節點連結串列就完成了反轉,題目到這是否就分析結束了呢?再引入一個節點 ④,如圖:
不難發現,④ 節點又丟失了。再思考,能否複用現有引用,來訪問到 ④,光從上圖的結果來看,是不行的,一旦一個節點完成了反轉,其後繼節點就丟失了,除非建立與連結串列節點數量一致的引用,每一個引用指向其中一個節點,然後按上述方式對每個節點完成反轉。這種方式顯然不夠優雅,那能否在反轉下一個節點之前,先將引用後移,再反轉呢?
接下來我們嘗試邊反轉,邊移動引用。通過上述分析,反轉連結串列至少要 3 個引用,可以得出移動的時機是在反轉 ③ 的時候,我們在反轉 ③ 之前,先後移引用,保證不丟失 ④:
然後反轉 ③:
我們需要指定一個引用,專門用來反轉節點 next 指向。顯然指定中間引用 n2 是合適的,n1 指向著 n2 的前驅節點,n3 指向著 n2 的後繼節點,這樣可以既完成反轉,又不會丟失後續的節點。因此,我們在反轉 ③ 之後,繼續後移引用,使得 n2 指向 ④,完成對 ④ 的反轉:
這裡將移動和反轉做了合併,可以看到,現在整個連結串列已經完成了反轉。
程式碼實現
現在,我們只需將上述的分析結果翻譯成程式碼即可。經過分析可知,反轉連結串列一共需要三個引用,為了清晰直觀,依次記為 prev、node、next,node 用於反轉節點 next 指標。每當完成一次反轉,三個引用便整體向後移動一個節點。程式碼實現如下:
public static Node reverse(Node node) {
if (node == null || node.next == null) {
return node;
}
Node prev = null;
Node next = node.next;
//next 指向空時,只需再進行最後一次反轉
while (next != null) {
//反轉節點
node.next = prev;
//引用後移
prev = node;
node = next;
next = next.next;
}
//反轉最後一個節點
node.next = prev;
//返回反轉後的連結串列頭引用
return node;
}
需要注意的是,當 next 引用指向空時,末尾最後一個節點還未反轉,所以在迴圈外要再反轉一次。
另外,這裡必須將處理好的 node 引用在方法中返回出去,通過拿方法的返回值來獲取反轉後的連結串列。如果仍然使用傳入的 node,會發現 node 只剩下一個節點。有如下測試程式碼:
//定義連結串列:1 -> 2 -> 3
Node node = new Node(1);
node.next = new Node(2);
node.next.next = new Node(3);
System.out.println("原始連結串列:");
show(node);
//反轉連結串列
Node rNode = reverse(node);
System.out.println("反轉後:");
show(node);
show(rNode);
結果如下:
原始連結串列:
[Node{num=1, next=2}, Node{num=2, next=3}, Node{num=3, next=}]
反轉後:
[Node{num=1, next=}]
[Node{num=3, next=2}, Node{num=2, next=1}, Node{num=1, next=}]
這是因為在 Java 中傳遞的是值,而不是引用。反轉後的圖例如下:
在傳遞 node 時,是將 node 儲存的記憶體地址複製了一份,傳給了方法引數 node,方法中對 node 的移動,不會影響方法外的 node。
反轉連結串列至此完成,在解決連結串列相關問題時,要時刻注意不能丟失節點,在修改節點 next 或者 prev 指標時,都要保證仍能訪問到其他節點,如果發現無法複用現有引用,可以嘗試新增引用。保證了這一點之後,剩下的就是按題目要求實現即可。