演算法2:鏈表的逆轉
- 首先,我們以單鏈表為例子進行演示。總所周知,單鏈表的每個節點都會持有當前節點的下一個節點的對象引用,即next。現在的題目是:「設計一個演算法,逆轉一個已知的單鏈表」。解題思路是:單鏈表是有序的,即知道一個節點,那麼我們就可以確認當前節點(node)的下一個節點(next),即node持有next對象引用。如果反過來,next持有node,那不就是逆轉嗎?
package code.code_02; /** * 題目:設計一種演算法,可以逆轉單鏈表 */ public class SingleNodeList { //單鏈表 private static class Node { public int data; public Node next; Node (int _data){ this.data = _data; } public int getData() { return data; } } //循環的方式逆轉 public static Node reverseNode (Node node) { if (node == null) { System.out.println("鏈表不存在"); } //標記前一個已經完成逆轉的節點 Node prev = null; //標記下一個待逆轉的節點 Node next = null; do { //步驟一 //記錄當前節點的下一個節點,因為它是下一個待逆轉的節點。 //如果不標記,執行步驟二會導致可達性分析無法完成,導致第一個節點完成逆轉後剩餘節點被記憶體回收 next = node.next; //步驟二 // 進行節點逆轉操作 //第一次進入,當前節點將會變成逆轉後的最後一個節點。而最後一個節點的next將會指向null //再次進入的時候,因為第一次執行了步驟三,prev將會變成上一個已經完成逆轉的節點,那麼它自然變成當前節點逆轉後的下一個節點 node.next = prev; //步驟三 //因為當前node已經逆轉完成,把它標記並為下一個節點的後繼節點做準備。 //第二次進入的時候在步驟二中調用 prev = node; //步驟四 //此時的node引用移動到逆轉前當前節點的下一個節點,next已經在步驟一中記錄 node = next; } while(node != null); //此處為什麼要返回prev,而不是返回當前節點node呢? //因為最後一個節有值點為4, 而他的下一個節點為null. 我們需要的是鏈表中實際存在的節點 return prev; } //遞歸的方式逆轉鏈表 public static Node reverseNode2 (Node curNode, Node preReverseNode, Node next) { if (curNode == null) { return curNode; } curNode.next = preReverseNode; preReverseNode = curNode; curNode = next; //逆轉後需要再次確認當前待逆轉的節點是不是null,如果是null說明已經是逆轉前的最後一個油值節點4的下一個節點了。 // 那麼我們只要返回最後一個有值節點即可,因為我們需要的是鏈表中實際存在的節點 return curNode != null ? reverseNode2(curNode, preReverseNode, curNode.next) : preReverseNode; } public static void printNode (Node node) { if (node == null) { System.out.println("鏈表不存在"); } System.out.println("當前鏈表的值為: " + node.getData()); //遞歸的方式逐層列印Node的子節點 if(node.next != null) { printNode(node.next); } } public static void main(String[] args) { //生成單鏈表 Node n = new Node(1); n.next = new Node(2); n.next.next = new Node(3); n.next.next.next = new Node(4); System.out.println("列印循環逆轉前的鏈表: ================================"); printNode(n); n = reverseNode(n); System.out.println("列印循環逆轉後的鏈表: ================================"); printNode(n); Node n1 = new Node(1); n1.next = new Node(2); n1.next.next = new Node(3); n1.next.next.next = new Node(4); System.out.println("列印遞歸逆轉前的鏈表: ================================"); printNode(n1); n1 = reverseNode2(n1, null, n1.next); System.out.println("列印遞歸逆轉後的鏈表: ================================"); printNode(n1); } }
列印結果為:
-
列印循環逆轉前的鏈表: ================================ 當前鏈表的值為: 1 當前鏈表的值為: 2 當前鏈表的值為: 3 當前鏈表的值為: 4 列印循環逆轉後的鏈表: ================================ 當前鏈表的值為: 4 當前鏈表的值為: 3 當前鏈表的值為: 2 當前鏈表的值為: 1 列印遞歸逆轉前的鏈表: ================================ 當前鏈表的值為: 1 當前鏈表的值為: 2 當前鏈表的值為: 3 當前鏈表的值為: 4 列印遞歸逆轉後的鏈表: ================================ 當前鏈表的值為: 4 當前鏈表的值為: 3 當前鏈表的值為: 2 當前鏈表的值為: 1 Process finished with exit code 0
- 既然是設計鏈表的逆轉,當然是要區分單鏈表和雙鏈表的。那麼雙鏈表又改如何設計呢?其實,原理一致,只不過雙鏈表的每個節點會有一個last和next節點對象的引用而已。理解了單鏈表,那麼改成雙鏈表也就非常簡單了
package code.code_02; public class DoubleNodeList { //雙鏈表 private static class Node { public int data; public Node last; public Node next; Node (int _data){ this.data = _data; } public int getData() { return data; } } public static Node reverseDoubleNodeList (Node node, Node prev) { if (node == null) { System.out.println("當前node節點為null"); return node; } //記錄當前節點的上一個節點,下一個節點,為逆轉當前節點做準備 //如果不記錄,當前節點逆轉後會導致數據丟失 Node last = node.last; Node next = node.next; //逆轉當前節點 node.next = last; node.last = next; //prev記錄當前已經完成逆轉的節點,為下一次遞歸做準備 //因為下一次遞歸的時候,我們需要記錄最新的完成逆轉的節點,而prev是為了記錄已經完成逆轉的節點 prev = node; //完成逆轉後, 當前節點node會移動到下一個節點 node = next; //如果逆轉前進入的節點是最後一個有值節點,那麼你轉完以後node會來到null的位置 //此時,我們不需要對node==null的節點進行逆轉,因此只需要返回最後一次完成逆轉的節點即可 //也許,prev會有歧義,我們可以換個合適的名稱來代替prev, 比如preReversedNode return node != null ? reverseDoubleNodeList(node, prev):prev; } public static void printNode (Node node) { if (node == null) { System.out.println("鏈表不存在"); } Node last = node.last; Node next = node.next; System.out.println("當前節點的值為: " + node.getData() + ", 上一個節點值為:" + (last != null ? last.getData():null) + ", 下一個節點為: " + (next !=null ? next.getData():null)); //遞歸的方式逐層列印Node的子節點 if(next != null) { printNode(next); } } public static void main(String[] args) { //構造出5個雙鏈表節點 Node node = new Node(0); Node node1 = new Node(1); Node node2 = new Node(2); Node node3 = new Node(3); Node node4 = new Node(4); node.next = node1; //node1節點 node1.last = node; node1.next = node2; //node2節點 node2.last = node1; node2.next = node3; //node3節點 node3.last = node2; node3.next = node4; //node4節點 node4.last = node3; System.out.println("逆轉前node的hash值為" + node.hashCode()); System.out.println("========================測試逆轉前的雙鏈表============"); printNode(node); //此處為什麼需要一個node來接受逆轉後的值呢? //其實說是逆轉,實際上是更新了記憶體中每一個對象的的值,並且更改了每一個node的指向。類似於生成了一個新對象 //我們可以根據逆轉前,逆轉後的node的hash值進行確認 //而實際開發中,如果只是一個object對象,比如set了一些變數值, 那麼無法接受值(此處需要理解) node =reverseDoubleNodeList(node, null); System.out.println("逆轉後node的hash值為" + node.hashCode()); System.out.println("========================測試逆轉後的雙鏈表============"); printNode(node); } }
列印結果如下
逆轉前node的hash值為1163157884 ========================測試逆轉前的雙鏈表============ 當前節點的值為: 0, 上一個節點值為:null, 下一個節點為: 1 當前節點的值為: 1, 上一個節點值為:0, 下一個節點為: 2 當前節點的值為: 2, 上一個節點值為:1, 下一個節點為: 3 當前節點的值為: 3, 上一個節點值為:2, 下一個節點為: 4 當前節點的值為: 4, 上一個節點值為:3, 下一個節點為: null 逆轉後node的hash值為1956725890 ========================測試逆轉後的雙鏈表============ 當前節點的值為: 4, 上一個節點值為:null, 下一個節點為: 3 當前節點的值為: 3, 上一個節點值為:4, 下一個節點為: 2 當前節點的值為: 2, 上一個節點值為:3, 下一個節點為: 1 當前節點的值為: 1, 上一個節點值為:2, 下一個節點為: 0 當前節點的值為: 0, 上一個節點值為:1, 下一個節點為: null Process finished with exit code 0