數據結構之排序算法之O(nlogn)

上一次寫了冒泡,選擇,和插入排序。

這一次寫一下堆排序、歸併、快速排序。

堆排序

堆排序,主要是利用完全二叉樹在數組中的存儲方式(層序遍歷),i 位置的節點的兒子是 2i 和 2i+1 。

最大堆的定義是,每個結點的值都比他的兒子大的一顆完全二叉樹。

那麼我們排序的過程就是:

  • 首先將數組中的元素構建成一個最大堆
  • 然後將堆頂(根節點)的值拿出來,和最後一個元素交換,這樣最大的數就在最後了。
  • 然後將交換到堆頂的節點進行下沉操作,找到其在當前最大堆中的位置。那麼除了最後一個元素,前面的又是一個最大堆了。
  • 重複上面的兩步,直到只剩最大堆中只剩下一個元素。

那麼如何將數組中的元素構建成最大堆呢

  • 首先實現除根節點外,兩個兒子樹都已經是最大堆的情況,將根節點的值和兒子比較,然後和兒子中大的交換,交換後又是一個只有除根節點外,兒子樹是最大堆的情況,繼續交換,直到不需要交換(比兩個兒子大或者沒有兒子)

代碼如下:

void HeapAdjust(double *a,int begin,int end)
{
	if (!a||end<begin)
		return;

	double temp=a[begin];
	int i;
	for (i=2*begin ; i<=end ; i*=2)
	{
		if (i<end && a[i]<a[i+1])//找到根節點兒子中的較大值
			i++;
		if (temp>=a[i])//根節點已經比兒子大,不需要交換
			break;
		else            //交換,並更新現在根節點的位置。
		{
			a[begin]=a[i];
			begin=i;
		}
	}
	a[begin]=temp;
}
  • 剩下的就好辦了,首先從二叉樹最後一個節點的父節點開始,將其和和兒子比較,挑選父節點和兩個兒子節點3箇中的最大值,放在父節點上。
  • 然後處理最後一個父節點之前的父節點,這樣能夠保證每個處理的父節點而根節點的堆都符合第一條的要求。循環即可。

下面是堆排序代碼:

void HeapSort(double *a,int begin,int end)
{
	if (!a||end<begin)
		return;

	int i;
	for (i=end/2 ; i >=begin ; i--)  //生成最大堆
		HeapAdjust(a,i,end);

	for (i=end;i>begin;i--)
	{
		swap(&a[begin],&a[i]);       //交換堆頂元素和最有一個元素
		HeapAdjust(a,begin,i-1);     //更新交換堆頂後的最大堆
	}

}

堆排序分析,HeapAdjust函數的時間複雜度爲 O(logN),因爲完全二叉樹的高度就是 logN,每次堆頂元素下降最多下降 logN 次。

綜合起來,HeapAdjust一共執行了 O(2N) 次,那麼複雜度就是 O(2NlogN)=O(NlogN)。


歸併排序

歸併排序的思想非常簡單,先是 1對1 排序,然後 2對2 ,然後 4對4 ,直到N,共執行了 logN 次,每次執行 O(N) 次,那麼複雜度就是 O(NlogN)。

下面是非遞歸的代碼,比較噁心,在寫的時候經常在劃分長度上出問題。

void Merge(double* a,double* temp,int begin,int mid,int end) //將a[begin]到a[end]中的數據歸併到temp[begin]到temp[end]中,mid是中間值
{
	if (!a || !temp || mid<begin || end<mid)
	{
		return;
	}
	int i,j,k;
	for (i=k=begin,j=mid+1 ; i<=mid&&j<=end ; )
	{
		if (a[i]<a[j])
			temp[k++]=a[i++];
		else
			temp[k++]=a[j++];
	}
	while (i<=mid)
	{
		temp[k++]=a[i++];
	}
	while (j<=end)
	{
		temp[k++]=a[j++];
	}
}

