m數據結構 day26 排序(四)歸併排序(分治法):倒置的完全二叉樹

完全二叉樹對排序的謎之天賦

堆排序用了完全二叉樹的深度信息,排序效率很高。其實,用到完全二叉樹的排序算法,效率都很高。完全二叉樹很適合用來排序,可能是它本身的結構的原因吧。

歸併排序在某種意義上,也是利用/藉助了完全二叉樹,不過和堆排序利用的原理不同,它是利用了倒置的完全二叉樹。比如:
在這裏插入圖片描述在這裏插入圖片描述

歸併排序的根本思想:先分解爲多個有序子序列,再把多個有序子序列合併爲一個有序序列

聽起來好像沒做啥,其實正是合併的過程在完成排序這個關鍵操作。分解步驟只是鋪墊,爲了後面和合並排序奠定物質基礎(即把整個長度爲n的待排序序列分解爲n個長度爲1的子序列。)。而且,只是代碼需要分解操作,如果我們人直接拿着一個無序表企圖使用歸併方法排序的話,根本用不着分解,因爲一開始就是分解好的呀,把他看做是n個單獨的有序數據就好了。

所以合併纔是最核心的操作,因爲是 合併這個動作完成了排序的目標,所以名字叫做合併排序/歸併排序,不叫分解合併排序。
在這裏插入圖片描述

展示一個更完整的示例。上圖只是展示了合併過程,沒有展示分解的過程。
在這裏插入圖片描述

代碼

我覺得歸併排序的思想理解起來很簡單,但是代碼好難實現啊,我看了好幾遍了,總是隻能看到外層淺層的東西,看不深入,有種鑽不透徹的感覺

遞歸版本

/*SqList * L*/
MergeSort(L->arr, 1, L->length);//主函數中的調用語句
/*MergeSort函數遞歸調用自己實現把整個無序表分解爲n個長度爲1的子序列*/
//第一個參數傳的是指針,不是按值傳遞,所以後續遞歸操作的都是這一個數組
void MergeSort(int arr[], int left, int right)
{
	if (left >= right)
		return;
	int middle = (right + left) / 2;
	MergeSort(arr, left, middle);
	MergeSort(arr, middle+1, right);
	Merge(arr, left, middle, right);
}
/*把arr數組中下標爲left到middle,和下標爲middle+1到right的兩部分合並起來*/
void Merge(int arr[], int left, int middle, int right)
{
	int tmp[right - left + 1];//把待合併排序的數值放在這個數組裏暫存備用,而排序完成的數值則存在原數組arr中
	//從arr中獲取待歸併的元素,arr是待排序的整個無序表
	for (int i=left;i<=right;++i)
	{
		tmp[i - left] = arr[i];
	}
	int i = left, j = middle + 1;
	//給arr的下標left到下標right重新賦值(即賦排序完成後的值)
	for (int k = left;k < right;++k)
	{
		//注意左邊組和右邊組分別都已經是有序的,這裏只是合併他倆得到一個有序數組,但並未新建一個數組,而是仍然放在原數組arr中
		if (i>middle && j<=right)
		{
			arr[k] = tmp[j - left];//左邊組的數字已經排序完成,直接把右邊組的剩餘第一個數字放進arr
			++j;
		}
		else if (j>right && i<=middle)//右邊組的數字已經排序完成,直接把左邊組的剩餘第一個數字放進arr
		{
			arr[k] = tmp[i-left];
			++i;
		}
		//依次比較left和middle+1,left+1和middle+2,直到i爲middle或者j爲right
		else if (tmp[i-left] <= tmp[j-left])
		{
			arr[k] = tmp[i - left];//等於較小的那個值
			++i;
		}
		else if (tmp[i-left] > tmp[j-left])
		{
			arr[k] = tmp[j - left];//等於較小的那個值
			++j;
		}
	}
}

複雜度和穩定性:佔用內存多,但是效率高,並且很穩定

時間複雜度:最好,最壞,平均都是O(nlogn)O(n\log n)

空間複雜度:O(n+logn)O(n + \log n)

因爲待排序序列的每一個元素都要被分解出來,並兩兩比較處理啥的,所以要O(n)時間。

整個歸併排序需要和原始記錄序列同樣數量的存儲空間來輔助,並且遞歸深度是log2n\lceil \log_2 n \rceil
所以是這麼多

兩兩比較無跳躍:穩定

迭代版本(時空複雜度都更低,比遞歸更優)

如果要用歸併排序,儘量用非遞歸的版本。因爲不僅空間複雜度更低(O(n)O(n), 因爲不再需要遞歸調用的O(logn)O(\log n)的棧空間),而且時間性能上也有提升。

我現在不喜歡遞歸了。之前一直覺得它的思路很智慧,把問題分解爲一個個子問題,分治和回溯的思想很美麗。但是現在我慢慢發現,遞歸併不是簡單的用空間換時間,它是時間和空間都需要的更多!!!很多個血淋淋的例子了,不忍回憶。
而且能把用遞歸很好理解的東西轉換爲迭代法實現,更聰明。

迭代版本不需要先分解,而是像咱們人類一樣,直接從最小的長度爲1的序列開始歸併。

void MergeSort_Iter(SqList * L)
{
	int * TR = (int *)malloc(L->length * sizeof(int));
	int k = 1;
	while (k < L->length)
	{
		MergePass(L->r, TR, k, L->length);//把長度爲k的子序列歸併爲長度爲2k的子序列
		k = 2 * k;//子序列長度加倍
		MergePass(TR, L->r, k, L->length);
		 k = 2 * k;//子序列長度加倍
	}
}
/*實現歸併,把SR數組中兩個相鄰的長度爲s的子序列兩兩歸併到TR數組*/
void MergePass(int SR[], int TR[], int s, int n)
{
	int i = 1;
	int j;
	while (i <= n-2*s+1)
	{
		Merge(SR, TR, i, i+s-1, i+2*s-1);
		i = i + 2*s;
	}
	if (i<n-s+1)
		Merge(SR, TR, i, i+s-1, n);
	else
		for (j=i;j<=n;++j)
			TR[j] = SR[j];
}
//把有序的SR[i, ```, m]和有序的SR[m+1,```, n]歸併爲有序的TR[i,```,n]
void Merge(int SR[], int TR[], int left, int middle, int right)
{
	int j,k,q;
	//比較left和middle+1, left+1和middle+2···
	for (j=middle+1, k=left;left<=middle && j<=right;++k)
	{
		if (SR[left] < SR[j])
			TR[K] = SR[left++];
		else 
			TR[k] = SR[j++];
	}
	//比較和歸併結束後(左邊組沒數字或者右邊組沒數字了)
	if (left<=middle)//右邊組更短,右邊組沒數字了,把左邊組的剩餘有序數字SR[left,```,middle]直接複製到TR即可
	{
		for(q=0;q<middle-left;++q)
			TR[k+1] = SR[left+1];//
	}
	if (j <= right)//左邊組沒數字了,右邊還有,把SR[j,```,right]複製到TR
	{
		for (q=0;q<right-j;++q)
			TR[k+q] = SR[j+q];
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章