都2020年了,聽說你還不會歸並排序?手把手教你手寫歸並排序算法


本文介紹了歸並排序的基本思想,遞歸方法的一般寫法,最后一步步手寫歸並排序,並對其性能進行了分析。

基本思想

歸並排序是建立在歸並操作上的一種有效的排序算法,該算法是采用分治法的一個非常典型的應用。即先使每個子序列有序,再將已有序的子序列合並,得到完全有序的序列。這里給出一種遞歸形式的歸並排序實現。

遞歸方法的一般寫法

遞歸方法的書寫主要有三步:

  1. 明確遞歸方法的功能邊界;
  2. 得到遞歸的遞推關系;
  3. 給定遞歸的終止條件。

遞歸方法均可按照這三步進行,切忌不要陷入遞歸實現的細節中。下面以歸並排序算法的書寫為例,來談一下遞歸方法的具體寫法。

手寫歸並排序

首先,明確遞歸方法的功能,這里我們定義方法的功能為,給定一個數組及左右邊界,方法完成數組邊界內元素的排序,如下:

private static void mergeSort(int[] arr,int left,int right);

先假設我們已經有了這么一個方法,不用管具體的實現。
接着,尋找遞推關系,什么是遞推關系呢?就是如何由子問題的求解,來得到原問題的求解,還是舉例說明,有如下的數組
原始數組
我們將其拆分為左右兩部分,如下
拆分數組
遞推關系就是,假如左右兩部分都已經有序了,如何使整個數組有序?這個問題其實就是給定了一個數組,數組的左半部分有序,右半部分也有序,如何使整個數組有序?
首先,定義兩個指針,分別指向左側部分起始位置和右側部分起始位置,同時創建一個輔助數組和指向其初始位置的輔助指針
定義指針及輔助數組
接着比較,左指針和右指針所對應的元素的大小,較小的元素填充至輔助數組,同時其對應的指針和輔助指針均加1,如下:
比較並填充輔助數組
依次進行,直至某左指針指向中間位置或者右指針指向數組的末尾,此時要將將剩余的元素填充至輔助數組。所有的元素填充完成后,再將輔助數組中的元素填充回原數組即可。具體的代碼如下:

/**
     *
     * @param arr 要合並的數組
     * @param left 左邊界
     * @param mid 中間的分界
     * @param right 右邊界
     */
	private static void merge(int[] arr,int left,int mid,int right){
		int[] helpArr = new int[right - left + 1];//首先定義一個輔助數組
		int lPoint = left;//左指針
		int rPoint = mid  + 1;//右指針
		int i = 0;//輔助指針
		while(lPoint <= mid && rPoint <= right){//比較並填充輔助數組
			if(arr[lPoint] <=  arr[rPoint])
				helpArr[i++] =  arr[lPoint++];
			else
				helpArr[i++] =  arr[rPoint++];
		}
		while(lPoint <= mid){//將剩余元素填充至輔助數組
			helpArr[i++] =  arr[lPoint++];
		}
		while(rPoint <= right){
			helpArr[i++] =  arr[rPoint++];
		}
		for(int j = 0;j < helpArr.length;j ++){//將輔助數組中的元素回填至原數組
			arr[left + j] = helpArr[j];
		}
	}

最后,確定終止條件,一般是數組為空或者數組中只有一個元素,返回即可。
現在我們可以寫出整個歸並排序的代碼了,如下:

private static void mergeSort(int[] arr,int left,int right){
		if(arr == null || right == left)//終止條件
			return ;
		int mid = left + (right - left) / 2;//確定分割的邊界
		mergeSort(arr,left,mid);//對左半部分調用遞歸方法,使其有序
		mergeSort(arr,mid + 1,right);//對右半部分調用遞歸方法,使其有序
		merge(arr,left,mid,right);//合並左右兩部分,使整個數組有序
	}

為了保證形式的統一,再對函數進行一下封裝,如下,這就是我們的歸並排序了。

	/**
	 * 歸並排序算法
	 * @param arr
	 */
	public static void mergeSort(int[] arr){
		mergeSort(arr,0,arr.length - 1);//調用寫好的遞歸版歸並排序方法
	}

至此,我們便完成了歸並排序算法的代碼實現。

性能分析

在分析歸並排序算法性能之前,先介紹幾個基礎的概念。
時間復雜度:一個算法執行所消耗的時間;
空間復雜度:運行完一個算法所需的內存大小;
原地排序:在排序過程中不申請多余的存儲空間,只利用原來存儲待排數據的存儲空間進行比較和交換的數據排序。
非原地排序:需要利用額外的數組來輔助排序。
穩定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 仍然在 b 的前面,則為穩定排序。
非穩定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 可能不在 b 的前面,則為非穩定排序。
下面我們分析下歸並排序算法的性能。
首先是時間復雜度。歸並排序算法在排序時首先將問題進行分解,然后解決子問題,再合並,所以總時間=分解時間+解決子問題時間+合並時間。分解時間就是把一個數組分解為左右兩部分,時間為一常數,即O(1);解決子問題時間是兩個遞歸方法,把一個規模為n的問題分成兩個規模分別為n/2的子問題,時間為2T(n/2);合並時間復雜度為O(n)。所以總時間T(n)=2T(n/2)+O(n)。這個遞歸問題的時間復雜度可以用下面的公式來計算
遞歸函數的時間復雜度計算公式
這個公式可針對形如:T(n) = aT(n/b) + f(n)的遞歸方程進行時間復雜度求解。帶入可知,歸並排序的時間復雜度為O(nlogn)。此外在最壞、最佳、平均情況下歸並排序時間復雜度均為O(nlogn)。
空間復雜度分析:在排序過程中使用了一個與原數組等長的輔助數組,估空間復雜度為O(n)。
穩定性分析:由排序過程可以知道,歸並排序是一種穩定排序。
是否原地排序:排序過程中用到了輔助數組,所以是非原地排序。
本文源碼已同步至github,地址:https://github.com/zhanglianchao/AllForJava/tree/master/src/algorithm/sort

覺得文章有用的話,點贊+關注唄,好讓更多的人看到這篇文章,也激勵博主寫出更多的好文章。
更多關於算法、數據結構和計算機基礎知識的內容,歡迎掃碼關注我的原創公眾號「超悅編程」。

超悅編程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM