問題描述
輸入兩個單調遞增的鏈表,輸出兩個鏈表合成後的鏈表,當然我們需要合成後的鏈表滿足單調不減規則。
舉個栗子:
鏈表1:1->3->5->7
鏈表2:2->4->8
合併之後的鏈表:1->2->3->4->5->7->8
解決思想
對於這個題,我想我們可以這樣做:
類似於排序的方式,並且兩個子鏈表都已經排好了順序。
以上述爲例,我們很容易發現鏈表1的第一個元素小於鏈表2的第一個元素,也就是1 < 2,毫無疑問,1肯定是兩條鏈表裏的最小值節點。好,我們不再看關注1。
那麼此時我們的鏈表1變爲了3->5->7,鏈表2變爲了2->4->8,
同理執行上述操作(即比較兩個鏈表的第一個元素的大小),我們發現2 < 3,這說明2是次小的,我們不再關注2。
此時鏈表1變爲了3->5->7,鏈表2變爲了4->8。
我們繼續重複執行上述操作,到最好我們發現鏈表1爲空了,這個時候鏈表2僅有一個節點,節點值爲8。
該例中,鏈表2僅存的最後一個節點就是兩個鏈表中的最大值節點。
這個時候我們選擇返回這個不爲空的鏈表(因爲沒有比較的意義了啊~ 這個鏈表本身就是排好的,也沒有和它比的鏈表了)。
通過上述操作,我們可以依次把排好序的整體鏈表的值依次輸出。我們發現:
1.每次操作都是類似的,即都比較了拋棄該節點之後的新鏈表與下個列表的第一個值進行比較。
2.當某個鏈表爲空時,我們只需要返回剩餘不爲空的鏈表即可。
我們用遞歸進行實現。
我的代碼
在貼代碼之前,我們不妨先看看C++語言中鏈表實現的這種方式。
struct ListNode {
// 節點的值
int val;
// 節點的next指針,指向下一個節點
struct ListNode *next;
// 有參構造函數(初始化)
ListNode(int x){
val = x;
next = nullptr;
}
};
使用結構體來定義了這樣的一個鏈表中的節點,這點很關鍵。一個鏈表其實就是指針指向了這個節點,這個節點又指向了下一個節點。
class Solution {
public:
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
// 遞歸出口1:鏈表1爲空,返回鏈表2
if(pHead1 == nullptr){
return pHead2;
// 遞歸出口2:鏈表2爲空,返回鏈表1
}else if(pHead2 == nullptr){
return pHead1;
}
// 若鏈表1的第一個元素小於鏈表2的第一個元素 那麼保留鏈表1的第一個元素並使它的指針指向後續鏈表合併的結果
if(pHead1->val <= pHead2->val){
pHead1->next = Merge(pHead1->next, pHead2);
// 返回這個鏈表
return pHead1;
// 若鏈表2的第一個元素小於鏈表1的第一個元素 那麼保留鏈表2的第一個元素並使它的指針指向後續鏈表合併的結果
}else{
pHead2->next = Merge(pHead1, pHead2->next);
// 返回這個鏈表
return pHead2;
}
}
};
代碼理解
我想很多人看這個代碼都是有點懵的。
我想從兩個角度來講一下我是如何理解的。
角度1
遞歸的習慣。
我們在上面的內容中已經分析了,遞歸的出口是什麼?——自然是當有個鏈表爲空時,返回另一個鏈表。
遞歸的狀態轉移方程是什麼?——比較當前階段下的鏈表1和鏈表2的首元素。若鏈表1的首元素小,我們就留下它,並使它的next指針指向新的鏈表1(去掉了首元素)和鏈表2合併的結果,反之鏈表2也一樣。
這樣代碼便好寫了。
角度2
分析代碼到底是怎麼實現的。
這裏用到了一種工具——棧。
我來解釋一下我畫的這個圖。重點在右側的棧區。
我們第一次對鏈表1的首元素和鏈表2的首元素進行比較時候,我們發現1 < 2。那麼計算機如何做呢?
我們留下1,並將next指向下一個入棧的東西(就是後續操作)。爲什麼呢?
我們傳過來的鏈表1是完整的鏈表1,那麼鏈表本身的節點不變吧,就是1,我們只能對next指針進行操作,令它指向後續鏈表的合併結果。
第二次,我們發現2 < 3,同理吧,我們肯定留下2,令它的指針指向後續鏈表合併的結果。
如此,直到最後,僅有一個節點(節點值爲8)的鏈表入棧了。
根據我們遞歸出口設定,這個時候要返回這個僅有一個節點(節點值爲8)的鏈表。
那麼節點值爲7的節點指向這個僅有一個節點(節點值爲8)的鏈表,5又指向7,4又指向5.。。。每次都會把完整指向的鏈表返回,直到最後就返回了節點值爲1的鏈表,即1->2->3->4->5->7->8。
此時所有元素均出棧完畢了。
我的總結
遞歸分析還是比較困難的。培養遞歸的思想還是很重要。