void MergePass(double* a,double* temp,int begin,int end,int sigleLength)
//將a[begin]到a[end]中的數據分段歸併到temp[begin]到temp[end]中,sigleLength是每一段的長度,執行完成後,每段內的數據是有序的
{
	int len=end-begin+1;
	int i=begin,j;
	while (i+2*sigleLength-1 <= end)
	{
		Merge(a,temp,i,i+sigleLength-1,i+2*sigleLength-1);
		i+=2*sigleLength;
	}
	if (i+sigleLength-1 < end)
		Merge(a,temp,i,i+sigleLength-1,end);
	else
		for (j=i;j<=end;j++)
			temp[j]=a[j];
}

void MergeSort_xunhuan(double* a,int begin,int end)
{
	if (!a || end<begin)
	{
		return;
	}
	int i;
	int len=end-begin+1;
	double *temp=(double*)malloc(sizeof(double)*len);
	for (i=0;i<len;i++)
		temp[i]=0;
	i=1;
	while (i<=len)
	{
		MergePass(a,temp,begin,end,i);
		i*=2;
		MergePass(temp,a,begin,end,i);
		i*=2;
	}
}

快速排序

  • 首先在數組中找到一個值,然後對數組操作,保證這個值左邊的值都比它小,右邊的都比它大。
  • 然後以這個值所在的位置分開,數組變成兩個小數組,然後繼續執行上一步,直到所有的數組長度變成1,那就不需要比較了,所有的數字都已經排序完畢。

代碼:

int FindPos(double *p,int low,int high)
{
	double val = p[low];
	while (low<high)
	{
		while(low<high&&p[high]>=val)
			high--;
		p[low]=p[high];
		while(low<high&&p[low]<val)
			low++;
		p[high]=p[low];
	}
	p[low]=val;
	return low;
}

上面的函數實現了第一步的過程,還需要一個遞歸函數來實現第二步。

代碼:

void QuickSort(double *a,int low,int high)
{
	if (!a || high<=low)
		return;	

	if (low<high)
	{
		int pos=FindPos(a,low,high);
		QuickSort(a,low,pos-1);
		QuickSort(a,pos+1,high);
	}
}

到這裏還有優化的餘地,即第一步中選取中間值,是從數組第一個元素開始的,這個值在數組中排序與靠近中間越好,但是第一個值是中間值的概率很小,可以先找出 第一位、中間位、最後位 這3個值的中間值,放在數組首位,然後在執行函數,實際測試中,這個優化會大大提生快速排序的性能,優化前,數組很大的時候,由於重複遞歸,很快就會棧溢出,但是優化後棧溢出就很難出現了。

還有一個,當數組個數較少時,採用插入排序比快速排序更好,我們可以加個判斷,當數組長度小於128(或者其他的數),退出遞歸,這樣數組中就是分段有序的,然後再調用一遍插入排序,就可以了(在數組分段有序的情況下,插入排序的複雜度接近 O(N) )。當然也可以在長度小於128時,直接調用插入排序對這個小數組進行插入排序。經測試,這個優化會縮短一半的時間。

下面是優化後遞歸代碼。

void QuickSort(double *a,int low,int high)
{
	if (!a || high-low<128)
		return;

	int mid=(low+high)/2;
	if (a[low]>a[high])
		swap(&a[high],&a[low]);
	if (a[mid]>a[high])
		swap(&a[high],&a[mid]);
	if (a[low]<a[mid])
		swap(&a[mid],&a[low]);	

	if (low<high)
	{
		int pos=FindPos(a,low,high);
		QuickSort(a,low,pos-1);
		QuickSort(a,pos+1,high);
	}
}

void QuickSort_2(double *a, int low, int high)   //快速排序
{
	QuickSort(a, low, high);
	InsertSort(a, low, high);          //最後用插入排序對整個數組排序
}


3個排序分析,由於堆排序進行兩次 logN 操作,所以時間是歸併排序的2倍,而快速排序由於是遞歸的原因,是比不上非遞歸的歸併的,下面的測試結果也說明這一點。等以後寫了非遞歸的快速排序再測試一遍。

測試數組大小爲5000萬,浮點


 


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