1. 程式人生 > >【資料結構與演算法】內部排序之四:歸併排序和快速排序(含完整原始碼)

【資料結構與演算法】內部排序之四:歸併排序和快速排序(含完整原始碼)

前言  

    之所以把歸併排序和快速排序放在一起探討,很明顯兩者有一些相似之處:這兩種排序演算法都採用了分治的思想。下面來逐個分析其實現思想。

歸併排序

實現思想   

    歸併的含義很明顯就是將兩個或者兩個以上的有序表組合成一個新的有序表。歸併排序中一般所用到的是2-路歸併排序,即將含有n個元素的序列看成是n個有序的子序列,每個子序列的長度為1,而後兩兩合併,得到n/2個長度為2或1的有序子序列,再進行兩兩合併。。。直到最後由兩個有序的子序列合併成為一個長度為n的有序序列。2-路歸併的核心操作是將一維陣列中前後相鄰的兩個有序序列歸併為一個有序序列。

    下面一系列圖展示了2-路歸併排序的過程:

    原始無序序列:


    第一次需要對各相鄰元素進行兩兩歸併,歸併後結果如下:


    第三次需要對上圖中相鄰色塊的元素進行兩兩歸併,歸併後的結果如下:


    接下來便是最後一次兩兩歸併了,歸併後便的到了有序的序列,如下:


    第一次實現的程式碼

    根據2-路歸併操作的思想,站在節省輔助空間的角度上考慮,我寫出的歸併操作的程式碼如下:

/*
將有序的arr[start...mid]和有序的arr[mid+1...end]歸併為有序的arr[start...end]
*/
void Merge(int *arr,int start,int mid,int end)
{
	int i = start;
	int j = mid+1;
	int k = 0;
	//brr為輔助陣列,
	int *brr = (int *)malloc((end-start+1)*sizeof(int));

	//比較兩個有序序列中的元素,將較小的元素插入到brr中
	while(i<=mid && j<=end)
	{	
		if(arr[i]<=arr[j])
			brr[k++] = arr[i++];
		else
			brr[k++] = arr[j++];
	}

	//將arr序列中剩餘的元素複製到brr中
	//這兩個語句只可能執行其中一個
	while(i<=mid)
		brr[k++] = arr[i++];
	while(j<=end)
		brr[k++] = arr[j++];

	//將brr中的元素複製到arr中,使arr[start...end]有序
	for(i=0;i<k;i++)
		arr[i+start] = brr[i];

	//釋放brr所佔的記憶體,並將其置為空
	free(brr);    
	brr = 0;
}

    呼叫上面的函式,得到的歸併排序的程式碼應該是這樣的:

/*
對arr[start...end]內的元素進行歸併排序
歸併排序後的順序為從小到大
*/
void MSort(int *arr,int start,int end)
{
	if(start < end)
	{
		int mid = (start+end)/2;
		MSort(arr,start,mid);		//左邊遞迴排序
		MSort(arr,mid+1,end);		//右邊遞迴排序
		Merge(arr,start,mid,end);	//左右序列歸併
	}
}
/*
將該排序演算法封裝起來
*/
void Merge_Sort(int *arr,int len)
{
	MSort(arr,0,len-1);
}

    輸入任意陣列來測試,結果也是正確的。

    第二次實現的程式碼

    我看很多書上或者網上的例子給出的程式碼都要在Merge函式中多傳入一個用來儲存歸併後有序序列的陣列,並在MSort函式中另外宣告一個臨時陣列,傳給該引數,這樣每次遞迴呼叫的時候都要在區域性宣告一個臨時陣列,很不好。正當我對自己的程式碼感覺良好時,看到了下面這段話:

    Merge例程是精妙的。如果對Merge的每個呼叫均區域性宣告一個臨時陣列(本人備註:即在MSort函式中宣告),那麼在任意時刻就可能有logN個臨時陣列處於活動期,這對於小記憶體的機器是致命的。另一方面,如果Merge例程動態分配並釋放最小量臨時記憶體,那麼由malloc佔用的時間會很多。嚴密測試指出,由於Merge位於MSort的最後一行,因此在任一時刻只需要一個臨時陣列活動,而且可以使用該臨時陣列的任意部分。(摘自Weiss資料結構與演算法分析)

    很明顯,我沒有考慮malloc所帶來的效率損耗,而且這裡說得很好,由於Merge位於MSort的最後一行,因此每一次遞迴呼叫中只會存在一個臨時陣列,而不會有上一層遞迴中宣告的臨時陣列(已經釋放掉了)。

    為了避免遞迴使用malloc和free,我們還是用這種經典的實現方式的好,程式碼(一塊貼上完整的測試程式碼)如下:

/*******************************
		    歸併排序
Author:蘭亭風雨 Date:2014-02-28
Email:[email protected]
********************************/
#include<stdio.h>
#include<stdlib.h>

/*
將有序的arr[start...mid]和有序的arr[mid+1...end]歸併為有序的brr[0...end-start+1],
而後再將brr[0...end-start+1]複製到arr[start...end],使arr[start...end]有序
*/
void Merge(int *arr,int *brr,int start,int mid,int end)
{
	int i = start;
	int j = mid+1;
	int k = 0;

	//比較兩個有序序列中的元素,將較小的元素插入到brr中
	while(i<=mid && j<=end)
	{	
		if(arr[i]<=arr[j])
			brr[k++] = arr[i++];
		else
			brr[k++] = arr[j++];
	}

	//將arr序列中剩餘的元素複製到brr中
	//這兩個語句只可能執行其中一個
	while(i<=mid)
		brr[k++] = arr[i++];
	while(j<=end)
		brr[k++] = arr[j++];

	//將brr中的元素複製到arr中,使arr[start...end]有序
	for(i=0;i<k;i++)
		arr[i+start] = brr[i];
}

/*
藉助brr陣列對arr[start...end]內的元素進行歸併排序
歸併排序後的順序為從小到大
*/
void MSort(int *arr,int *brr,int start,int end)
{
	if(start < end)
	{
		int mid = (start+end)/2;
		MSort(arr,brr,start,mid);		//左邊遞迴排序
		MSort(arr,brr,mid+1,end);		//右邊遞迴排序
		Merge(arr,brr,start,mid,end);	//左右序列歸併
	}
}
/*
將該排序演算法封裝起來
*/
void Merge_Sort(int *arr,int len)
{
	int *brr = (int *)malloc(len*sizeof(int));
	MSort(arr,brr,0,len-1);
	free(brr);
	brr = 0;
}

int main()
{
	int num;
	printf("請輸入排序的元素的個數:");
	scanf("%d",&num);

	int i;
	int *arr = (int *)malloc(num*sizeof(int));
	printf("請依次輸入這%d個元素(必須為整數):",num);
	for(i=0;i<num;i++)
		scanf("%d",arr+i);

	printf("歸併排序後的順序:");
	Merge_Sort(arr,num);
	for(i=0;i<num;i++)
		printf("%d ",arr[i]);
	printf("\n");

	free(arr);
	arr = 0;
	return 0;
}

    小總結

歸併排序的最好最壞和平均時間複雜度都是O(n*logn),但是需要額外的長度為n的輔助陣列(每次遞迴呼叫前都會釋放上次遞迴中傳入到Merge函式的brr陣列),因此空間複雜度為O(n),而不會因為棧的最大深度為O(logn)而積累至O(n*logn)佔用額外空間是歸併排序不足的地方,但是它是幾個高效排序演算法(快速排序、堆排序、希爾排序)中唯一穩定的排序方法。

快速排序

     如名所示,快速排序是已知的平均時間複雜度均為O(n*logn)的幾種排序演算法中效率最高的一個,該演算法之所以特別快,主要是由於非常精煉和高度優化的內部迴圈,它在最壞情況下的時間複雜度為O(n*n),但只要稍加努力(正確選擇樞軸元素)就可以避免這種情形。

    本部分的重點在於對分治思想的理解和程式碼的書寫,不打算過多地討論樞軸元素的選擇,因為這本身就不是一個簡單的問題,筆者對此也沒有什麼研究,更不敢造次。先來看實現思想。

    實現思想

    快速排序的基本思想如下:

    1、從待排序列中任選一個元素作為樞軸;

    2、將序列中比樞軸大的元素全部放在樞軸的右邊,比樞軸小的元素全部放在其左邊;

    3、以樞軸為分界線,對其兩邊的兩個子序列重複執行步驟1和2中的操作,直到最後每個子序列中只有一個元素。

    一趟快速排序(以排序後從小到大為例)的具體做法如下:

    附設兩個元素指標low和high,初值分別為該序列的第一個元素的序號和最後一個元素的序號,設樞軸元素的值為val,則首先從high所指位置起向前搜尋到第一個值小於val的元素,並將其和val互換位置,而後從low所指位置起向後搜尋到第一個值大於val的元素,並將其和val交換位置,如此反覆 ,直到low=high為止。

    我們上面說交換位置,只是為了便於理解,我們在前面幾篇內部排序的博文中一直在強調,應儘量避免比較多的元素交換操作,因此下面的分析和程式碼的實現中,我們並不是採取交換操作,而是先將樞軸元素儲存在val變數中,然後每次遇到需要交換的元素時,先將該元素賦給val所在的位置,而後再將該元素所在位置“挖空”,之後的每一次比較,就用需要交換的元素來填充上次“挖空”的位置,同時將交換過來的元素所在的位置再“挖空”,以等待下次填充。

    同樣為了便於理解,我們以下面的序列為例來展示快速排序的思想。

    下圖為無序序列的初始狀態,我們選取val為第一個元素4,low和high分別指向4和5:

