排序算法之——快速排序(剖析)


對於快速排序,相信大家都有聽過,這是一個被封為聖經的算法,足以體現算法的神奇的偉大。接下來本人將從算法思想、算法具體實現、不同情況下的復雜度、算法的優化和代碼展示等幾個方面為讀者深入講解。

 

一、算法思想

快速排序是一種分治的排序算法,主要操作是進行一趟操作將數據分成兩部分,左邊的部分都比基准值小,右邊的部分都比基准值大(非降序排序),切分的位置取決於數組的內容,再分別對兩部分進行同樣的遞歸操作。

了解二分查找的朋友應該很容易理解,不同的是,二分查找每次划分數據后只需對一邊進行操作,所以它的速度更快(二分查找的復雜度為log2n,(以2為底,n的對數))。

快速排序是對冒泡排序的改進(冒泡排序是對相鄰的兩個元素比較。快速排序是找到一個基准點,將數據划分成兩部分,對兩部分進行遞歸操作)

快速排序之所以快,是因為它采用了分治法的思想,把一個大的數據,分成多個小的塊,對小的塊進行處理,再將處理后的小塊合並再處理。

 

二、算法具體實現(三種實現)

1、前后指針法(重點介紹)

把前后指針法放到最前面,是因為它有獨有的優勢,由於指針只是單側的移動,對於單鏈表等只支持單向移動的數據結構,它依然能派上用場(另外兩種算法都需要兩指針分別向后向前移動)。

①將數組最左的元素記為key,創建指針i和j,i指向數組最左邊的元素,j指向i的下一位。

②比較array[j]和key:如果array[j]>key,j后移一位,i不動。若array[j]<key,交換i+1和j的值,ij同時后移一位。

③當j走到數組的末尾時,先執行②操作,再交換array[i]和key。

④對兩邊進行上述操作的遞歸操作。

一趟走完key左邊的數都小於key,key右邊的值都大於key,下面用一個例子來理解(希望讀者能動手驗證)。

 

 

取key=6;

j走到5時,5<6,交換5和i+1,即交換5和10,ij同時后移一位。

j走到3時,3<6,交換3和i+1,即交換3和13,ij同時后移一位。

j走到2時,2<6,交換2和i+1,即交換2和10,ij同時后移一位。

j走到11時,11>6,不用交換,此時j到達數組末尾,交換key和i,即交換6和2。

一趟排序完成,6左邊的元素都小於6,右邊的元素都大於6。

 

2、左右指針法(非降序)

 之所以叫左右指針,是因為初始的兩指針是在數據的兩端,看作左右,與前后指針法不同的是兩指針是往中間移動的。

①將最右的元素定為key,左指針最左的元素,右指針指向最右的元素。

②右指針向左走,如果遇到了比key小的數,右指針停下,接着左指針往右走,遇到比key大的數時停下。交換左右指針的值,兩指針再向中間移一位。

③檢查兩指針是否重合,不重合時進行②操作,重合時判斷key的值與重合位置的值,如果key<重合位置的值,進行交換。

④對兩邊進行上述操作的遞歸操作。

(放圖說明)

 

取key為5,初始化兩指針。

右指針向左移動,到2時2<5,停住。

左指針向右移動,到9時9>5,停住。

交換兩指針的值,兩指針都向中間移動一位。

兩指針相遇,交換key和指針位置的值,至此一趟排序完成。

 

 

3、填坑法(非降序)

 填坑法的坑只有一個,是由指針指向且不斷變化的,有些類似於左右指針法,具體不同會在下面分析。

①將數組最右的端定為坑,並記下坑值為key(一趟排序只記這一次),左指針最左的元素,右指針指向最右的元素。

②左指針向右移動,當遇到比坑值大的數時停下,將這個數填入坑中,將左指針此時的位置設為坑。右指針向左移,當遇到比key小的數時,將這個數填入坑中,將右指針此時的位置設為坑。

③判斷兩指針是否重合,若不重合執行②操作,重合就將初始的key值填入重合的位置。至此一趟排序結束。

(上圖中圈圈代表坑)

初始化兩指針,將最右端設為坑,並記下其值。

左指針向右移動,一開始就發現7>1,將7填入坑中,左指針停留。

右指針向左移動,一直到7都沒有找到比1小的數,這時兩指針重合,將key值填入重合位置。

(這只是一種特殊情況,請讀者自己作更多的數組來練習)

 

三種方法比較:

指針移動方向:前后指針法比較特別,兩指針都是朝一邊移動(另外兩個方法兩指針移動方向不同)。

兩指針的完成順序:左右指針法是兩指針都移動到位了,才執行交換,填坑法只要一個指針到位了就執行交換。

兩指針的作用:可以說左右指針法和填坑法是相同的。而前后指針法中一個指針決定了插入的位置,一個指針用來檢索元素。

最后key值的插入:前后指針法是后指針到頭后,交換key值和array[i](也就是前一個指針指向的值)。左右指針法和填坑法都是在兩指針相遇后將key值與指針重合位置的值交換。

 

