1. 程式人生 > >快速排序的三種方式以及快排的優化

快速排序的三種方式以及快排的優化

一.快速排序的基本思想

關於快速排序,它的基本思想就是選取一個基準,一趟排序確定兩個區間,一個區間全部比基準值小,另一個區間全部比基準值大,接著再選取一個基準值來進行排序,以此類推,最後得到一個有序的數列。

二.快速排序的步驟

  • 1.選取基準值,通過不同的方式挑選出基準值。
  • 2.用分治的思想進行分割,通過該基準值在序列中的位置,將序列分成兩個區間,在準值左邊的區間裡的數都比基準值小(預設以升序排序),在基準值右邊的區間裡的數都比基準值大。
  • 3.遞迴呼叫快速排序的函式對兩個區間再進行上兩步操作,直到呼叫的區間為空或是隻有一個數。

三.關於選取基準值的方式

1.固定位置選取基準值
基本思想:選取第一個或最後一個元素作為基準值。
這裡寫圖片描述


如上是以第一個數作為選取基準的方式的第一趟排序的結果,接著就是對分好的兩個區間再進行遞迴的快排。

但是,這種選取基準值的方法在整個數列已經趨於有序的情況下,效率很低。比如有序序列(0,1,2,3,4,5,6,7,8,9),當我們選取0為基準值的時候,需要將後面的元素每個都交換一遍,效率很低。所以這種以固定位置選取基準值的方式,只適用於該序列本身並不是趨於有序的情況下,比如一串隨機數列,此時的效率還能夠差強人意。

為了避免這種已經有序的情況,於是有了下面兩種選取基準值的方式

下面是關於選取固定基準值快排的程式碼

int SelectPivot(int* a, int left, int right)//選取基準值函式
{
    return
a[left]; } void QuickSort(int* a, int left, int right) { assert(a); int i, j; int pivot = SelectPivot(a, left, right);//確定基準值 if (left < right) { i = left + 1;//以第一個數left作為基準數,從left+1開始作比較 j = right; while (i < j) { if (a[i] > pivot)//如果比較的數比基準數大 { swap(a
[i], a[j]);//把該比較數放到陣列尾部,並讓j--,比較過的數就不再比較了 j--; } else { i++;//如果比較的數比基準數小,則讓i++,讓下一個比較數進行比較 } } //跳出while迴圈後,i==j //此時陣列被分成兩個部分,a[left+1]-a[i-1]都是小於a[left],a[i+1]-a[right]都是大於a[left] //將a[i]與a[left]比較,確定a[i]的位置在哪 //再對兩個分割好的部分進行排序,以此類推,直到i==j不滿足條件 if (a[i] >= a[left]) //這裡必須要用>=,否則相同時會出現錯誤 { i--; } swap(a[i], a[left]); QuickSort(a, left, i); QuickSort(a, j, right); } } //測試函式 int main() { int a[] = { 2,5,4,9,3,6,8,7,1,0}; const size_t n = sizeof(a) / sizeof(a[0]); QuickSort(a, 0, n - 1); Print(a, n); system("pause"); return 0; }

2.隨機選取基準
基本思想:選取待排序列中任意一個數作為基準值。
因為快排函式部分的程式碼是一樣的,只是選取基準值部分的函式不相同,下面只附上選取基準值函式的程式碼

int SelectPivot(int* a, int left, int right)//選取基準值函式
{
    srand((unsigned)time(NULL));
    int pivotPos;
    if (left < right)//這裡需要保證傳進來的left必須小於left
    {
        pivotPos = rand() % (right - left) + left;
    }
    else
    {
        pivotPos = left;//在遞迴呼叫裡走到這一步,肯定是left=right,直接讓pivotPos=left
    }

    swap(a[pivotPos], a[left]);
    return a[left];
}

引入隨機化快速排序的作用,就是當該序列趨於有序時,能夠讓效率提高,大量的測試結果證明,該方法確實能夠提高效率。但在整個序列數全部相等的時候,隨機快排的效率依然很低,它的時間複雜度為O(N^2),但出現這種最壞情況的概率非常的低,所以它還是一種效率比較好的方法,一般情況下都能夠達到O(N*lgN)。

3.三數取中法選取基準值
基本思想:取第一個數,最後一個數,第(N/2)個數即中間數,三個數中數值中間的那個數作為基準值。舉個例子,對於int a[] = { 2,5,4,9,3,6,8,7,1,0};,‘2’、‘3’、‘0’,分別是第一個數,第(N/2)個是數以及最後一個數,三個數中3最大,0最小,2在中間,所以取2為基準值。

下面也是附上三數取中法的程式碼

int SelectPivot(int* a, int left, int right)//選取基準值函式
{
    int mid;
    if (left < right)
    {
        mid = (right - left) / 2;
    }
    else
    {
        return a[left];//在遞迴呼叫裡走到這一步,肯定是left=right,直接讓pivotPos=left
    }

    if (a[mid] > a[right])
    {
        swap(a[mid], a[right]);
    }
    if (a[left] > a[right])
    {
        swap(a[left], a[right]);
    }
    if (a[mid] > a[left])
    {
        swap(a[mid], a[left]);
    }
    //上面三步完成之後,a[left]就是三個數中最小的那個數
    return a[left];
}