相關推薦

資料結構演算法內部排序歸併排序快速排序完整原始碼

前言      之所以把歸併排序和快速排序放在一起探討,很明顯兩者有一些相似之處:這兩種排序演算法都採用了分治的思想。下面來逐個分析其實現思想。歸併排序實現思想       歸併的含義很明顯就是將兩個或者兩個以上的有序表組合成一個新的有序表。歸併排序中一般所用到的是2-路歸併

資料結構演算法插入排序

 插入排序是演算法中的基礎入門和氣泡排序、選擇排序都是必要掌握的。他們都是對比排序,需要通過比較大小交換位置,進行排序。 插入排序的實現思路: 1、 從第一個元素開始,這個元素可以認為已經被排序。 2、取出下一個元素,在已排序的序列中從後往前掃描。 3、如果該元素小於小於前

資料結構演算法 ---快速排序

快速排序流程: 1.從數列中挑出一個基準值 2.將所有比基準值小的擺放在基準前面,所有比基準值大的擺在後面(相同的數可以放到任一邊);在這個分割槽退出之後,該基準就處於數列的中間位置。 3.遞迴地把“基準值前面的子數列”和“基準值後面的子數列”進行排序。   下面以數列

資料結構演算法------氣泡排序

 學習開發一年的時間裡,很少去了解排序演算法,氣泡排序也是最開始學習的樣子,靠死記硬背,沒有引入自己的理解。  對於什麼時間複雜度和空間複雜度和穩定性也不清楚其原委,或許在程式碼方面少了幾許的天分: 氣泡排序: 氣泡排序每一輪的比較都是前面的數和後面的數進行比較,並交

資料結構演算法連結串列——遞增排序

