1. 程式人生 > >面試時該死的排序演算法

面試時該死的排序演算法

2014-07-09

程式設計師面試時考演算法是個很頭疼的問題,但還是不得不看一下。排序分為內部排序和外部排序,內部排序指待排序的記錄在記憶體中,外部排序的記錄數量很大,以至於記憶體放不下而放在外存中,排序過程需要訪問外存。這裡僅介紹內部排序,包括插入排序、交換排序、選擇排序、歸併排序、基數排序。

1 插入排序

1.1直接插入(straight insertion sort)

演算法思路:陣列{k1,k2,……,kn},排序一開始k1是一個有序序列,讓k2插入得到一個表長為2的有序序列,依此類推,最後讓kn插入上述表長為n-1的有序序列,得到表長為n的有序序列。

c實現的程式碼:

//    從小到大排序
    int a[]={98,97,34,345,33};
    int k=sizeof(a)/sizeof(a[0]);
    int j;
    for (int i=1; i<k; i++) {
        int temp=a[i];
        for (j=i-1; j>=0&&a[j]>temp; j--) {
            a[j+1]=a[j];
        }
        a[j+1]=temp;
    }



1.2折半插入(binary insertion sort)

演算法思路:當直接插入進行到某一趟時,對於r[i]來講,前面i-1個記錄已經按關鍵字有序。此時不用直接插入排序的方法,而改為折半查詢,找出r[i]應插入的位置。

c實現的程式碼:

//    從小到大排序
void binasort(int r[100],int n){
    for (int i=1; i<n; i++) {
        int temp =r[i];
        int low=0;
        int high=i-1;
        while (low<=high) {
            int middle=(low+high)/2;
            if (temp<r[middle]) {
                 high=middle-1;
             }else{
                 low=middle+1;
            }
         }
        for (int j=i-1; j>=low; j--) {
            r[j+1]=r[j];
        }
        r[low]=temp;
    }
 }

1.3希爾排序(shell sort)

演算法思路:“縮小增量”的排序方法,初期選用增量較大間隔比較,然後增量縮小,最後為1,希爾排序對增量序列沒有嚴格規定。設有組關鍵字{99,22,33,333,2,3,23,44},由小到大排序,這裡n=8,先第一個個增量取d1=4,那麼記錄分為4組,第一組r[0],r[4],第二組r[1],r[5],……在各組內部使用插入排序,使得每組內是有序的,接著取d2=2,分為兩組,d3=1,最後就程式設計有序序列。

c語言實現的程式碼:

//    從小到大排序
void shellsort(int r[100],int n){
    int k=n/2;
    while (k>0) {
        for (int i=k; i<n; i++) {
             int temp=r[i];
             int j=i-k;
             while ((r[j]>temp)&&(j>=0)) {
                r[j+k]=r[j];
                j=j-k;
            }
            r[j+k]=temp;

        }
        k/=2;

    }
 }

2 交換排序

2.1氣泡排序(bubble sort)

演算法思路:在排序過程,關鍵字較小的記錄經過與其他記錄的對比交換,好像水中的氣泡一樣,移到資料序列的最前面。

c語言實現的程式碼:

//    從小到大排序
void bubblesort(int r[100],int n){
    for (int i=0; i<n-1; i++) {
        for (int j=0; j<n-1-i; j++) {
             if (r[j]>r[j+1]) {
                int temp=r[j];
                r[j]=r[j+1];
                r[j+1]=temp;
            }
        }
    }
}

2.2快速排序(quick sort)

演算法思路:通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序。整個排序過程可以遞迴實現,也可以非遞迴實現。

c語言實現遞迴的快速排序的程式碼:

//    從小到大排序
void quickSort(int a[],int numsize){
    int i=0,j=numsize-1;
    int val=a[0];//指定參考值val大小
    if (numsize>1) { //確保陣列長度至少為2,否則無需排序
        while (i<j) {//迴圈結束條件
 //            從後向前搜尋比val小的元素,找到後填到a[i]中並跳出迴圈
             for (; j>i; j--) {
                if (a[j]<val) {
                    a[i]=a[j];
                    break;
                }
            }
//            從前向後搜尋比val大的元素,找到後填到a[j]中並跳出迴圈
            for (; i<j; i++) {
                 if (a[i]>val) {
                    a[j]=a[i];
                    break;
                }
            }
        }

        a[i]=val;//將儲存再val中的數放到a[i]中
        quickSort(a, i);//遞迴,對前i個數排序
        quickSort(a+i+1, numsize-1-i);//對i+1到numsize-1-i個數排序
    }

}

