內容轉載來自:單鏈表逆序
第二個題目是很經典的“單鏈表逆序”問題。很多公司的面試題庫中都有這道題,有的公司明確題目要求不能使用額外的節點存儲空間,有的沒有明確說明,但是如果面試者使用了額外的節點存儲空間做中轉,會得到一個比較低的分數。如何在不使用額外存儲節點的情況下使一個單鏈表的所有節點逆序?我們先用迭代循環的思想來分析這個問題,鏈表的初始狀態如圖(1)所示:
圖(1)初始狀態
初始狀態,prev是NULL,head指向當前的頭節點A,next指向A節點的下一個節點B。首先從A節點開始逆序,將A節點的next指針指向prev,因爲prev的當前值是NULL,所以A節點就從鏈表中脫離出來了,然後移動head和next指針,使它們分別指向B節點和B的下一個節點C(因爲當前的next已經指向B節點了,因此修改A節點的next指針不會導致鏈表丟失)。逆向節點A之後,鏈表的狀態如圖(2)所示:
圖(2)經過第一次迭代後的狀態
從圖(1)的初始狀態到圖(2)狀態共做了四個操作,這四個操作的僞代碼如下:
head->next = prev;
prev = head;
head = next;
next = head->next;
這四行僞代碼就是循環算法的迭代體了,現在用這個迭代體對圖(2)的狀態再進行一輪迭代,就得到了圖(3)的狀態:
圖(3)經過第二次迭代後的狀態
那麼循環終止條件呢?現在對圖(3)的狀態再迭代一次得到圖(4)的狀態:
圖(4)經過第三次迭代後的狀態
此時可以看出,在圖(4)的基礎上再進行一次迭代就可以完成鏈表的逆序,因此循環迭代的終止條件就是當前的head指針是NULL。
現在來總結一下,循環的初始條件是:
prev = NULL;
循環迭代體是:
next = head->next;
head->next = prev;
prev = head;
head = next;
循環終止條件是:
head == NULL
根據以上分析結果,逆序單鏈表的循環算法如下所示:
61 LINK_NODE *ReverseLink(LINK_NODE *head) 62 { 63 LINK_NODE *next; 64 LINK_NODE *prev = NULL; 65 66 while(head != NULL) 67 { 68 next = head->next; 69 head->next = prev; 70 prev = head; 71 head = next; 72 } 73 74 return prev; 75 } |
現在,我們用遞歸的思想來分析這個問題。先假設有這樣一個函數,可以將以head爲頭節點的單鏈表逆序,並返回新的頭節點指針,應該是這個樣子:
77 LINK_NODE *ReverseLink2(LINK_NODE *head) |
現在利用ReverseLink2()對問題進行求解,將鏈表分爲當前表頭節點和其餘節點,遞歸的思想就是,先將當前的表頭節點從鏈表中拆出來,然後對剩餘的節點進行逆序,最後將當前的表頭節點連接到新鏈表的尾部。第一次遞歸調用ReverseLink2(head->next)函數時的狀態如圖(5)所示:
圖(5)第一次遞歸狀態圖
這裏邊的關鍵點是頭節點head的下一個節點head->next將是逆序後的新鏈表的尾節點,也就是說,被摘除的頭接點head需要被連接到head->next才能完成整個鏈表的逆序,遞歸算法的核心就是一下幾行代碼:
84 newHead = ReverseLink2(head->next); /*遞歸部分*/ 85 head->next->next = head; /*回朔部分*/ 86 head->next = NULL; |
現在順着這個思路再進行一次遞歸,就得到第二次遞歸的狀態圖:
圖(6)第二次遞歸狀態圖
再進行一次遞歸分析,就能清楚地看到遞歸終止條件了:
圖(7)第三次遞歸狀態圖
遞歸終止條件就是鏈表只剩一個節點時直接返回這個節點的指針。可以看出這個算法的核心其實是在回朔部分,遞歸的目的是遍歷到鏈表的尾節點,然後通過逐級回朔將節點的next指針翻轉過來。遞歸算法的完整代碼如下:
77 LINK_NODE *ReverseLink2(LINK_NODE *head) 78 { 79 LINK_NODE *newHead; 80 81 if((head == NULL) || (head->next == NULL)) 82 return head; 83 84 newHead = ReverseLink2(head->next); /*遞歸部分*/ 85 head->next->next = head; /*回朔部分*/ 86 head->next = NULL; 87 88 return newHead; 89 } |
循環還是遞歸?這是個問題。當面對一個問題的時候,不能一概認爲哪種算法好,哪種不好,而是要根據問題的類型和規模作出選擇。對於線性數據結構,比較適合用迭代循環方法,而對於樹狀數據結構,比如二叉樹,遞歸方法則非常簡潔優雅。
按照此思路java實現:
public class IntNode
{
// 節點的整型數據
private int data;
// 對於鏈表中的最後一個節點,其link部分爲null.否則,link爲引向鏈表中下一個節點的引用。
private IntNode link;
// 根據制定的數據和引向下一節點的引用創建生成一個節點
public IntNode(int initialData, IntNode initialLink)
{
this.data = initialData;
this.link = initialLink;
}
// 鏈表的反轉,利用遞歸思想
public static IntNode reverse1(IntNode head)
{
if (head == null || head.getLink() == null)
{
return head;
}
IntNode newHead = reverse1(head.getLink());
head.link.link = head;
head.link = null;
return newHead;
}
// 鏈表的反轉,利用循環方法
public static IntNode reverse2(IntNode head)
{
if (head == null || head.link == null)
{
return head;
}
IntNode newHead;
IntNode nextNode;
newHead = null;
while (head != null)
{
nextNode = head.link;
head.link = newHead;
newHead = head;
head = nextNode;
}
return newHead;
}
}