前言
對單連結串列進行反轉是一個很基本的演算法。下面將介紹3種不同的單連結串列反轉操作,需要注意的是,我們所討論的單連結串列是包含頭節點的。
我們的連結串列節點和main函式以及部分函式的程式碼如下:
1 #include <cstdio> 2 3 struct LNode { 4 int data; 5 LNode *next; 6 }; 7 8 LNode* read(); 9 void print(LNode *L); 10 11 int main() { 12 LNode *L1, *L2; 13 L1 = read(); 14 L2 = reverse3(L1); 15 print(L2); 16 17 return 0; 18 } 19 20 LNode* read() { 21 LNode *head = new LNode; // 申請一個連結串列節點做為頭節點 22 head -> next = NULL; 23 LNode *last = head; // last指標用於指向最後一個節點,我們通過尾插法進行插入 24 25 int n; 26 scanf("%d", &n); 27 while (n--) { 28 LNode *p = new LNode; 29 scanf("%d", &p -> data); 30 p -> next = NULL; 31 last = last -> next = p; 32 } 33 34 return head; 35 } 36 37 void print(LNode *L) { 38 L = L -> next; // 因為連結串列帶有頭節點,我們需要在頭節點的下一個節點才開始遍歷輸出 39 while (L) { 40 printf("%d ", L -> data); 41 L = L -> next; 42 } 43 putchar('\n'); 44 }
1、迭代反轉連結串列
1 LNode* reverse(LNode *L) { 2 LNode *preNode = NULL, *curNode = L -> next; 3 while (curNode) { 4 LNode *nextNode = curNode -> next; 5 curNode -> next = preNode; 6 preNode = curNode; 7 curNode = nextNode; 8 } 9 L -> next = preNode; 10 11 return L; 12 }
需要說明的是curNode指向的是當前需要反轉的節點。
preNode指向的是當前節點的上一個節點,也就是curNode所指向的節點的上一個節點。
而nextNode指向的是下一個節點,也就是curNode所指向的節點的下一個節點,因為當前節點指向上一個後,該節點原本存有的後續節點的地址就丟失了,所以我們需要nextNode來存放後續節點的地址。
現在我們先建立一個存放著4個資料的連結串列。然後,第一次進入迴圈,執行完上述第4行程式碼後,有如下圖:
重複上面的步驟,在第二次的迴圈結束後,變化為下圖:
繼續重複,當我們走完了迴圈後,整個連結串列就會變為下面這個樣子:
我們發現頭節點並沒有指向存放著資料4的這個節點,所以退出了迴圈後,就要讓頭節點指向存放著資料4的這個節點,而此時preNode正指向它,所以我們可以直接執行L -> next = preNode; 也就是執行第9行這個語句。之後,連結串列就完成了反轉,變成下面這個樣子,然後返回頭指標L,就通過迭代這個方法完成了整一個連結串列反轉的操作。
2、就地逆置法反轉連結串列
1 LNode* reverse(LNode *L) { 2 LNode *curNode = L -> next; 3 L -> next = NULL; 4 while (curNode) { 5 LNode *nextNode = curNode -> next; 6 curNode -> next = L -> next; 7 L -> next = curNode; 8 curNode = nextNode; 9 } 10 11 return L; 12 }
curNode指向的是當前需要反轉的節點。
同樣的,我們需要一個nextNode來存放後續節點的地址。
與上述的迭代反轉不同,我們是把當前的節點插入到頭節點之後,也就是通過頭插法把每一個節點插到頭節點之後。
一樣,現在我們先建立一個存放著4個資料的連結串列。在進入迴圈之前,我們先要讓頭節點指向NULL,不過在此之前,需要用curNode來儲存頭節點所指向的節點的地址,也就是第1個存放資料的節點的地址。
然後,第一次進入迴圈,執行完上述第4行程式碼,有如下圖:
重複上面的步驟,在第二次的迴圈結束後,變化為下圖:
繼續重複,當我們走完了迴圈後,整個連結串列就會變為下面這個樣子:
迴圈結束後,我們的連結串列反轉操作也結束了,接下來只需要返回頭指標L就可以了。
3、遞迴反轉連結串列
1 LNode* reverseFrom2Node(LNode *L) { 2 L -> next = reverse(L -> next); 3 return L; 4 } 5 6 LNode* reverse(LNode *L) { 7 if (L == NULL || L -> next == NULL) { 8 return L; 9 } 10 LNode *last = reverse(L -> next); 11 L -> next -> next = L; 12 L -> next = NULL; 13 14 return last; 15 }
看不懂?先不要急!
先補充說明,在上述程式碼中,reverse函式可以對不帶頭節點的連結串列進行反轉操作,也就是說,如果連結串列不帶有頭節點,只需要通過reverse函式就可以完成整一個連結串列的反轉。但是,由於我們的連結串列帶有頭節點,如果只呼叫reverse函式就行不通了。所以我們還需要另一個函式reverseFrom2Node(正如函式名一樣,從第二個節點開始反轉)。
為了方便說明,我們先假設連結串列不帶有頭節點,先來看看reverse函式是如何工作的。
對於這個遞迴演算法,我們首先要明確這個遞迴的定義。
就是,我們傳入一個頭指標L,然後將以頭指標為起點的連結串列進行反轉,並返回反轉後的連結串列的第一個節點的地址。而當連結串列的長度為1,也就是隻有一個節點時,由於反轉後還是其自身,所以返回的依然是原來傳入的那個頭指標。注意,此時我們的連結串列不帶有頭節點。接下來,我們對下面這個連結串列執行reverse函式,進行遞迴反轉。
第一次進入reverse函式後,由於L -> next != NULL,所以不會進入到選擇語句中。然後我們執行第10行,進行第一次遞迴。
不要跳進遞迴裡面去!而是要根據剛才的遞迴函式定義,來弄清楚這行程式碼會產生什麼結果:
第10行程式碼執行完後,整個連結串列就會變成這樣子,根據定義,reverse返回的是反轉後連結串列的第一個節點的地址,我們用last來接收,如圖:
接下來是第11行的程式碼,目的是讓上圖的第二個節點(從左到右數)指向第一個節點(從左到右數),執行完後如下:
第12行程式碼就是讓L指向的那個節點,也就是第一個節點指向NULL。
之後我們return last,就將整一個連結串列進行反轉,並返回反轉後的連結串列的頭節點。
OK,現在你應該理解了reverse函式是如何工作的了。接下來解釋reverseFrom2Node這個函式。
在解釋reverse函式中,我們的連結串列是不帶有頭節點的。現在,我們的連結串列又帶有頭節點了。
你可能會問,如果我們讓帶頭節點的連結串列直接執行reverse這個函式,會產生什麼樣的結果。正如我們前面所說的,reverse函式是將連結串列的第一個節點到最後一個節點進行反轉,並沒有說可以只反轉其中一個部分。如果讓帶頭節點的連結串列執行reverse函式,就會變成下面這樣子:
很明顯,這不是我們想要的結果。
我們其實是想讓頭節點之後的剩下節點進行反轉,然後再將頭節點指向last所指向的這個節點,也就是這樣子:
為了達到這個目的,其實我們只要給reverse函式傳入L -> next,不就可以了嗎。也就是說,我們只反轉除頭節點之外的剩下部分的節點,而不反轉頭節點。reverse呼叫結束後(注意,並不是執行完第2行的程式碼後),會返回反轉後連結串列的第一個節點的地址,(也就是上圖對應的last所指向的節點,只是在我們的程式碼種並沒有last這個中間變數,而是將reverse返回的值直接賦值給L -> next),如圖:
此時,我們只需要L -> next = last,就可以完成整一個連結串列的反轉了。
在上面的代買中我們直接讓L -> next來接收reverse返回的值,達到同樣的效果。
所以,我們才定義了reverseFrom2Node這個函式。
綜上,我們的遞迴反轉是分兩部進行的。由於我們的連結串列帶有頭節點,所以需要先讓除頭節點之外剩餘節點進行反轉,也就是執行reverse(L -> next); 然後再讓頭節點來接收reverse函式返回的反轉後的連結串列的第一個節點的地址,從而完成了整一個連結串列的反轉。
到這裡,我們已經解釋完遞迴反轉是如何實現的了。
其實,還有一種演算法是隻對連結串列中的第n個到第m個節點進行反轉,上述只是該演算法的一種特殊情況,也就是讓連結串列中第2個節點到最後一個節點進行反轉。如果要實現連結串列中的第n個到第m個節點反轉,相關的程式碼就完全不是我們上面的那個樣子了。
關於這個演算法,可以參考:如何遞迴反轉連結串列 —— https://zhuanlan.zhihu.com/p/86745433
結語
就此,關於帶有頭節點的單連結串列反轉操作的3種方法已經介紹完了,下面再附上包含這3種反轉方法的完整程式碼:
1 #include <cstdio> 2 3 struct LNode { 4 int data; 5 LNode *next; 6 }; 7 8 LNode* read(); 9 void print(LNode *L); 10 LNode* reverse1(LNode *L); 11 LNode* reverse2(LNode *L); 12 LNode* reverseFrom2Node(LNode *L); 13 LNode* reverse3(LNode *L); 14 15 int main() { 16 LNode *L1, *L2; 17 L1 = read(); 18 19 // 反轉的3種方法 20 // L2 = reverse1(L1); 21 // L2 = reverse2(L1); 22 // L2 = reverseFrom2Node(L1); 23 24 print(L2); 25 26 return 0; 27 } 28 29 LNode* read() { 30 LNode *head = new LNode; 31 head -> next = NULL; 32 LNode *last = head; 33 34 int n; 35 scanf("%d", &n); 36 while (n--) { 37 LNode *p = new LNode; 38 scanf("%d", &p -> data); 39 p -> next = NULL; 40 last = last -> next = p; 41 } 42 43 return head; 44 } 45 46 void print(LNode *L) { 47 L = L -> next; 48 while (L) { 49 printf("%d ", L -> data); 50 L = L -> next; 51 } 52 putchar('\n'); 53 } 54 55 // 迭代反轉連結串列 56 LNode* reverse1(LNode *L) { 57 LNode *preNode = NULL, *curNode = L -> next; 58 while (curNode) { 59 LNode *nextNode = curNode -> next; 60 curNode -> next = preNode; 61 preNode = curNode; 62 curNode = nextNode; 63 } 64 L -> next = preNode; 65 66 return L; 67 } 68 69 // 就地逆置法反轉連結串列 70 LNode* reverse2(LNode *L) { 71 LNode *curNode = L -> next; 72 L -> next = NULL; 73 while (curNode) { 74 LNode *nextNode = curNode -> next; 75 curNode -> next = L -> next; 76 L -> next = curNode; 77 curNode = nextNode; 78 } 79 80 return L; 81 } 82 83 // 遞迴反轉連結串列 84 LNode* reverseFrom2Node(LNode *L) { 85 L -> next = reverse3(L -> next); 86 return L; 87 } 88 89 LNode* reverse3(LNode *L) { 90 if (L == NULL || L -> next == NULL) { 91 return L; 92 } 93 LNode *last = reverse3(L -> next); 94 L -> next -> next = L; 95 L -> next = NULL; 96 97 return last; 98 }
感謝你的閱讀!
參考資料
如何遞迴反轉連結串列:https://zhuanlan.zhihu.com/p/86745433