3 選擇排序

3.1 簡單選擇排序(simple selection sort)

演算法思路:對於一組關鍵字{k1,k2,……kn},將其從小到大排序,首先從k1,k2,……k3中選擇最小值Kz,在將Kz與k1對換;然後從k2……Kn中選最小值Kz,與k2交換。如此選擇和調換n-2趟,第n-1趟只要調換Kn-1 和Kn比較就好了。

c語言實現的程式碼:

//從小到大排序
void sisort(int r[100],int n){
    for (int i=0; i<n-1; i++) {
        int z=i;
        for (int j=i+1; j<n; j++) {
             if (r[z]>r[j]) {
                z=j;
            }
        }
        if (z!=i) {
            int temp=r[i];
            r[i]=r[z];
            r[z]=temp;
        }
    }
}

3.2堆排序(heap sort)

演算法思路:堆有兩個性質,一是堆中某個節點的值總是不大於或不小於其父節點的值,二是堆是一棵完全樹。以從大到小排序為例,首先要把得到的陣列構建為一個最小堆,這樣父節點均是小於或者等於子結點,根節點就是最小值,然後讓根節點與尾節點交換,這樣一次之後,再把前n-1個元素構建出最小根堆,讓根結點與第n-2個元素交換,依此類推,得到降序序列。

c語言實現程式碼:

//從大到小排序
//以i節點為根,調整為堆的演算法,m是節點總數,i節點的子結點為i*2+1,i*2+2
void heapMin(int r[100],int i,int m){
//    temp儲存根節點,j為左孩子編號
    int j,temp;
    temp=r[i];
    j=2*i+1;

    while (j>m) {
        if (j+1<m && r[j+1]<r[j]) {//在左右孩子中找最小的
            j++;
        }
        if (r[j]>=temp) {
            break;
        }

        r[i]=r[j];
        i=j;
        j=2*i+1;

    }
    r[i]=temp;

}
void heapSort(int r[100],int n){
// n/2-1最後一個非葉子節點
// 下面這個操作是建立最小堆
    for (int i=n/2-1; i>=0; i--) {
        heapMin(r, i, n);
    }
// 一下for語句為輸出堆頂元素,調整堆操作
    for (int j=n-1; j>=1; j--) {
// 堆頂與堆尾交換
        int temp=r[0];
        r[0]=r[j];
        r[j]=temp;
        heapMin(r, 0, j);
    }
//得到的就是降序序列
    for (int i=0; i<n; i++) {
        printf(" %d",r[i]);
    }
}

時間複雜度:O(n log2n)

4 歸併排序(merge sort)

4.1兩路歸併排序

演算法思路:它指的是將兩個順序序列合併成一個順序序列的方法。如有數列{6,202,100,301,38,8,1},第一次歸併後變成了{6,202},{100,301},{8,38},{1};第二次歸併後,{6,100,202,301},{1,8,38};第三次歸併後{1,6,8,38,100,202,301}。

程式碼實現分三步,通過自底向上實現歸併子演算法,一趟歸併掃描子演算法,二路歸併排序演算法

//歸併子演算法
//將有序的X[s..u]和X[u+1..v]歸併為有序的Z[s..v]
void merge(int X[], int Z[], int s, int u, int v)
{
    int i, j, q;
    i = s;
    j = u + 1;
    q = s;

    while( i <= u && j<= v )
    {
        if( X[i] <= X[j] )
            Z[q++] = X[i++];
        else
            Z[q++] = X[j++];
    }

    while( i <= u )   //將X中剩餘元素X[i..u]複製到Z
        Z[q++] = X[i++];
    while( j <= v )   //將X中剩餘元素X[j..v]複製到Z
        Z[q++] = X[j++];
}
/*
一趟歸併掃描子演算法
將參加排序的序列分成若干個長度為 t 的,且各自按值有序的子序列,然後多次呼叫歸併子演算法merge將所有兩兩相鄰成對的子序列合併成若干個長度為2t 的,且各自按值有序的子序列。
若某一趟歸併掃描到最後,剩下的元素個數不足兩個子序列的長度時:
若剩下的元素個數大於一個子序列的長度 t 時,則再呼叫一次歸併子演算法 merge 將剩下的兩個不等長的子序列合併成一個有序子序列;
若剩下的元素個數小於或者等於一個子序列的長度 t 時,只須將剩下的元素依次複製到前一個子序列後面。
*/

/* X[0..n-1]表示參加排序的初始序列
* t為某一趟歸併時子序列的長度
* 整型變數i指出當前歸併的兩個子序列中第1個子序列的第1個元素的位置
* Y[0..n-1]表示這一趟歸併後的結果
*/


void mergePass(int X[], int Y[], int n, int t)
{
    int i = 0, j;
    while( n - i >= 2 * t ) //將相鄰的兩個長度為t的各自有序的子序列合併成一個長度為2t的子序列
    {
        merge(X, Y, i, i + t - 1, i + 2 * t - 1);
        i = i + 2 * t;
    }

    if( n - i > t ) //若最後剩下的元素個數大於一個子序列的長度t時
        merge(X, Y, i, i + t - 1, n - 1);
    else //n-i <= t時,相當於只是把X[i..n-1]序列中的資料賦值給Y[i..n-1]
        for( j = i ; j < n ; ++j )
            Y[j] = X[j];
}
//二路歸併排序演算法
void mergeSort(int X[], int n)
{
    int t = 1;
    int *Y = (int *)malloc(sizeof(int) * n);
    while( t < n )
    {
        mergePass(X, Y, n, t);
        t *= 2;
        mergePass(Y, X, n, t);
        t *= 2;
    }
    free(Y);
}
void print_array(int array[], int n)
{
    int i;
    for( i = 0 ; i < n ; ++i )
        printf("%d ", array[i]);
    printf("\n");
}
int main()
{
    int array[] = {65, 2, 6, 1, 90, 78, 105, 67, 35, 23, 3, 88, -22};
    int size = sizeof(array) / sizeof(int);
    mergeSort(array, size);
    print_array(array, size);
    return 0;
}

時空複雜度:二路歸併排序演算法:將參加排序的初始序列分成長度為1的子序列使用mergePass函式進行第一趟排序,得到 n / 2 個長度為 2 的各自有序的子序列(若n為奇數,還會存在一個最後元素的子序列),再一次呼叫mergePass函式進行第二趟排序,得到 n / 4 個長度為 4 的各自有序的子序列, 第 i 趟排序就是兩兩歸併長度為 2^(i-1) 的子序列得到 n / (2^i) 長度為 2^i 的子序列,直到最後只剩一個長度為n的子序列。由此看出,一共需要 log2n 趟排序,每一趟排序的時間複雜度是 O(n), 由此可知

該演算法的總的時間複雜度是是 O(n log2n),但是該演算法需要 O(n) 的輔助空間,空間複雜度很大,是 O(n).

5 基數排序(radix sort)

演算法思路:

基數排序可以採用LSD(Least significant digital)或者MSD(Most significant digital),LSD的排序由鍵值的最右邊開始,MSD從最左邊開始。

以LSD為例,假設原來有一串數值如下所示:

73, 22, 93, 43, 55, 14, 28, 65, 39, 81

第一步

首先根據個位數的數值,在走訪數值時將它們分配至編號0到9的桶子中:

0

1 81

2 22

3 73 93 43

4 14

5 55 65

6

7

8 28

9 39

第二步

接下來將這些桶子中的數值重新串接起來,成為以下的數列:

81, 22, 73, 93, 43, 14, 55, 65, 28, 39

接著再進行一次分配,這次是根據十位數來分配:

0

1 14

2 22 28

3 39

4 43

5 55

6 65

7 73

8 81

9 93

第三步

接下來將這些桶子中的數值重新串接起來,成為以下的數列:

14, 22, 28, 39, 43, 55, 65, 73, 81, 93

這時候整個數列已經排序完畢;如果排序的物件有三位數以上,則持續進行以上的動作直至最高位數為止。

LSD的基數排序適用於位數小的數列,如果位數多的話,使用MSD的效率會比較好。

關於各個演算法的對比可以看百度百科的這張圖: sort