今天看書時偶然想到的問題,書上是要求將一個數據插入一個有序連結的線性連結串列中, 所以我想先進行連結串列內的資料排序在進行插入資料。 在這裡我只寫了排序的函式。   函式實現: void Sort(LinkList&list, int &n) {   f

資料結構演算法排序全家桶(十大排序詳解及其Java實現)---第七篇

本篇文章彙總了10種場常見的排序演算法,篇幅較長,可以通過下面的索引目錄進行定位查閱: 7、桶排序 一、排序的基本概念 1、排序的定義 排序:就是使一串記錄,按照其中的某個或者某些關鍵字的大小,遞增或遞減的排列起來

資料結構演算法八大排序整理python+java

1.氣泡排序 氣泡排序很簡單,就是從第一個數開始,把數依次和後面一個數比較,大的數交換位置,直到陣列中最後一個數。 與此同時用end限定陣列的結尾。 arry = [2,4,6,8,1,9,0] def swap(arry,i,j): tem = ar

資料結構演算法003—排序演算法Python

寫在前面 常見排序演算法可以分為兩大類: 非線性時間比較類排序:通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此稱為非線性時間比較類排序。 線性時間非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間執行,因此稱為線性時間非比

資料結構演算法——排序演算法

原文連結     由於研究生考試的需要,加上我對演算法的情有獨鍾,這段時間一直在研究演算法。跟大家分享一些我的經驗和想法:一、歡迎大家批評指正我錯誤的地方;二、歡迎大家補償自己的見解進來,我如果發現有獨到見解的評論,我會編輯新增到文章中來,並註明。希望給大家帶來

資料結構演算法紅黑樹 --- 第十

樹是一種非線性資料結構,這種資料結構要比線性資料結構複雜的多,因此分為三篇部落格進行講解: 第一篇:樹的基本概念及常用操作的Java實現(二叉樹為例) 第二篇:二叉查詢樹 第三篇:紅黑樹 第三篇:紅黑樹 開篇說明:對於紅黑樹的學習,近階段只需要掌握這種資料結構的思想、特點、適

資料結構演算法二叉查詢樹 --- 第十三篇

樹是一種非線性資料結構,這種資料結構要比線性資料結構複雜的多,因此分為三篇部落格進行講解: 第一篇:樹的基本概念及常用操作的Java實現(二叉樹為例) 第二篇:二叉查詢樹 第三篇:紅黑樹 本文目錄 1、二叉查詢樹的基本概念 2、二叉查詢樹的查詢操作 3、二叉查詢樹的插

資料結構演算法樹的基本概念及常用操作的Java實現二叉樹為例 --- 第十二篇

樹是一種非線性資料結構,這種資料結構要比線性資料結構複雜的多,因此分為三篇部落格進行講解: 第一篇:樹的基本概念及常用操作的Java實現(二叉樹為例) 第二篇:二叉查詢樹 第三篇:紅黑樹 本文目錄: 1、基本概念 1.1  什麼是樹 1.2  樹的

資料結構演算法回溯法解決裝載問題

回溯法解決裝載問題(約束函式優化) 解題思想 遍歷各元素,若cw+w[t]<=c(即船可以裝下),則進入左子樹,w[t]標記為1,再進行遞迴,若cw+r>bestw(即當前節點的右子樹包含最優解的可能),則進入右子樹,否則,則不遍歷右子樹。 完整程式碼實現如下 p

資料結構演算法回溯法解決N皇后問題,java程式碼實現

N皇后問題 問題描述 在8×8格的國際象棋上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法,這稱為八皇后問題。 延伸一下,便為N皇后問題。 核心思想 解決N皇后問題有兩個關鍵點。一是如何進行放置棋子,二是如何驗證棋子是否符合

資料結構演算法貪心演算法解決揹包問題。java程式碼實現

揹包問題(貪心演算法) 貪心演算法思想 簡單的說,就是將大問題轉化為最優子問題,例如本題所要求的,揹包容量有限,要想使物品的總價值最高,那麼,我們必須儘可能的選擇權重高的(即單位價值更高)的物品進行裝載。 在揹包問題中,物品是可拆的,即可以分成任意部分進行裝載,而最終實現的目標是

資料結構演算法線性表——刪除重複元素

線性表是一種隨機存取的結構,和連結串列不同,連結串列順序存取的結構。但是,線性表是一種順序儲存的結構,而連結串列是鏈式儲存結構。兩者都是線性的,但區別不同。   進入主題: 1.假如有一串資料元素,要求刪除其中的重複元素。 首先想到的是用兩層迴圈,第一層從第一個元素開始,第

資料結構演算法演算法

一、演算法定義 演算法(algorithm),在數學(算學)和電腦科學之中,為任何良定義的具體計算步驟的一個序列[1],常用於計算、資料處理(英語:Data processing)和自動推理。精確而言,演算法是一個表示為有限長[2]列表的有效方法(英語:Eff

資料結構演算法複雜度分析---第一篇

一、首先明確兩個問題: 1、為什麼需要對演算法進行復雜度分析? 實際上一個演算法執行所耗費的時間和空間是無法從理論上準確算出來的,必須在計算機上實際執行才知道,但是我們不可能對每個演算法都先在計算機上執行一遍,再決定採用其中效率最高的那個。所以我們就需要從理論上分析出每種

資料結構演算法一、基本

一、絮絮叨叨 計劃寫一系列資料結構與演算法的部落格: 一是給自己立個flag——堅持做完, 二是記錄自己的學習過程,總結和分享知識 1、Why? 面試 =》考查基礎 =》資料結構與演算法 工作 =》有助於理解、使用框架;優化程式,提升效率、效能 鍛鍊邏輯思

資料結構演算法二、陣列

一、線性表 1、定義 線性表(Linear List):零個或多個數據元素的有限序列。 序列(有序):若元素存在多個,則第一個元素無前驅,最後一個無後繼,其他每個元素都有且只有一個前驅和後繼 2、數