三、復雜度(要想透徹理解務必看完)

通常情況下,算法在不同情況的復雜度是不同的,對於快排,是否也是這樣?下面我將對多種情況下的快排進行分析。

 

1、最優情況

根據快排的分治法思想,每次用key值將數組分為兩部分,試想怎樣才算是最優情況?

當key值每次都是放在最中間時,數據被分成均勻的兩半,這時就出現了最優情況。

讓我們看一眼遞歸樹:

得出在最優情況下快排的復雜度是nlgn

 

2、最壞情況

了解的最優情況,想一下最壞情況的樣子吧。

對於一組排序好的數組,我們一般不去驗證它是否排好,而是將它重新排一遍。想象一下用快排處理一組已經排好的數據,每次的key值不是最大就是最小,划分的兩部分,一部分沒有元素,剩下的n-1都在另一部分。這是很糟糕的情況,此時排序的效率大幅降低。

這時快排的復雜度達到了n²(對計算有疑問請在評論區提出)

 

3、固定位置排序

若每次排序key都被放在1/10或9/10的位置呢?請讀者參考上面的方式作出分析。

需要注意的是上圖標注的兩條路徑的長度是不一樣的(1/10的路徑更短),所以在右邊每層的相加后面出現小於Cn。

大家應該注意到,在T(n)的算式中沒有出現1/10,Cnlog(9/10)n中log(9/10)n代表的是最長路徑,Cn是每層的相加,而θ(n) 表示的是所以葉節點。

可以得出即使在固定位置的時候,我們依然是處於最優情況。

 

4、優劣交替排序

發揮想象力,如果在排序時,出現了第一次是最優排序,第二次是最差排序,第三次又回到最優排序...

這時對於整個排序而言,我們到底是在經歷最差情況還是最好情況?(請讀者在心中給出答案)

利用之前的分析和算式,給出下圖:

them中的式子用了等價代換(只是將n/2帶入U(n))

你猜對了嗎,不管猜對與否,你已經對算法有了深刻的認識。除了最壞情況外,快排的復雜度都是一樣的。

 

四、算法的優化

鄙人才疏學淺,在這里只提供兩個簡單的方法供大家參考。算法的思路都很簡單,具體的實現在后面代碼展示部分。

 

1、三數取中法

在上面的各種情況中,我們不難看出排序的好壞受制於key值,如果我們不選最大或最小的數作為key,那么就可以避開最壞情況,三數取中法就利用了這種思想。

做法:在數組的頭、中和尾各取一個元素,將三個數進行比較,選擇位於中間的一個數作為key。

 

2、加入插入排序

當排序進行到接近完成時,數組會被分得很小,這時我們可以用一些特定的算法對這些小塊排序,插入排序正適用於這種情況(復雜度為O(n²))。

做法:遍歷數組,每次將當前元素插入合適的位置。

 

五、代碼展示

 

1、前后指針法

 1 #include<iostream>
 2 using namespace std;
 3 #include<assert.h>
 4 int GetMidIndex(int *a,int left,int right);
 5 int PartSort(int *a,int left,int right);
 6 void QuickSort(int *a,int left,int right);
 7 void InsertSort(int* a,int n);
 8 void printArray(int *a,int n);
 9 int main()
10 {
11     int arr[] = { 1, 3, 6, 0, 8, 2, 9, 4, 7, 5 };
12     //int arr[] = { 49, 38, 65, 97, 76, 13, 27, 49 };
13     int n = sizeof(arr) / sizeof(arr[0]);
14     cout << "進行快速排序后:";
15     QuickSort(arr, 0, n - 1);
16     printArray(arr, n);
17     system("pause");
18     return 0;
19 }
20 
21 int GetMidIndex(int *a,int left,int right)
22 {//三數取中法 
23     int max,maxAdress;
24     int mid=left+(right-left)/2;
25     if(a[left]>=a[mid]){
26         max=a[left];
27         maxAdress=left;
28     }
29     else{
30         max=a[mid];
31         maxAdress=mid;
32     }
33     if(a[right]>max){
34         max=a[right];
35         maxAdress=right;
36     }
37 }
38 
39 int PartSort(int *a,int left,int right)
40 {//前后指針法的實現 
41     int mid=GetMidIndex(a,left,right);
42     swap(a[mid],a[left]);
43     int key=a[left];
44     int j=left+1;
45     int i=left;
46     while(j<=right&&i<j){
47         if(a[j]<key&&++i!=j){
48             swap(a[i],a[j]);
49         }
50         j++;//如果最后一個也比key小,j就會超出數組 
51     }
52     swap(a[left],a[i]);//交換key值和array[i] 
53     return i;    
54 }
55 
56 void QuickSort(int *a,int left,int right)
57 {//快排主要思想(分治法 
58     if(left>=right){
59         return;
60     } 
61     if((right-left)<5){
62         InsertSort(a,right-left+1);
63     }
64     int div=PartSort(a,left,right);
65     QuickSort(a,left,div-1);
66     QuickSort(a,div+1,right);
67 }
68 
69 void InsertSort(int* a,int n)//當數組規模較小或者存在多個局部有序的子數組時,算法的執行速度會更快
70 {//插入排序 每次將一個元素插入以排好的序列 
71     for(int i=1;i<n;i++){
72         for(int j=i;j>0&&a[j]<a[j-1];j--){
73             swap(a[j],a[j-1]);
74         }        
75     }
76 }
77 void printArray(int *a,int n)
78 {
79     for(int i=0;i<n;i++){
80         printf("%d ",a[i]);
81     }
82 }

 

