數據結構和算法面試題系列—排序算法之基礎排序

這個系列是我多年前找工做時對數據結構和算法總結,其中有基礎部分,也有各大公司的經典的面試題,最先發布在CSDN。現整理爲一個系列給須要的朋友參考,若有錯誤,歡迎指正。本系列完整代碼地址在 這裏html

0 概述

排序算法也是面試中經常說起的內容,問的最多的應該是快速排序、堆排序。這些排序算法很基礎,可是若是平時不怎麼寫代碼的話,面試的時候總會出現各類bug。雖然思想都知道,可是就是寫不出來。本文打算對各類排序算法進行一個彙總,包括插入排序、冒泡排序、選擇排序、計數排序、歸併排序,基數排序、桶排序、快速排序等。快速排序比較重要,會單獨寫一篇,而堆排序見本系列的二叉堆那篇文章便可。git

須要提到的一點就是:插入排序,冒泡排序,歸併排序,計數排序都是穩定的排序,而其餘排序則是不穩定的。本文完整代碼在 這裏github

1 插入排序

插入排序是很基本的排序,特別是在數據基本有序的狀況下,插入排序的性能很高,最好狀況能夠達到O(N),其最壞狀況和平均狀況時間複雜度都是 O(N^2)。代碼以下:面試

/**
 * 插入排序
 */
void insertSort(int a[], int n)
{
    int i, j;
    for (i = 1; i < n; i++) {
        /*
         * 循環不變式:a[0...i-1]有序。每次迭代開始前,a[0...i-1]有序,
         * 循環結束後i=n,a[0...n-1]有序
         * */
        int key = a[i];
        for (j = i; j > 0 && a[j-1] > key; j--) {
            a[j] = a[j-1];
        }
        a[j] = key;
    }
}
複製代碼

2 希爾排序

希爾排序內部調用插入排序來實現,經過對 N/2,N/4...1階分別排序,最後獲得總體的有序。算法

/**
 * 希爾排序
 */
void shellSort(int a[], int n)
{
    int gap;
    for (gap = n/2; gap > 0; gap /= 2) {
        int i;
        for (i = gap; i < n; i++) {
            int key = a[i], j;
            for (j = i; j >= gap && key < a[j-gap]; j -= gap) {
                a[j] = a[j-gap];
            }
            a[j] = key;
        }
    }
}
複製代碼

3 選擇排序

選擇排序的思想就是第i次選取第i小的元素放在位置i。好比第1次就選擇最小的元素放在位置0,第2次選擇第二小的元素放在位置1。選擇排序最好和最壞時間複雜度都爲 O(N^2)。代碼以下:shell

/**
 * 選擇排序
 */
void selectSort(int a[], int n)
{
    int i, j, min, tmp;
    for (i = 0; i < n-1; i++) {
        min = i;
        for (j = i+1; j < n; j++) {
            if (a[j] < a[min])
                min = j;
        }
        if (min != i)
            tmp = a[i], a[i] = a[min], a[min] = tmp; //交換a[i]和a[min]
    }
}
複製代碼

循環不變式:在外層循環執行前,a[0...i-1]包含 a 中最小的 i 個數,且有序。數組

  • 初始時,i=0a[0...-1] 爲空,顯然成立。bash

  • 每次執行完成後,a[0...i] 包含 a 中最小的 i+1 個數,且有序。即第一次執行完成後,a[0...0] 包含 a 最小的 1 個數,且有序。數據結構

  • 循環結束後,i=n-1,則 a[0...n-2]包含 a 最小的 n-1 個數,且已經有序。因此整個數組有序。數據結構和算法

4 冒泡排序

冒泡排序時間複雜度跟選擇排序相同。其思想就是進行 n-1 趟排序,每次都是把最小的數上浮,像魚冒泡同樣。最壞狀況爲 O(N^2)。代碼以下:

/**
 * 冒泡排序-經典版
 */
void bubbleSort(int a[], int n)
{
    int i, j, tmp;
    for (i = 0; i < n; i++) {
        for (j = n-1; j >= i+1; j--) {
            if (a[j] < a[j-1])
                tmp = a[j], a[j] = a[j-1], a[j-1] = tmp;
        }
    }
}
複製代碼

循環不變式:在循環開始迭代前,子數組 a[0...i-1] 包含了數組 a[0..n-1]i-1 個最小值,且是排好序的。

對冒泡排序的一個改進就是在每趟排序時判斷是否發生交換,若是一次交換都沒有發生,則數組已經有序,能夠不用繼續剩下的趟數直接退出。改進後代碼以下:

/**
 * 冒泡排序-優化版
 */
void betterBubbleSort(int a[], int n)
{
    int tmp, i, j;
    for (i = 0; i < n; i++) {
        int sorted = 1;
        for (j = n-1; j >= i+1; j--) {
            if (a[j] < a[j-1]) {
                tmp = a[j], a[j] = a[j-1], a[j-1] = tmp;
                sorted = 0;
            }   
        }   
        if (sorted)
            return ;
    }   
}
複製代碼

5 計數排序

假定數組爲 a[0...n-1] ,數組中存在重複數字,數組中最大數字爲k,創建兩個輔助數組 b[]c[]b[] 用於存儲排序後的結果,c[] 用於存儲臨時值。時間複雜度爲 O(N),適用於數字範圍較小的數組。

計數排序

計數排序原理如上圖所示,代碼以下:

/**
 * 計數排序
 */
void countingSort(int a[], int n) 
{
    int i, j;
    int *b = (int *)malloc(sizeof(int) * n);
    int k = maxOfIntArray(a, n); // 求數組最大元素
    int *c = (int *)malloc(sizeof(int) * (k+1));  //輔助數組

    for (i = 0; i <= k; i++)
        c[i] = 0;

    for (j = 0; j < n; j++)
        c[a[j]] = c[a[j]] + 1; //c[i]包含等於i的元素個數

    for (i = 1; i <= k; i++)
        c[i] = c[i] + c[i-1];  //c[i]包含小於等於i的元素個數

    for (j = n-1; j >= 0; j--) {  // 賦值語句
        b[c[a[j]]-1] = a[j]; //結果存在b[0...n-1]中
        c[a[j]] = c[a[j]] - 1;
    }

    /*方便測試代碼,這一步賦值不是必須的*/
    for (i = 0; i < n; i++) {
        a[i] = b[i];
    }

    free(b);
    free(c);
}
複製代碼

擴展: 若是代碼中的給數組 b[] 賦值語句 for (j=n-1; j>=0; j--) 改成 for(j=0; j<=n-1; j++),該代碼仍然正確,只是排序再也不穩定。

6 歸併排序

歸併排序經過分治算法,先排序好兩個子數組,而後將兩個子數組歸併。時間複雜度爲 O(NlgN)。代碼以下:

/*
 * 歸併排序-遞歸
 * */
void mergeSort(int a[], int l, int u) 
{
    if (l < u) {
        int m = l + (u-l)/2;
        mergeSort(a, l, m);
        mergeSort(a, m + 1, u);
        merge(a, l, m, u);
    }
}
 
/**
 * 歸併排序合併函數
 */
void merge(int a[], int l, int m, int u) 
{
    int n1 = m - l + 1;
    int n2 = u - m;

    int left[n1], right[n2];
    int i, j;
    for (i = 0; i < n1; i++) /* left holds a[l..m] */
        left[i] = a[l + i];

    for (j = 0; j < n2; j++) /* right holds a[m+1..u] */
        right[j] = a[m + 1 + j];

    i = j = 0;
    int k = l;
    while (i < n1 && j < n2) {
        if (left[i] < right[j])
            a[k++] = left[i++];
        else
            a[k++] = right[j++];
    }
    while (i < n1) /* left[] is not exhausted */
        a[k++] = left[i++];
    while (j < n2) /* right[] is not exhausted */
        a[k++] = right[j++];
}
複製代碼

擴展:歸併排序的非遞歸實現怎麼作?

歸併排序的非遞歸實現實際上是最天然的方式,先兩兩合併,然後再四四合並等,就是從底向上的一個過程。代碼以下:

/**
 * 歸併排序-非遞歸
 */
void mergeSortIter(int a[], int n)
{
    int i, s=2;
    while (s <= n) {
        i = 0;
        while (i+s <= n){
            merge(a, i, i+s/2-1, i+s-1);
            i += s;
        }

        //處理末尾殘餘部分
        merge(a, i, i+s/2-1, n-1);
        s*=2;
    }
    //最後再從頭至尾處理一遍
    merge(a, 0, s/2-1, n-1);
}
複製代碼

7 基數排序、桶排序

基數排序的思想是對數字每一位分別排序(注意這裏必須是穩定排序,好比計數排序等,不然會致使結果錯誤),最後獲得總體排序。假定對 N 個數字進行排序,若是數字有 d 位,每一位可能的最大值爲 K,則每一位的穩定排序須要 O(N+K) 時間,總的須要 O(d(N+K)) 時間,當 d 爲常數,K=O(N) 時,總的時間複雜度爲O(N)。

基數排序

而桶排序則是在輸入符合均勻分佈時,能夠以線性時間運行,桶排序的思想是把區間 [0,1) 劃分紅 N 個相同大小的子區間,將 N 個輸入均勻分佈到各個桶中,而後對各個桶的鏈表使用插入排序,最終依次列出全部桶的元素。

桶排序

這兩種排序使用場景有限,代碼就略過了,更詳細能夠參考《算法導論》的第8章。

參考資料

相關文章
相關標籤/搜索