上一次寫了冒泡,選擇,和插入排序。
這一次寫一下堆排序、歸併、快速排序。
堆排序
堆排序,主要是利用完全二叉樹在數組中的存儲方式(層序遍歷),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萬,浮點