最近在做筆試題時,遇到一道編程題:單向鏈表反轉算法。一時緊張,沒寫出來就提前交卷了,然而交完卷就想出來了。。。
最初想出來的是遞歸版,遺憾的是沒能做到尾遞歸,后來又琢磨出了迭代版。后來用實際編譯運行測試了一遍,能正常運行。
遞歸版的靈感來源於《Haskell 趣學指南》中非常簡潔的快速排序算法的實現,其思想是將單向鏈表分割頭部和尾部。其中頭部指是鏈表的第一個節點,尾部是指除去第一個節點后的子鏈表。通過遞歸的方法,將子鏈表繼續分割成頭部和尾部,直至尾部指剩下一個節點,無法繼續分割,然后將頭部和尾部的位置交換,返回尾部,然后將前一個頭部接到返回的尾部后面,繼續返回新的尾部,依此類推,直到整個遞歸完成。
迭代版的思想是,用三個指針(first, now, next)來協助調整順序。其中每一次迭代(即:每遍歷一個節點),都將單鏈表動態分割成反轉后的和反轉前的兩部分。每一次迭代后,反轉后的鏈表會變長,反轉前的鏈表會變短,直到反轉前的鏈表長度為0,反轉完成,返回反轉后的鏈表。整個思想和冒泡排序有點像,把序列(鏈表)划分成有序區(反轉后的部分)和無序區(反轉前的部分),但是只需對整個單向鏈表進行一趟遍歷。
值得注意的是,第一次迭代,是從單向鏈表的第二個節點開始的。而第一個節點默認就是反轉后的節點,而且是反轉后的尾節點,為了避免環路導致的死循環,需要將該節點指向的下一個節點的指針設為 NULL 。
這三個指針的作用如下:
- first 指針始終指向反轉后的鏈表的首節點;
- now 指針指向當前遍歷到的反轉前的鏈表的首節點;
- next 指針指向 now 節點的下一個節點,避免在 now->next 指向反轉后的鏈表首節點時,丟失了反轉前的鏈表的引用。
關鍵數據結構如下:
1 typedef struct _Node { 2 int data; 3 struct _Node * next; 4 } Node;
遞歸版的反轉算法代碼如下:
1 Node * Node_reverse(Node *node) { 2 if (node == NULL) return NULL; 3 if (node->next == NULL) return node; 4 Node * n = Node_reverse(node->next); 5 if (n != NULL) { 6 n->next = node; 7 node->next = NULL; 8 } 9 return node; 10 }
其實,這個遞歸版反轉算法有個小缺陷:那就是整個遞歸完成時,返回的節點就是傳入的參數。也就是說,如果傳入的是反轉前的單向鏈表首節點,那么最終返回的也是這個節點,而這個節點卻是反轉后的單向鏈表的尾節點(因為反轉了,首尾交換)。因此,使用時,需要做些微不足道的工作來彌補缺陷。即:在使用前需要先對單向鏈表進行一趟遍歷,找到尾節點並用指針引用它。。。其實還能改善一下這段代碼來修正這個小缺陷,把這些微不足道的工作交給這個函數來完成,也就是加個 if 分支的事,我懶得寫了。迭代版的就沒這個問題,而且效率略高。
迭代版的反轉算法代碼如下:
1 Node * Node_reverse_v2(Node* node) { 2 if (node == NULL) return NULL; 3 if (node->next == NULL) return node; 4 5 Node *first = node; //總是指向新鏈表的首部。 6 Node *now = node->next; 7 Node *next = now->next; 8 first->next = NULL; //首節點變成尾節點,尾節點的下一個節點置空,防止環路。 9 do { 10 next = now->next; 11 now->next = first; 12 first = now; 13 now = next; 14 } while (next != NULL); 15 16 return first; 17 }
測試代碼如下:
1 void printNode(Node *node) { 2 if (node != NULL) { 3 printf("%d ", node->data); 4 } 5 } 6 7 int main() 8 { 9 LinkedList *l = List_create(); 10 for (int i=0; i<20; i++) { 11 List_append(l, i); 12 } 13 List_foreach(l, printNode); 14 printf("\n"); 15 List_foreach(List_reverse(l), printNode); 16 printf("\n"); 17 List_foreach(List_reverse_v2(l), printNode); 18 printf("\n"); 19 return 0; 20 }
輸出如下:
完整代碼下載: