一、思路
寫在前面,遞歸和迭代的分析是不同的,如果我們希望利用遞歸實現,就掌握與遞歸有關的知識點;如果我們希望用迭代實現,就掌握與迭代有關的知識點。先掌握一種,然後掌握另一種。
1.1 知識點1 單鏈表
單鏈表是一種只能前進,不能後退的鏈表。爲了找到當前結點的前一個,必須有一個前驅指針緊緊跟隨。很明顯,可以用棧(用戶棧或者系統棧)幫助我們找到前驅結點。
1.2 知識點 插入排序
讓我們先抽象到邏輯結構來理解插入排序:
- 只有1個元素,默認有序;
- 每次將無序部分的1個元素插入到有序部分中;
- 直到線性表全部有序。
理解了插入排序之後,還有一個小知識點,是按照從後向前插入還是從前向後插入呢?因爲單鏈表的特點,我們選擇從前向後插入。
1.3 知識點 遞歸分析
簡潔是遞歸的優點——將大規模的問題轉化爲小規模的問題,使得問題最終得到解決;效率較低是遞歸的缺點,但那是相對於循環而言的。任何談及效率的說法,不可或缺的前提是問題得到了解決,問題沒解決前,談論效率,沒有意義。
最後我們來看看,如何將遞歸和單鏈表的插入排序結合起來設計,大體思想如下:
- 如果單鏈表爲空表或者只有1個結點,直接返回;
- 除去第1個結點,剩餘結點已經有序(遞歸單元之所在,注意,遞歸問題,層序分析;腦袋能裝幾個棧呢?);
- 將第一個結點插入到有序部分中。
對於第3條,第一個結點插入到有序部分中,可能恰好在有序部分的第一個,也可能是有序部分的中間,還有可能是有序部分的末尾,結合代碼觀察是如何討論的。
1.4 知識點 迭代分析
在數組中,我們可以用下標反應物理位置,也即存儲結構與邏輯結構一致;而在單鏈表中,我們只能用指針去標誌範圍。首先,對於不帶頭的單鏈表(首個數據結點不存在前驅),爲了統一插入操作,暫時使用一個啞結點作爲首個數據結點的前驅。
- 待插入的結點總是有序部分的下一個,直到單鏈表完全有序爲止;
- 通過遍歷有序部分,來確定插入位置;確定插入位置後爲了插入,還需要額外的前驅指針;
- 如果待插入的結點比有序部分最後一個都大的話,直接尾插併入有序部分即可;
經過以上討論,我們需要指針prev(用於遍歷有序部分),指針tail(指向有序部分的末尾),指針cur(指向待插入的結點)。一共需要三根指針。
二、代碼實現(C/C++)
2.1 遞歸版本
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
ListNode* insertionSortList(ListNode* head) {
if (!head || !head->next) //如果鏈表爲空或者只有1個結點,直接返回
return head;
ListNode *q = insertionSortList(head->next); //對剩餘部分插入排序完成
if (head->val <= q->val) { //插在第一個
head->next = q;
}
else {
ListNode *p = head, *prev = head, *cur = q; //將p指向的結點插入到有序部分中
prev->next = cur; //防止斷鏈
int cmp = head->val;
while (cur && cmp > cur->val) {
prev = prev->next;
cur = cur->next;
}//找到插入位置
if (cur) { //不是最後一個
p->next = cur;
prev->next = p;
}
else { //是最後一個
prev->next = p;
p->next = NULL;
}
head = q;
}
return head;
}
2.2 迭代版本
迭代版本效率一般會比遞歸高,但是我們學的是方法,不要過分追求效率,以免難以編寫代碼。優化的點:如果經過判斷,待插結點可以直接尾插的話,那麼就不要到有序序列中去查找插入位置。
ListNode* insertionSortList(ListNode* head) {
if (!head || !head->next) {
return head;
}//空表或者只包含1個結點
ListNode *dummy = new ListNode(0);
dummy->next = head;
ListNode *tail = head; //tail標記有序部分的末尾
ListNode *current = head->next; //current指向無序部分的第1個結點
tail->next = NULL; //斷開有序部分和無序部分
while (current) {
if (tail->val < current->val) { //直接尾插的情況
tail->next = current;
tail = tail->next; //擴大有序部分
current = current->next;
tail->next = NULL; //斷開有序部分和無序部分
}
else { //需要尋找插入位置的情況
ListNode *prev = dummy;
while (prev->next->val < current->val) {
prev = prev->next;
} //指向待插入位置前1個結點
ListNode *save = current->next; //暫存後繼,防止斷鏈
current->next = prev->next;
prev->next = current;
current = save;
}
}
head = dummy->next;
delete dummy;
return head;
}
寫在最後,我不太希望我們使用手工模擬的方式去感受遞歸程序,如果非要這樣做的話,我提供兩組測試用例,第一組 4 2 1 3;第二組 1 1。只要我們設計沒問題,就不會有死遞歸的發生,遞歸實在是太難調試了。看完本篇博客,爲了鞏固,還可以到LeetCode上運行代碼。