圖解帶頭節點的單連結串列的反轉操作

onlyblues發表於2021-03-13

前言

  對單連結串列進行反轉是一個很基本的演算法。下面將介紹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

相關文章