1. 程式人生 > >單鏈表排序(快速排序、歸併排序)

單鏈表排序(快速排序、歸併排序)

本題目來源於LeetCode,具體如下:

Sort a linked list in O(n log n) time using constant space complexity.

題目要求複雜度O(nlogn),因此我們很自然考慮使用快速排序或者歸併排序,但是後來經過實踐證明,使用快速排序總是AC超時,歸併排序則可以正確AC。

分析一下原因,個人認為是與測試資料有關,因為快速排序不能保證演算法複雜度一定是O(nlogn),當資料比較集中時,即使做隨機選取key值,演算法的複雜度也非常接近O(N^2),因此會出現超時,所以考慮使用歸併排序。

下面是採用歸併排序的思路已經AC程式碼:

主要考察

3個知識點,知識點1:歸併排序的整體思想知識點2:找到一個連結串列的中間節點的方法知識點3:合併兩個已排好序的連結串列為一個新的有序連結串列歸併排序的基本思想是:找到連結串列的middle節點,然後遞迴對前半部分和後半部分分別進行歸併排序,最後對兩個以排好序的連結串列進行Merge

#include <iostream>
#include <string>
#include <algorithm>
#include <stack>
#include <vector>
#include <fstream>
using namespace std;

struct ListNode {
	int val;
	ListNode *next;
	ListNode(int x) : val(x), next(NULL) {}
};

class Solution {
public:

	ListNode* mergeLists(ListNode *a, ListNode *b) //合併兩個已經排序的連結串列
	{
		if (a == NULL) return b ;
		if (b == NULL) return a ;
		ListNode *ret = NULL ;
		ListNode *tail = NULL ;
		
		ret = new ListNode(-1) ;
		tail = ret ;
		while (a && b)
			if (a->val < b->val)
			{
				tail->next = a ;
				tail = tail->next ;
				a = a->next ;
			}
			else
			{
				tail->next = b ;
				tail = tail->next ;
				b = b->next ;
			}
		if (a)
			tail->next = a ;
		if (b)
			tail->next = b ;

		ListNode *del = ret ;
		ret = ret->next ;
		delete del ;

		return ret ;
	}

	ListNode *getMid(ListNode *head) //得到中間節點
	{ 
		if (!head) return NULL ;
		if (!head->next) return head ;

		ListNode *slow = head ;
		ListNode *fast = head->next ;

		while (fast && fast->next)
		{
			slow = slow->next ;
			fast = fast->next->next ;
		}
		return slow ;
	}

	ListNode *sortList(ListNode *head) { //合併排序

		if (!head) return NULL ;
		if (!head->next) return head ;

		ListNode *mid = getMid(head) ;
		ListNode *nextPart = NULL ;
		if (mid)
		{
			nextPart = mid->next ;
			mid->next = NULL ;
		}

		return mergeLists(
			 sortList(head) ,
			 sortList(nextPart) 
			) ;
	}
};

void insertBack(ListNode** head, ListNode** tail,  ListNode* n) //從尾部插入
{	
	if (n)
	{
		if (*head == NULL)
		{
			*head = n ;
			*tail = n ;
		}
		else
		{
			(*tail)->next = n ;
			*tail = n ;
		}
	}
}

int main(int argc, char** argv)
{

	ifstream in("data.txt") ;
	ListNode* head = NULL ;
	ListNode* tail = NULL ;
	int val ;

	Solution s ;
	while(in >> val)
	{
		ListNode*tmp = new ListNode(val) ;
		insertBack(&head, &tail, tmp) ;
	}
	head = s.sortList(head) ;
	while(head)
	{
		cout << head->val << " " ;
		head = head->next ;
	}
	cout << endl ;
	return 0 ;
}

下面再說一下自己AC超時的程式碼吧,

這裡我嘗試了兩種實現方案:

第一種是:

在找劃分點的過程中,維護連個連結串列Left 和Right 所有不大於key的元素都鏈到Left上,大於key的鏈到Right上,最後再將Left, key , Right三部分連線起來。

程式碼如下:

#include <iostream>
#include <string>
#include <algorithm>
#include <stack>
#include <vector>
#include <fstream>
using namespace std;

struct ListNode {
  int val;
  ListNode *next;
  ListNode(int x) : val(x), next(NULL) {}
};

