題目:給定一個帶附加頭節點的單鏈表,設first為其頭指針,節點的結構為(data,link),data為數據域,link為指針域,試寫出算法:通過遍歷一趟鏈表,將鏈表中所有節點逆序連接
分析:這是很經典的“單鏈表逆序”問題。很多公司的面試題庫中都有這道題,有的公司明確題目要求不能開辟額外的節點空間(否則可以在遍歷原鏈表的同時使用前插法建立一個逆序鏈表),有的沒有明確說明,但是如果面試者使用了額外的節點存儲空間,會得到一個比較低的分數。如何在不使用額外節點的情況下使一個單鏈表的所有節點逆序?我們先用迭代循環的思想來分析這個問題。
這種方法需要另外定義一個前驅指針prev和后繼指針latter。初始狀態,p指向當前節點即第一個節點A,prev為NULL,latter指向A的下一個節點B。鏈表的初始狀態如下圖所示:

從A節點開始逆序,首先將A節點的link指針指向prev(因為prev的當前值是NULL,所以A節點就從鏈表中脫離出來了),然后整體后移,prev指向p,后移p和latter指針。逆向節點A之后,鏈表的狀態如下圖所示:

從圖(1)的初始狀態到圖(2)狀態共做了四個操作,這四個操作的偽代碼如下:
p->link = prev; prev = p; p = latter; latter = p->link;
這四行偽代碼就是循環算法的迭代體了,現在用這個迭代體對圖(2)的狀態再進行一輪迭代,就得到了下圖的狀態:

那么循環終止條件呢?現在對圖(3)的狀態再迭代一次得到下圖的狀態:

此時可以看出,在圖(4)的基礎上再進行一次迭代就可以完成鏈表的逆序,因此循環迭代的終止條件就是p指針為NULL。
當然,最后還要注意將附加頭節點放到首節點的前面
源碼:
template<class T> void List<T>::Inverse() { if(first == NULL) return; LinkNode<T> *p, *prev, *latter; p = first->link; // 當前結點 prev = NULL; // 前一結點 latter = p->link; // 下一結點 while(p != NULL) { p->link = prev; // 當前結點指針指向前一結點 prev = p; // 后移 p = latter; if(p != NULL) // 如果p指針是NULL,已經滿足終止條件 latter = p->link; } first->link = prev;; // 最后連上附加頭結點 }
現在,我們用遞歸的思想來分析這個問題。先假設有這樣一個函數reverseLink,可以將以head為頭節點的單鏈表逆序,並返回新的頭節點指針,函數原型如下:
LinkNode<T>* reverseLink(LinkNode<T> *head)
現在利用reverseLink對問題進行求解,將鏈表分為當前頭節點和其余節點,遞歸的思想就是:先將當前的頭節點從鏈表中拆出來(附加頭節點暫時忽略,只要最后連上即可),然后對剩余的節點進行逆序,最后將當前的表頭節點連接到新鏈表的尾部。第一次遞歸調用reverseLink函數時的狀態如下圖所示:
注意:遞歸過程的狀態圖並不反應程序執行順序的狀態,而是設計遞歸過程邏輯順序的狀態,這一點容易產生誤解。

這里邊的關鍵點是頭節點head的下一個節點head->link將是逆序后的新鏈表的尾節點,也就是說,被摘除的頭接點head需要被連接到head->link->link才能完成整個鏈表的逆序,遞歸算法的核心就是以下幾行代碼:
newHead = reverseLink(head->link); /*遞歸部分*/ head->link->link = head; /*回朔部分*/ head->link = NULL;
現在順着這個思路再進行一次遞歸,就得到第二次遞歸的狀態圖:

再進行一次遞歸分析,就能清楚地看到遞歸終止條件了:

遞歸終止條件就是鏈表只剩一個節點時直接返回這個節點的指針。可以看出這個算法的核心其實是在回朔部分,遞歸的目的是遍歷到鏈表的尾節點,然后通過逐級回朔將節點的link指針翻轉過來。
源碼:
template<class T> LinkNode<T>* List<T>::reverseLink(LinkNode<T> *head) //遞歸逆置鏈表 { if((head == NULL) || (head->link == NULL)) return head;//遞歸終止條件 LinkNode<T> *newHead; newHead = reverseLink(head->link); /*遞歸部分*/ head->link->link = head; /*回朔部分*/ head->link = NULL; return newHead; } template<class T> void List<T>::Inverse() //迭代循環逆置鏈表 { first->link = reverseLink(first->link);//調用遞歸函數,返回逆序鏈表新頭節點的指針,並連接到附加頭節點之后 }
循環迭代還是遞歸?這是個問題。當面對一個問題的時候,不能一概認為哪種算法好,哪種不好,而是要根據問題的類型和規模作出選擇。對於線性數據結構,比較適合用循環迭代方法,而對於樹狀數據結構,比如二叉樹,遞歸方法則非常簡潔優雅。