採用三數取中法很好的解決了很多特殊的問題,但對於很多重複的序列,效果依然不好。於是在這三種選取基準值的方法下,另外地還有三種優化方法。

四.快速排序的優化

優化一:當待排序序列的長度分割到一定大小後,使用插入排序。
優化原因:對於待排序的序列長度很小或是基本趨於有序時,快排的效率還是插排好。

自定義截止範圍:序列長度N=10。當待排序的序列長度被分割到10時,採用快排而不是插排。

if (n <= 10)//當整個序列的大小n<=10時,採用插排
{
    InsertSort(a, n);
}

優化二:在一次排序後,可以將與基準值相等的數放在一起,在下次分割時可以不考慮這些數。
這裡寫圖片描述

因為這一次改動的程式碼比較多,所以再繼續把所有的程式碼全部拿出來看一下

int SelectPivot(int* a, int left, int right)//選取基準值函式
{
    int mid;
    if (left < right)
    {
        mid = (right - left) / 2;
    }
    else
    {
        return a[left];//在遞迴呼叫裡走到這一步,肯定是left=right,直接讓pivotPos=left
    }

    if (a[mid] > a[right])
    {
        swap(a[mid], a[right]);
    }
    if (a[left] > a[right])
    {
        swap(a[left], a[right]);
    }
    if (a[mid] > a[left])
    {
        swap(a[mid], a[left]);
    }
    //上面三步完成之後,a[left]就是三個數中最小的那個數
    return a[left];
}

void QuickSort(int* a, int left, int right)
{
    assert(a);
    if (n <= 10)//當整個序列的大小n<=10時,採用插排
    {
        InsertSort(a, n);
        return;
    }
    int i, j;
    int pivot = SelectPivot(a, left, right);//確定基準值 
    if (left < right)
    {
        i = left + 1;//以第一個數left作為基準數,從left+1開始作比較
        j = right;

        while (i < j)
        {
            if (a[i] == pivot)//處理與基準值相等的數,都放到陣列末尾
            {
                swap(a[i], a[j]);
                --j;
            }
            else if (a[i] > pivot)//如果比較的數比基準數大
            {
                while (1)
                {
                    if (a[j] == pivot)//如果要換的數值等於基準值,讓j--,與前一個交換
                    {
                        --j;
                    }
                    else
                    {
                        break;
                    }
                }
                swap(a[i], a[j]);//把該比較數放到陣列尾部,並讓j--,比較過的數就不再比較了
                --j;
            }
            else
            {
                ++i;//如果比較的數比基準數小,則讓i++,讓下一個比較數進行比較
            }
        }

        //跳出while迴圈後,i==j
        //此時陣列被分成兩個部分,a[left+1]-a[i-1]都是小於a[left],a[i+1]-a[right]都是大於a[left]
        //將a[i]與a[left]比較,確定a[i]的位置在哪
        //再對兩個分割好的部分進行排序,以此類推,直到i==j不滿足條件

        if (a[i] >= a[left]) //這裡必須要用>=,否則相同時會出現錯誤
        {
            i--;
        }


        swap(a[i], a[left]);
        int tmp = right;//用tmp表示從後往前第一個不是基準值的數
        while (tmp > i)
        {
            if (a[tmp] == pivot)
            {
                --tmp;
            }
            else//else表示沒有與基準值重複的值
            {
                QuickSort(a, left, i - 1);
                QuickSort(a, i + 1, right);
                return;
            }
        }
        int pos = tmp;//因為後面要用到tmp,所以運算的話用一個pos來代替tmp進行運算
        int r = right;//因為後面要保證right還是先前的右邊界,所以運算的話用另外一個變數來表示
        int count = 0;
        while (pos > i&&r > tmp)
        {
            swap(a[pos], a[r]);
            --r;
            --pos;
            ++count;//換了一次讓count++
        }

        QuickSort(a, left, i-1);//對左區間快排,i-1是左區間的最後一個數
        QuickSort(a, right-count+1, right);//對右區間快排,right-count+1是右區間的第一個數
    }
}

這種聚集與基準值相等的值的優化方法,在解決資料冗餘的情況下非常有用,提高的效率也是非常多。

優化三:優化遞迴操作
快排函式在函式尾部有兩次遞迴操作,我們可以對其使用尾遞迴優化

優點:如果待排序的序列劃分極端不平衡,遞迴的深度將趨近於n,而棧的大小是很有限的,每次遞迴呼叫都會耗費一定的棧空間,函式的引數越多,每次遞迴耗費的空間也越多。優化後,可以縮減堆疊深度,由原來的O(n)縮減為O(logn),將會提高效能。

只是在尾部的遞迴呼叫的時候做了以下改變

while (left < right)
{
    QuickSort(a, left, i - 1);//對左區間快排,i-1是左區間的最後一個數
    left = right - count + 1;
}

其實這種優化編譯器會自己優化,相比不使用優化的方法,時間幾乎沒有減少。

所以到這裡,總結一下,對於快速排序(一組隨機陣列),效率最快的優化方案應該是三數取中法+插排+聚集相等元素,對於尾遞迴可以有也可以沒有,對於效率的變化改變不大。