十大排序算法——快速排序法【挖坑法、左右指針法、前後指針法和優化方式三路快排】(C語言)

快速排序法

快速排序是C.R.A.Hoare於1962年提出的一種劃分交換排序。它採用了一種分治的策略,通常稱其爲分治法(Divide-and-ConquerMethod)。

基本思想

從待排序序列中選取一個值作爲基準值key,通過一趟排序將待排序列分割成兩部分,其中一部分記錄的值不大於key,另一部分記錄的值不小於key,然後分別對這兩部分數據繼續進行排序,整個排序過程可以遞歸進行,以達到整個序列有序的目的。

實施基本步驟

1.選擇基準值:在待排序序列中選擇一個數作爲基準值。
2.用基準值分割數據:把待排序序列分割成兩個子區間,一個子區間數據都小於基準值,另一個子區間數據都大於基準值。
3.對分割後的子區間按1、2步繼續進行排序,直到子區間個數爲1(採用遞歸的方式進行)

選擇基準值(關鍵字)key方式

選擇待排序序列的哪個元素作爲基準值是非常重要的,因爲基準值影響到分割後兩個子序列的長度。對於分治算法,當每次劃分時,算法若都能分成兩個等長的子序列時,那麼分治算法效率會達到最大。總之,基準對整個算法的效率有着決定性影響,通過合理的選擇基準值使排序效率增加。

1.取頭尾位置

取待排序序列的第一個元素或者最後一個元素作爲基準值

//選擇頭作爲基準值
void quick_sort1(int *a,int left,int right)
{
	int key = a[left];  //選擇頭爲基準值(首先就要和最後一個數進行)
	return key;
}
//選擇尾作爲基準值
void quick_sort1(int *a,int left,int right)
{
	int key = a[left];  //選擇尾爲基準值(首先就要和第一個數進行)
	return key;
}

如果待排序序列是一個有序或者部分有序的,用頭尾作爲基準值,對快排的分割是非常不利的,因爲每次劃分只能使待排序序列減一。此時爲最壞情況失去了快速的效果。

2.隨機選取基準法

取待排序序列任意元素作爲基準值

 int quick_sort1(int a[],int lift,int right)
{
 	srand((unsigned)time(NULL));
	int key = rand() % (right - lift) + left;
	swap(&a[key],&a[right]);  //互換一下位置,爲了調用劃分函數時與上面方式下的代碼保持統一
 	return a[right];
}

選取基準值的位置是隨機產生的,所以分割也不會總是會出現劣質的分割,這是一種比較常見的優化方法

3.三數取中法

就是在待排序序列中取左端、中間、右端三個數,然後進行排序,將中間數作爲基準值。

//用三數取中法選擇基準值,對左端、中間、右端的三個數進行排序,把中間值作爲基準值,放在right-1的位置
void Pick_up(int *a,int left,int right)
{
    int mid = (left+right) / 2;
    if (a[left] > a[mid])
    {
        swap(&a[left],&a[mid]);
    }
    if (a[left] > a[right])
    {
        swap(&a[left],&a[right]);
    }
    if (a[right] < a[mid])
    {
        swap(&a[right],&a[mid]);
    }
    swap(&a[right-1],&a[mid]);
}

使用三數取中法消除了待排序序列有序情況下造成的極大消耗,同時也是對隨機數法下有可能出現的小概率事件的完善(當待排序序列有序的情況下隨機數法仍然有可能給我們的基準是序列頭尾的元素)

三種基準分治法

1.挖坑法

選擇頭爲基準值來說,思路就是:
頭爲初始坑,就從右往左找第一個小於基準值的數來填初始坑,就產生了第二個坑,在從左往右找第一個大於基準值的數來填第二個坑,一次反覆進行填坑,直到begin=end時,說明坑左邊的數不大於key,坑右邊的不小於key,就直接將key填入坑中,得到兩個子區間,在用遞歸的方式對子區間進行上述操作。(文字看不明白可以去博客園或者其他博客看看圖解方式,用數據操作一遍,我在是就省略圖解步驟啦)