class Solution {
public:
	inline void insertBack(ListNode** head, ListNode** tail,  ListNode* n) //從尾部插入
	{	
		if (n)
		{
			if (*head == NULL)
			{
				*head = n ;
				*tail = n ;
			}
			else
			{
				(*tail)->next = n ;
				*tail = n ;
			}
		}
	}

    ListNode *sortList(ListNode *head) {
		if (!head) return NULL ;
		if (head->next == NULL) return head ;
		//劃分
		ListNode *tmpNode = head ;
		head = head->next ;
		ListNode *sleft = NULL , *eleft = NULL ;
		ListNode *sright = NULL , *eright = NULL ;
		while (head)
		{
			ListNode *insNode = head ;
			head = head->next ;

			insNode->next = NULL ;
			if (insNode->val > tmpNode->val)
				insertBack(&sright, &eright, insNode) ;
			else
				insertBack(&sleft, &eleft, insNode) ;
		}

		//遞迴呼叫	
		sleft = sortList(sleft) ;
		sright = sortList(sright) ;

		//下面三句話第一次沒有加上,除錯了一下午才找到原因
		eleft = sleft ;
		if (eleft)
		{
			while(eleft->next)
				eleft = eleft->next ;
		}

		//拼接起來
		if (eleft)
		{
			head = sleft ;
			eleft->next = tmpNode ;
		}
		else
			head = tmpNode ;
		tmpNode->next = sright ; //連線起來

		//返回結果
		return head ;
    }
};

int main(int argc, char** argv)
{
	ifstream in("data.txt") ;
	ListNode* head = NULL ;
	ListNode* tail = NULL ;
	int val ;

	Solution s ;
	while(in >> val)
	{
		ListNode*tmp = new ListNode(val) ;
		s.insertBack(&head, &tail, tmp) ;
	}
	head = s.sortList(head) ;
	while(head)
	{
		cout << head->val << " " ;
		head = head->next ;
	}
	cout << endl ;
	return 0 ;
}

第二種方案:使用快排的另一種思路來解答。我們只需要兩個指標pq,這兩個指標均往next方向移動,移動的過程中保持p之前的key都小於選定的keypq之間的key都大於選定的key,那麼當q走到末尾的時候便完成了一次劃分點的尋找。如下圖所示:


實現程式碼如下:

#include <iostream>
#include <string>
#include <algorithm>
#include <stack>
#include <vector>
#include <fstream>
using namespace std;

struct ListNode {
	int val;
	ListNode *next;
	ListNode(int x) : val(x), next(NULL) {}
};

class Solution {
public:
	ListNode* getPartation(ListNode *start, ListNode *end)
	{
		if (start == end) return start ;

		ListNode *p1 = start ;
		ListNode *p2 = p1->next ;
		int key = start->val ;

		while(p2 != end)
		{
			if (p2->val < key)
			{
				p1 = p1->next ;
				swap(p1->val, p2->val) ; //找到一個比key小的數字,與p1到p2間的數交換,
			}										//這之間的數都大於等於key
			p2 = p2->next ;
		}
		swap(start->val, p1->val) ; //找到劃分位置
		return p1 ;
	} ;

	void QuickSort(ListNode* start, ListNode *end)
	{
		if (start != end)
		{
			ListNode *pt = getPartation(start, end) ;
			QuickSort(start, pt) ;
			QuickSort(pt->next, end) ;
		}
	}

	ListNode *sortList(ListNode *head) {
		QuickSort(head, NULL) ;
		return head ;
	}
};

void insertBack(ListNode** head, ListNode** tail,  ListNode* n) //從尾部插入
{	
	if (n)
	{
		if (*head == NULL)
		{
			*head = n ;
			*tail = n ;
		}
		else
		{
			(*tail)->next = n ;
			*tail = n ;
		}
	}
}

int main(int argc, char** argv)
{

	ifstream in("data.txt") ;
	ListNode* head = NULL ;
	ListNode* tail = NULL ;
	int val ;

	Solution s ;
	while(in >> val)
	{
		ListNode*tmp = new ListNode(val) ;
		insertBack(&head, &tail, tmp) ;
	}
	head = s.sortList(head) ;
	while(head)
	{
		cout << head->val << " " ;
		head = head->next ;
	}
	cout << endl ;
	return 0 ;
}
雖然使用快速排序的兩種方案都因為超時不能AC,但是練習一下還是很有幫助的。

如果大家發現那裡不對的地方還請批評指正,大家共同學習進步!先行謝過!