單鏈表-插入排序

一、思路

寫在前面,遞歸和迭代的分析是不同的,如果我們希望利用遞歸實現,就掌握與遞歸有關的知識點;如果我們希望用迭代實現,就掌握與迭代有關的知識點。先掌握一種,然後掌握另一種。

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上運行代碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章