//挖坑法-取頭爲基準值
void quick_sort1(int *a,int left,int right)
{
    if (left>=right) //如果左邊索引大於或等於右邊得索引就代表已經整理完成了一組
    { 
        return  ;
    }
    int begin = left;
    int end = right;
    int key = a[left];  //選擇頭爲基準值作爲初始坑(首先就要和最後一個數進行)
    while (begin < end)
    {
        while (begin < end && key <= a[end])  //從右往左找第一個小於基準值的數來填key
        {
            end--;  //從右往左尋找
        }
        a[begin]=a[end];
        while (begin < end && key >= a[begin])  //從左往右找第一個大於基準值的數來填key
        {
            begin++;  //從左往右尋找
        }
        a[end]=a[begin];
        
    }
    /*當begin=end時,說明坑左邊的數不大於key,坑右邊的不小於key,
    就直接將key填入坑中,得到兩個子區間*/
    a[begin]=key; 
    //遞歸操作
    quick_sort1(a,left,begin-1);   //左子區間
    quick_sort1(a,begin+1,right);  //右子區間
    
    
}

2.左右指針法

看下面這個圖就明白了
借用

//數值交換函數
void swap(int *a,int *b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}
//左右指針法-取尾爲基準值
void quick_sort2(int *a,int left,int right)
{
    if (left>=right) //如果左邊索引大於或等於右邊得索引就代表已經整理完成了一組
    { 
        return  ;
    }
    int key=a[right];  //取尾爲基準值(先從左往右找)
    int begin = left;
    int end = right;
    while (begin < end)
    {
        while(begin < end && a[begin] <= key)   //從左往右找第一個數比key大的
        {
            begin++;
        }
        while (begin < end && a[end] >=key)  //從右往左找第一個數比key小的
        {
            end--;
        }
        swap(&a[begin],&a[end]);         //將找到的大值與小值交換 
    }
    /*當begin和end相遇時,正好就是把key與begin交換,
    使得得到兩個子區間,左子區間都小於key,右區間都大於key*/
    swap(&a[begin],&a[right]);   
    //遞歸操作
    quick_sort2(a,left,begin-1);    //左子區間
    quick_sort2(a,begin+1,right);   //右子區間
}

3.前後指針法

基本思路:
定義兩個指針,一前一後,前面指針找比基數值大的數,後面指針找比基數值小的數,前面的指針找到後,將前後指針所指向的數據交換,當前面的指針遍歷完整個數組時,將基準值與後指針的後一個位置的數據進行交換,然後以後指針的後一個位置作爲分界,然後將數組分開,進行遞歸排序。

//數值交換函數
void swap(int *a,int *b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}
//前後指針法-取尾爲基準值
void quick_sort3(int *a,int left,int right)
{
     if (left>=right) //如果左邊索引大於或等於右邊得索引就代表已經整理完成了一組
    { 
        return  ;
    }
    int pre = left-1;  //後指針
    int cur = left;   //前指針
    int key = a[right];  //取尾爲基準值(先從前往後找)
    while (cur < right)  
    {
        /*將前指針與基準值進行比較,前指針比基準值小將後指針自增1,前指針比基準值大將後指針不變
        此時後指針與前指針相等不交換位置, 不相等前後指針交換位置*/
        if (a[cur] < key && ++pre != cur)   
        {
            swap(&a[cur],&a[pre]);    //前後指針交換位置
        }
        cur++;   //前指針向後移動
        
    }
    /*當cur==right時,pre往後移動一位,交換pre與right位置的值,
    得到兩個子區間,使得左子區間值都小於pre位置的值,右子區間的值都大於pre位置的值*/
    swap(&a[++pre],&a[right]);
    //遞歸操作
    quick_sort3(a,left,pre-1);   //左子區間
    quick_sort3(a,pre+1,right);  //右子區間
}

三種優化方式

1.對於基準的選擇按情況而定(取頭尾位置、隨機選取基準法、三數取中法)

對於無序的序列可用取頭尾位置的
對於一個有序或者部分有序的可用隨機選取基準法和三數取中法

2.三路快排

三路排序算法把排序的數據分爲三部分,分別爲小於key,等於key,大於key,這樣三部分的數據中,等於key的數據在下次遞歸中不再需要排序,小於key和大於key的數據也不會出現某一個特別多的情況,通過此方式三路快速排序算法的性能更優。

適用於數據多且相等的數據多時

//數值交換函數
void swap(int *a,int *b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}
//三路快排法
void Three_way_quick_sort(int *a,int left,int right)
{
    if (left >= right)  //如果左邊索引大於或等於右邊索引就代表已經整理完成一個組了
    {
        return ;
    }
    int key = a[left];  //選擇left爲基準
    int lt = left;  //將<key的分界線的索引值lt初始化爲第一個元素的位置(也就是<key部分的最後一個元素所在位置)
    int gt = right+1;  //將>key的分界線的索引值gt初始化爲最後一個元素right的後一個元素所在位置(也就是>key部分的第一個元素所在位置)
    int i = left + 1;  //將遍歷序列的索引值i初始化爲left+1
    while (i < gt)
    {
        if (a[i] < key)  //如果當前位置元素<key,則將當前位置元素與=key部分的第一個元素交換位置
        {
            swap(&a[i],&a[lt+1]);  
            lt++;  //表示<key部分多一個元素
            i++;  //考慮下一個元素
        } 
        else if(a[i] > key)  //如果當前位置元素>key,則將當前位置元素與>key部分的第一個元素的前一個元素交換位置
        {
            swap(&a[i],&a[gt - 1]);  //此時i不用動,因爲交換過來的元素還沒有考慮他的大小
            gt--;  //表示>key部分多了一個元素
        } 
        else  //如果當前位置元素=key,則只需要將i++,表示=key部分多了一個元素
        {
            i++;
        }
        swap(&a[left],&a[lt]);  //上面的遍歷完成之後,將整個序列的第一個元素(也就是基準元素)放置在合適的位置
        Three_way_quick_sort(a,left,lt-1);  //對<key部分遞歸
        Three_way_quick_sort(a,gt,right);  //對>key部分遞歸
         
    }
    
}

3.小區間優化可用插入排序

當快排不斷遞歸處理子區間時,隨着子區間的不斷縮短,子區間數量快速增加,用快排處理這些區間很小且數量很多的子區間時,系統要爲每次的函數調用分配棧幀空間,這對我們是很不利的。當待排序序列的長度分割到一定大小後,繼續分割的效率比插入排序要差,此時使用插排對其優化。

//插入排序法
void Insertion_Sort(int *a,int left,int right)
{  
    if (!a)
    {
        return ;
    }
    
    if (left==right)
    {
        return ;
    }
    
    int i,j;
    for ( i = left; i <= right; i++)  //假設第一個數爲有序序列,所以數組下標從一開始
    {
        int temp=a[i];    //從無序序列中取一個數爲待插入數字,與有序序列中的數進行比較,找到合適的位置插入其中
        for ( j = i; j>0 && a[j-1]> temp; --j)  //判斷條件爲兩個,j>0爲數組邊界判斷,a[j-1]>temp爲插入的判斷條件
        {
            a[j]=a[j-1];
        }
        a[j]=temp;  //找到合適的位置插入其中
        
    }
}
//數值交換函數
void swap(int *a,int *b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}
//左右指針法-取尾爲基準值
void quick_sort2(int *a,int left,int right)
{
    if (left>=right) //如果左邊索引大於或等於右邊得索引就代表已經整理完成了一組
    { 
        return  ;
    }
    int key=a[right];  //取尾爲基準值(先從左往右找)
    int begin = left;
    int end = right;
    while (begin < end)
    {
        while(begin < end && a[begin] <= key)   //從左往右找第一個數比key大的
        {
            begin++;
        }
        while (begin < end && a[end] >=key)  //從右往左找第一個數比key小的
        {
            end--;
        }
        swap(&a[begin],&a[end]);         //將找到的大值與小值交換 
    }
    /*當begin和end相遇時,正好就是把key與begin交換,
    使得得到兩個子區間,左子區間都小於key,右區間都大於key*/
    swap(&a[begin],&a[right]);   
    //遞歸操作
    quick_sort2(a,left,begin-1);    //左子區間
    quick_sort2(a,begin+1,right);   //右子區間
}
//加入插入排序進行優化
void Insert_quick_sort(int *a,int left,int right)
{
    if (right-left<=15)
    {
        Insertion_Sort(a,left,right);
        return ;
    }
    else
    {
        quick_sort2(a,left,right);

    }
}

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