1. 程式人生 > >筆試面試常考排序演算法總結

筆試面試常考排序演算法總結

在筆試面試的過程中,常常會考察一下常見的幾種排序演算法,包括氣泡排序,插入排序,希爾排序,直接選擇排序,歸併排序,快速排序,堆排序等7種排序演算法,下面將分別進行講解。另外,我自己在學習這幾種演算法過程中,主要參考了MoreWindows Blog中的排序演算法,在此向他表示感謝,他寫的很詳細全面,有興趣可參考這裡寫連結內容中的白話經典演算法部分。

提示:以下排序均是以排序完成後序列從左往右非遞減為例講解

1:氣泡排序(bubbleSort)
氣泡排序是每次將亂序中的最大的數字通過兩兩交換的方式往後移動,直到序列有序為止。猶如水中的氣泡從下往上浮時,越來越大。該演算法共執行了n趟,每趟執行n-i次比較,所以其複雜度為O(n^2)。
基本的氣泡排序演算法程式如下所示:

//氣泡排序
void bubbleSort(int a[],int n)
{
    int i,j;
    for(i=0;i<n;++i)//共執行n趟
        for(j=1;j<n-i;++j)//每趟執行n-i次比較,選出一個最大值
        {
            if(a[j]<a[j-1])//通過兩兩比較使得大的資料“上浮”
                swap(a[j],a[j-1]);
        }
}

氣泡排序雖然簡單,但當序列部分有序或者基本有序時,基本的氣泡排序演算法會做一些無用功,可對原演算法進行一些改進,從而可以提高排序效率。

改進1:在氣泡排序中,通過前後2個數據的兩兩交換,來完成排序過程,而如果某一趟並沒有發生交換,說明此時序列已經有序,就可以終止排序過程。
改進的氣泡排序程式如下所示:

//改進的氣泡排序
void bubbleSort_advanced(int a[],int n)
{
    int i,j;
    bool flag;

    i=n;
    flag=true;//設定標誌位
    while(flag)//當標誌位為真時才執行某一趟
    {
        flag=false;
        for(j=1;j<i;++j)
        {
            if
(a[j]<a[j-1]) { swap(a[j],a[j-1]); flag=true;//當交換髮生時,標誌位為真 //顯然如果某一趟沒有發生交換,說明排序已經完成 } } i--; } }

改進2:待排序序列可能部分有序,我們可以在某一趟排序過程中確定序列最後有序的位置,從而可以減少排序趟數,簡化排序過程。
改進的氣泡排序程式如下:

//改進的氣泡排序
//其主要區別體現在對最後排序位置的提取上
void bubbleSort_advanced_2(int a[],int n)
{
    int i,j,flag;//flag表示某趟排序的最後位置
    flag=n;
    while(flag>0)
    {
        i=flag;
        for(j=1;j<i;++j)
        {
            if(a[j]<a[j-1])
            {
                swap(a[j],a[j-1]);
                flag=j;//表示flag之後的資料都已經有序,flag永遠小於i
            }
        }
        if(i==flag)
            return;//與改進方法1結合,i==flag說明某一趟沒有發生交換,即排序完成
    }
}

2:插入排序(insertSort)
插入排序可以簡單概括為:假定序列下標i之前資料是有序的,則從i-1位置資料開始,依次將其與i進行比較並交換(當該值不滿足插入條件,即該位置值大於i位置值時),最終找到一個合適的位置插入下標i資料,以形成一個更大的有序序列。
插入排序程式如下所示:

void InsertSort(int a[],int n)
{
    int i,j;
    for(i=1;i<n;++i)//從1開始,認為a[0]是有序的
        if(a[i]<a[i-1])
        {
            int temp=a[i];
            for(j=i-1;j>=0&&a[j]>temp;--j)
                a[j+1]=a[j];//在找到合適的插入點前,資料都往後移一位
            //a[j]為小於等於temp(不滿足a[j]>temp)的第一個位置
            a[j+1]=temp;//找到了合適的插入點
        }
}

3:希爾排序(shellSort)
希爾排序演算法可以概括為:先將整個待排序序列分割成若干個子序列(一般分成2個),分別進行直接插入排序,然後依次縮減增量再進行排序,待整個序列中整個元素增量為1時,再對全體元素進行一次直接插入排序。
希爾排序程式如下所示:

void shellSort(int a[],int n)
{
    int gap,i,j;
    for(gap=n/2;gap>0;gap/=2)
        for(i=gap;i<n;++i)//從gap位置開始比較
            for(j=i-gap;j>=0&&a[j]>a[j+gap];j-=gap)
                swap(a[j],a[j+gap]);
}

4:直接選擇排序(selectSort)
選擇排序簡單的說就是每次找到序列中的最小值,然後將該值放在有序序列的最後一個位置,以形成一個更大的有序序列。選擇排序進行n趟,每趟從i+1開始,每趟找到最小值下標min_index,再將a[min_index]與a[i]交換。
選擇排序程式如下所示:

void selectSort(int a[],int n)
{
    int i,j,min_index;
    for(i=0;i<n;++i)//找到未排序陣列中最小的那個元素,放在已排序部分的後面
    {
        min_index=i;
        for(j=i+1;j<n;++j)
            if(a[j]<a[min_index])
                min_index=j;
        swap(a[min_index],a[i]);
    }
}

5:歸併排序(mergeSort)
對於歸併排序,記好一句話即可:遞迴的分解+合併。另外歸併排序需要O(n)的輔助空間
歸併排序程式如下所示:

void mergeSortCore(int a[],int first,int mid,int last,int pTemp[])
{
    int i=first,j=mid;
    int m=mid+1,n=last;
    int k=0;
    while(i<=j&&m<=n)
    {
        if(a[i]>a[m])
            pTemp[k++]=a[m++];
        else
            pTemp[k++]=a[i++];
    }
    while(i<=j)
        pTemp[k++]=a[i++];
    while(m<=n)
        pTemp[k++]=a[m++];
    for(i=0;i<k;++i)//將輔助空間內的資料轉移到原始陣列
        a[first+i]=pTemp[i];
}

void mergeSort(int a[],int first,int last,int pTemp[])
{
    //遞迴的分解
    while(first<last)
    {
        int mid=(first+last)/2;
        mergeSort(a,first,mid,pTemp);//左邊有序
        mergeSort(a,mid+1,last,pTemp);//右邊有序
        mergeSortCore(a,first,mid,last,pTemp);//合併有序
    }
}

void main()
{
    int n;//資料長度
    int *pTemp=new int[n];
    if(pTemp==NULL)
        cerr<<"No Space!";
    mergeSort(a,0,n-1,pTemp);
    delete[] pTemp;
}

提示:記好歸併排序演算法由3個程式組成及每個程式的作用

6:快速排序(quickSort)
快速排序一般是選定第一個數為基準數,然後分別從後向前找比基準數小的數,從前向後找比基準數大的數,然後交換前後找到的數的位置,並在最後為基準數找到一個合適的位置,使得基準數左側的資料都比基準數小,基準數右側的資料都比基準數大,然後以基準數為界將序列分為左右2個子序列,最後利用遞迴分解的方法完成排序過程。
提示:在遇到選擇或者填空題時,在做某一趟的快速排序推算時,用“挖坑填數法”+“分治法”,而在寫程式時,用“交換法”+“分治法”。
快速排序程式如下所示:

void quickSort(int a[],int first,int last)
{
    int i=first,j=last;
    if(i>j)
        return;
    while(i<j)
    {       
        while(i<j&&a[j]>=a[first])//從後往前找小於基準數的位置
            j--;
        while(i<j&&a[i]<=a[first])//從前往後找大於基準數的位置
            i++;
        if(i<j)//注意,i,j不能相遇或交叉
            swap(a[i],a[j]);
    }
    swap(a[first],a[j]);
    quickSort(a,first,j-1);
    quickSort(a,j+1,last);
}

7:堆排序
首先介紹一下最大堆和最小堆:
最大堆:父結點鍵值大於等於任一子結點,對最大堆排序後得到遞增序列。
最小堆:父結點鍵值小於等於任一子結點,對最小堆排序後得到遞減序列。
以下以最小堆為例講解堆的插入,刪除,建立及排序過程:

堆的插入:插入值最開始放在最後的子結點,將插入值與根結點逐級比較,將較大的根結點向下移動,替換其子結點。
堆插入程式程式碼如下:

void MinHeapInsert(int a[],int i)
{
    int j=(i-1)/2;//找到i結點的根節點
    int temp=a[i];//將待插入新值儲存
    while(j>=0&&i!=0)
    {
        if(a[j]<=temp)//說明若將值放在此位置,此時的堆已是最小堆
            break;
        a[i]=a[j];//根結點向下移動
        i=j;
        j=(i-1)/2;
    }
    a[i]=temp;//找到了合適的位置,對i點進行賦值
}

堆的刪除:堆的刪除就是刪除根結點,然後再調整堆的過程。其調整過程為:每次拿2個子節點中最小的結點與根結點比較,並將較小的子結點向上移動,替換根結點。

void MinHeapDelete(int a[],int i,int n)//堆的刪除
{
    int j,temp=a[i];
    j=2*i+1;//節點i的左孩子
    while(j<n)
    {
        if(j+1<n&&a[j+1]<a[j])//在左右孩子中找到最小的
            j++;
        if(a[j]>=temp)//滿足該條件時說明原始堆有序
            break;
        a[i]=a[j];//小資料上移,替換其父節點
        i=j;
        j=2*i+1;//繼續處理
    }
    a[i]=temp;
}

堆的建立:有以下兩種方法:
法1:

void MakeMinHeap(int a[],int n)
{
    for(int i=0;i<n;++i)
        MinHeapInsert(a,i);
}

法2:

void MakeMinHeap(int a[],int n)//建立最小堆
{
    for(int i=n/2-1;i>=0;--i)//認為葉子節點都是符合最小堆的
        MinHeapDelete(a,i,n);
}

堆排序

void MinHeapSort(int a[],int n)//最小堆排序獲得的是遞減的陣列
{
    for(int i=n-1;i>=1;--i)
    {
        swap(a[i],a[0]);//每次“刪除”最小堆的根節點
        MinHeapDelete(a,0,i);//每次都是從0(根節點開始調整)
    }
}

補充:幾種排序演算法的效能比較
1:複雜度
平均複雜度:
O(N^2)的有氣泡排序、插入排序、選擇排序
O(N*logN)的有希爾排序、歸併排序、快速排序、堆排序

複雜度最壞情況:氣泡排序、插入排序、選擇排序、快速排序均為O(N^2),歸併排序,堆排序均為O(N*logN)。
補充:對於快速排序:最壞的情況,待排序的序列為正序或者逆序,每次劃分只得到一個比上一次劃分少一個的子序列,另外一個為空。如果遞迴樹畫出來,就是一顆斜樹。此時需要執行n-1次遞迴呼叫,且第i次劃分需要經(n-i)次關鍵字比較才能找到才能找到第i個記錄,因此比較的次數為(n-1)+(n-2)+…+1 = n*(n-1)/2,最終時間複雜度為O(n^2)。

複雜度最好情況:氣泡排序、插入排序均為O(N),選擇排序仍為O(N^2),歸併排序,快速排序,堆排序仍為O(N*logN)。

另可注意到:最好、最壞、平均三項複雜度全是一樣的、就是與初始排序無關的排序方法為:選擇排序、堆排序、歸併排序

2:空間複雜度
除歸併排序空間複雜度為O(N),快速排序空間複雜度為O(logN)外,其他幾種排序方法空間複雜度均為O(1)。

3:穩定性
所謂排序過程中的穩定性是指:假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,則稱這種排序演算法是穩定的;否則稱為不穩定的。
為穩定排序的有:氣泡排序,插入排序,歸併排序;其餘幾種均為非穩定排序。

補充:找出若干個數中最大/最小的前K個數(K遠小於n),用什麼排序方法最好?
答:用堆排序是最好的。建堆O(n),k個數據排序klogn,總的複雜度為n+klogn。不考慮桶排序,n+klogn小於n*logn只有在k趨近n時才不成立,所以堆排序在絕大多數情況下是最好的。