2、左右指針法

#include<iostream>
using namespace std;
#include<assert.h>

int PartSort(int* array,int left,int right);
void QuickSort(int* array,int left,int right);
int GetMidIndex(int* a,int left,int right);
void InsertSort(int* a,int n);
void PrintArray(int* a,int n);
int main()//快速排序(填坑法) 
{
    int arr[]={1,4,2,7,8,5,3,9,0,6};
    int n=sizeof(arr)/sizeof(arr[0]);
    QuickSort(arr,0,n-1);
    PrintArray(arr,n);
    system("pause");
    return 0;
}

int PartSort(int* array,int left,int right)
{
    int mid=GetMidIndex(array,left,right);
    swap(array[mid],array[right]);
    int key=array[right];
    while(left<right){//除第一次外,left和right都是從坑開始移動 
        while(left<right&&array[left]<=key){
            ++left;
        }
        array[right]=array[left];
        while(left<right&&array[right]>=key){
            --right;
        }
        array[left]=array[right];
    }
    array[right]=key;
    return right;
}

void QuickSort(int* array,int left,int right)
{//快排核心思想(分治法 
    if(left>=right){
        return;
    }
    if(right-left<=5){
        //printf("insert!");
        InsertSort(array,right-left+1);
    }
    int div=PartSort(array,left,right);
    QuickSort(array,left,div-1);
    QuickSort(array,div+1,right);
} 

void InsertSort(int* a,int n)//當數組規模較小或者存在多個局部有序的子數組時,算法的執行速度會更快
{//插入排序 每次將一個元素插入以排好的序列 
    for(int i=1;i<n;i++){
        for(int j=i;j>0&&a[j]<a[j-1];j--){
            swap(a[j],a[j-1]);
        }        
    }
}

void PrintArray(int* a,int n)
{
    for(int i=0;i<n;i++){
        printf("%d ",a[i]);
    }
}

int GetMidIndex(int *a,int left,int right)
{//三數取中法(序列是正序或者逆序時,每次選到的樞軸都是沒有起到划分的作用。快排的效率會極速退化。 
    assert(a);
    int max,maxAdress;
    int mid=left+(right-left)/2;
    if(a[left]<=a[right]){
        if(a[mid]<a[left]){
            return left;
        }
        else if(a[mid]>a[right]){
            return right;
        }
        else{
            return mid;
        }
    }
    else{
        if(a[mid]<a[right]){
            return right; 
        }
        else if(a[mid]>a[left]){
            return left;
        }
        else{
            return mid;
        }
    }
}

 

3、填坑法

#include<iostream>
using namespace std;
#include<assert.h>

int PartSort(int* array,int left,int right);
void QuickSort(int* array,int left,int right);
int GetMidIndex(int* a,int left,int right);
void PrintArray(int* a,int n);
int main()//快速排序(填坑法) 
{
    int arr[]={1,4,2,7,8,5,3,9,0,6};
    int n=sizeof(arr)/sizeof(arr[0]);
    QuickSort(arr,0,n-1);
    PrintArray(arr,n);
    system("pause");
    return 0;
}

int PartSort(int* array,int left,int right)
{
    int mid=GetMidIndex(array,left,right);
    swap(array[mid],array[right]);
    int key=array[right];
    while(left<right){//除第一次外,left和right都是從坑開始移動 
        while(left<right&&array[left]<=key){
            ++left;
        }
        array[right]=array[left];
        while(left<right&&array[right]>=key){
            --right;
        }
        array[left]=array[right];
    }
    array[right]=key;
    return right;
}

void QuickSort(int* array,int left,int right)
{
    if(left>=right){
        return;
    }
    int div=PartSort(array,left,right);
    QuickSort(array,left,div-1);
    QuickSort(array,div+1,right);
} 

void PrintArray(int* a,int n)
{
    for(int i=0;i<n;i++){
        printf("%d ",a[i]);
    }
}

int GetMidIndex(int *a,int left,int right)
{//三數取中法(序列是正序或者逆序時,每次選到的樞軸都是沒有起到划分的作用。快排的效率會極速退化。 
    assert(a);
    int max,maxAdress;
    int mid=left+(right-left)/2;
    if(a[left]<=a[right]){
        if(a[mid]<a[left]){
            return left;
        }
        else if(a[mid]>a[right]){
            return right;
        }
        else{
            return mid;
        }
    }
    else{
        if(a[mid]<a[right]){
            return right; 
        }
        else if(a[mid]>a[left]){
            return left;
        }
        else{
            return mid;
        }
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM