1. 程式人生 > >常見14種經典排序演算法(Java程式碼實現)

常見14種經典排序演算法(Java程式碼實現)

尊重原創,轉載請標明出處   http://blog.csdn.net/abcdef314159 ,想了解更多演算法題可以關注微信公眾號“資料結構和演算法”,每天一題為你精彩解答。

一,氣泡排序

排序演算法其實有很多,氣泡排序基本上算是最簡單的一種排序演算法了。他的原理就和他的名字一樣,通過不斷的比較把小的資料不斷的往前移。其實氣泡排序有很多變種,我們會一一來看。我們先看下最常見的一種程式碼

public static void bubbleSort1(int array[]) {
    int length = array.length;
    for (int i = 0; i < length - 1; i++) {
        for (int j = i + 1; j < length; j++) {
            if (array[j] < array[i]) {
                swap(array, i, j);
            }
        }
    }
}
                                             
public static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

上面排序的原理相當於氣泡從水中往上冒,會逐漸變大。首先拿第一個元素和後面的所有一個個比較,如果比後面的大就交換,所以始終會保證第一個元素是最小的,然後再從第二個第三個,以此類推,swap方法表示交換兩個數字的值。上面排序是每次都把待排陣列中最小的值放到前面,我們能不能每次都把待排陣列中最大的值放到後面呢,其實這種也是可以的,看下程式碼

public static void bubbleSort2(int array[]) {
    int length = array.length;
    for (int i = length - 1; i >= 0; i--) {
        for (int j = i - 1; j >= 0; j--) {
            if (array[j] > array[i]) {
                swap(array, i, j);
            }
        }
    }
}
                                           
public static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

這種排序和上面第一種的那個排序正好相反,它的原理相當於氣泡從大到小往水中鑽,越往上氣泡越大,越往下氣泡越小。我們還可以前後相鄰的兩兩比較,每一輪比較完了之後其實也會把最大的放到後面,我們看下程式碼

public static void bubbleSort3(int array[]) {
    int length = array.length;
    for (int i = 0; i < length - 1; i++) {
        for (int j = 1; j < length - i; j++) {
            if (array[j] < array[j - 1]) {
                swap(array, j, j - 1);
            }
        }
    }
}
                                     
public static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

如果陣列本來就是排序好的,或者後面的都已經排序好了,我們在一個個迴圈其實效率並不是很高。在排序的時候如果我們發現後面待排陣列已經排序好了,後面的我們就不用一個個再排序了,我們可以改進一下,看一下程式碼

public static void bubbleSort4(int array[]) {
    int length = array.length;
    boolean flag;
    for (int i = 0; i < length - 1; i++) {
        flag = true;
        for (int j = 1; j < length - i; j++) {
            if (array[j] < array[j - 1]) {
                swap(array, j, j - 1);
                flag = false;
            }
        }
        if (flag) break;
    }
}
                                     
public static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

如果不喜歡for迴圈,我們也可以改為while迴圈,看下程式碼

public static void bubbleSort5(int array[]) {
    int length = array.length;
    int i = 0, j;
    while (i < length - 1) {
        j = i + 1;
        while (j < length) {
            if (array[j] < array[i]) {
                swap(array, i, j);
            }
            j++;
        }
        i++;
    }
}
                                     
public static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

如果喜歡遞迴我們也可以改為遞迴的方式

public static void bubbleSort6(int[] array, int n) {
    if (n == 1)
        return;
    if (array == null || array.length == 0)
        return;
    //逐漸減少n,每次都是把最大的放在最後面,直到n為1
    for (int i = 0; i < n - 1; i++) {
        if (array[i] > array[i + 1]) {
            swap(array, i, i + 1);
        }
    }
    bubbleSort6(array, n - 1);
}
                                     
public static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

二,選擇排序

選擇排序是預設前面都是已經排序好的,然後從後面選擇最小的放在前面排序好的的後面,首先第一輪迴圈的時候預設的排序好的為空,然後從後面選擇最小的放到陣列的第一個位置,第二輪迴圈的時候預設第一個元素是已經排序好的,然後從剩下的找出最小的放到陣列的第二個位置,第三輪迴圈的時候預設前兩個都是已經排序好的,然後再從剩下的選擇一個最小的放到陣列的第三個位置,以此類推,直到所有資料都遍歷完為止。我們看一下程式碼。

private static void selectSort1(int[] array) {
    for (int i = 0; i < array.length - 1; i++) {
        int index = i;
        for (int j = i + 1; j < array.length; j++) {
            if (array[index] > array[j]) {
                index = j;
            }
        }
        if (i != index) {
            swap(array, i, index);
        }
    }
}
                                      
public static void swap(int[] A, int i, int j) {
    A[i] = A[i] + A[j];
    A[j] = A[i] - A[j];
    A[i] = A[i] - A[j];
}

我們看到,他和氣泡排序很像,但不同的是每次遇到需要交換的時候並沒有立即交換,而是記錄下他的index,等這一輪迴圈完了之後在交換,選擇排序很簡單,基本上沒什麼可說的,我們來看一下他的遞迴該怎麼實現

//呼叫方式selectSort2(array, 0);
private static void selectSort2(int array[], int index) {
    if (index == array.length)
        return;
    int min = index;
    for (int i = index + 1; i < array.length; i++) {
        if (array[i] < array[min]) min = i;
    }
    if (min != index) {
        swap(array, index, min);
    }
    selectSort2(array, ++index);
}
                                    
public static void swap(int[] A, int i, int j) {
    A[i] = A[i] + A[j];
    A[j] = A[i] - A[j];
    A[i] = A[i] - A[j];
}

三,插入排序

插入排序的原理是預設前面的元素都是已經排序好的,然後從後面逐個讀取插入到前面排序好的合適的位置,就相當於打撲克的時候每獲取一張牌的時候就插入到合適的位置一樣。插入排序可以分為兩種,一種是直接插入還一種是二分法插入,直接插入的原理比較簡單,就是往前逐個查詢直到找到合適的位置然後插入,第一次迴圈的時候相當於第一個元素是排序好的,然後第二個元素和第一個元素對比,確定是插入他的前面還是他的後面……,第n次迴圈的時候相當於前面n個元素都是排序好的,第n+1個元素在和前面的n個比較,找到合適的位置插入。二分法插入是先折半查詢,找到合適的位置然後再插入。我們先看一下簡單的直接插入排序程式碼

private static void insertSort1(int[] array) {
    for (int i = 1; i < array.length; i++) {
        int j;
        int temp = array[i];
        for (j = i - 1; j >= 0; j--) {
            if (array[j] > temp) {
                array[j + 1] = array[j];//往後挪
            } else {//如果前面沒有比他大則break
                break;
            }
        }
        array[j + 1] = temp;
    }
}

把後面的一個個插入到前面合適的位置,如果不喜歡for迴圈也可以改為while迴圈

private static void insertSort2(int[] array) {
    int length = array.length;
    int i = 0;
    while (i < length) {
        int j = i;
        int key = array[i];
        while (j > 0 && array[j - 1] > key) {
            array[j] = array[j - 1];
            j--;
        }
        if (i != j) {
            array[j] = key;
        }
        i++;
    }
}

如果資料很大的時候,到後面還要在一個個往前找合適的位置其實效率並不高,因為插入排序的原理就是相當於前面的資料都已經排序好了,然後從下一個開始找到前面合適的位置插入,既然我們已經知道前面的都是排序好的了,在一個個找其實很費時,我們完全可以使用二分法查詢來找到合適的位置然後再插入,關於二分法查詢在下一章會有介紹。我們先看一下程式碼

private static void insertSort3(int[] array) {
    int length = array.length;
    for (int i = 1; i < length; i++) {
        if (array[i - 1] > array[i]) {
            int key = array[i];
            int low = 0;
            int hight = i - 1;
            while (low <= hight) {
                int mid = (low + hight) >> 1;
                if (array[mid] > key) {
                    hight = mid - 1;
                } else {
                    low = mid + 1;
                }
            }
            for (int j = i; j > low; j--) {
                array[j] = array[j - 1];
            }
            array[low] = key;
        }
    }
}

如果看不明白沒關係,下一章會介紹二分法查詢。我們來看一下使用遞迴該怎麼寫

private static void insertSort4(int[] data, int n) {
    if (n < 2) return;
    insertSort4(data, --n);//相當於前面n-1個都已經排序好了
    int temp = data[n];//把最後一個元素插入到合適的位置
    int index = n - 1;
    while (index >= 0 && data[index] > temp) {
        data[index + 1] = data[index];
        index--;
    }
    data[index + 1] = temp;
}

四,快速排序

前面3種排序都非常簡單,也很好理解,前面在講到氣泡排序的時候說過氣泡排序是有很多變種,其實快速排序也有很多變種。快速排序原理是首先要找到一箇中樞,把小於中樞的值放到他前面,大於中樞的值放到他的右邊,然後再以此方法對這前後兩部分資料再分別進行快速,一直排序下去,直到不可再劃分為止,我們看下程式碼

private static void quickSort1(int[] array, int start, int end) {
    if (start < end) {
        int key = array[start];//用待排陣列的第一個作為中樞
        int i = start;
        for (int j = start + 1; j <= end; j++) {
            if (key > array[j]) {
                swap(array, j, ++i);
            }
        }
        array[start] = array[i];//先挪,然後再把中樞放到指定位置
        array[i] = key;
        quickSort1(array, start, i - 1);
        quickSort1(array, i + 1, end);
    }
}
                               
private static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

他的原理就是先找一箇中樞值,預設我們選擇第一個作為中樞值,然後對陣列進行遍歷,比中樞值小的放到前面,比中樞值大的放到後面,第一輪迴圈完後再把中樞值插入到這兩部分的中間,所以第一輪迴圈完之後的結果就是中樞值前面的數都比他小,後面的數都比他大。然後在對中樞值前面部分和中樞值後面部分在分別採用這種方法,我們來看一張圖加深一下理解

我們上面說過快速排序有很多變種,上面的方式就是中樞值先不變,待第一輪排完之後再把中樞值放到合適的位置。下面再來看另外一種方式,就是中樞值在比較的時候始終是變的,一會在前面一會在後面,當第一輪執行完之後,大於中樞值的都會放到他的後面,小於中樞值的都會放到他的前面,我們來看一下程式碼

private static void quickSort2(int[] array, int start, int end) {
    if (start < end) {
        int pivot = partition(array, start, end);
        quickSort2(array, start, pivot - 1);
        quickSort2(array, pivot + 1, end);
    }
}
                           
private static int partition(int[] array, int start, int end) {
    int pivot = start;
    while (start != end) {
        if (pivot != end) {
            /**
             * 第一次迴圈的時候用第一個元素作為中樞,和最後一個進行對比,
             * 如果小於最後一個元素,執行end--然後和倒數第二個元素進行
             * 對比,如果還小於繼續執行end--……。如果大於最後一個元素那
             * 麼就和最後一個元素交換,然後讓中樞值pivot指向最後一個元素,
             * 下一輪迴圈的時候會指向下面的else方法,然後和前面的元素進行
             * 比較,如果前面沒有大於他的則執行start++,如果有大於他的則
             * 和前面的交換。也即是說這個中樞的位置 始終是在變動的,一會在
             * 和麵一會在前面。
             * 比如陣列{5,2,6,3,8}
             * 第一輪迴圈的時候:首先5和最後第一個數8比較,由於5小於8所以
             * 中樞值5不動,然後5繼續和最後第二個數3比較,由於比3大,所以
             * 中樞值5和3交換,變為{3,2,6,5,8},然後中樞值5在和前面的2
             * 比較,由於5大於2,所以5不動,然後5在和6比較,由於小於6,所以
             * 中樞值5和6交換,交換結果為{3,2,5,6,8},所以第一輪執行完之後
             * 大於中樞值5的數都放到了他的後面,小於中樞值5的數都放到了他的
             * 前面,然後對前面和後面部分再分別使用此方法。
             */
            if (array[end] < array[pivot]) {
                swap(array, end, pivot);
                pivot = end;
            } else {
                end--;
            }
        } else {
            if (array[start] > array[pivot]) {
                swap(array, start, pivot);
                pivot = start;
            } else {
                start++;
            }
        }
    }
    return pivot;
}

如果還看不明白,我們可以換種方法,估計下面這種程式碼更好理解

private static void quickSort3(int[] array, int start, int end) {
    if (start < end) {
        int pivot = partition3(array, start, end);
        quickSort3(array, start, pivot - 1);
        quickSort3(array, pivot + 1, end);
    }
}
                           
private static int partition3(int[] array, int start, int end) {
    int pivot = array[start];// 採用子序列的第一個元素作為樞紐元素
    while (start < end) {//
        // 從後往前在後半部分中尋找第一個小於樞紐元素的元素
        while (start < end && array[end] >= pivot) {
            --end;
        }
        // 將這個比樞紐元素小的元素交換到前半部分
        swap(array, start, end);
        // 從前往後在前半部分中尋找第一個大於樞紐元素的元素
        while (start < end && array[start] <= pivot) {
            ++start;
        }
        swap(array, start, end);// 將這個樞紐元素大的元素交換到後半部分
    }
    return start;// 返回樞紐元素所在的位置
}

我們上面分析了快速排序的兩種方式,一種數中樞值不動,把小於中樞值的往前挪,大於中樞值的往後挪,最後再把中樞值放到指定的位置。另一種方法是中樞值始終是變動的,一會和後面的比較一會和前面的比較。我們下面再來看一種方式是上面兩種方式的結合,他是中樞值先不動,後面的資料前後兩兩比較,排完之後再把中樞值放到合適的位置,我們看下程式碼

private static void quickSort4(int[] array, int start, int end) {
    if (start < end) {
        int pivot = partition4(array, start, end);
        quickSort4(array, start, pivot - 1);
        quickSort4(array, pivot + 1, end);
    }
}
                          
private static int partition4(int[] array, int start, int end) {
    int pivot = start;
    start++;
    while (start != end) {
        while (start < end && array[start] < array[pivot]) {
            start++;
        }
        while (start < end && array[end] >= array[pivot]) {
            end--;
        }
        swap(array, start, end);
    }
    swap(array, --start, pivot);
    pivot = start;
    return pivot;
}

上面的快速排序我們都使用遞迴的方法,我們下面再來看一下非遞迴該怎麼寫

private static void quickSort5(int[] a, int start, int end) {
    Stack<Integer> temp = new Stack();
    temp.push(end);
    temp.push(start);
    while (!temp.empty()) {
        int i = temp.pop();//start
        int j = temp.pop();//end
        int k = partition5(a, i, j);
        if (k > i) {
            temp.push(k - 1);//end
            temp.push(i);//start
        }
        if (j > k) {
            temp.push(j);//end
            temp.push(k + 1);//start
        }
    }
}
                         
private static int partition5(int[] array, int start, int end) {
    int pivot = array[start];// 採用子序列的第一個元素作為樞紐元素
    while (start < end) {//
        // 從後往前在後半部分中尋找第一個小於樞紐元素的元素
        while (start < end && array[end] >= pivot) {
            --end;
        }
        // 將這個比樞紐元素小的元素交換到前半部分
        swap(array, start, end);
        // 從前往後在前半部分中尋找第一個大於樞紐元素的元素
        while (start < end && array[start] <= pivot) {
            ++start;
        }
        swap(array, start, end);// 將這個樞紐元素大的元素交換到後半部分
    }
    return start;// 返回樞紐元素所在的位置
}

五,歸併排序

要明白歸併排序,必須搞清楚遞迴的原理。歸併排序是將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。他採用的一種分治的演算法,先分然後在合併,我們看一下原理圖

我們來看一下歸併排序的程式碼

private static void mergeSort1(int array[], int left, int right) {
    if (left < right) {
        int center = (left + right) >> 1;
        mergeSort1(array, left, center);
        mergeSort1(array, center + 1, right);
        merge1(array, left, center, right);
    }
}
                        
private static void merge1(int[] data, int left, int center, int right) {
    int[] tmp = new int[data.length];
    int tempIndex = left;
    //_left是前半部分開始的位置,_right是後半部分開始的位置
    int _left = left;
    int _right = center + 1;
    while (_left <= center && _right <= right) {
        if (data[_left] <= data[_right]) {
            tmp[tempIndex++] = data[_left++];
        } else {
            tmp[tempIndex++] = data[_right++];
        }
    }
    while (_right <= right) {
        tmp[tempIndex++] = data[_right++];
    }
    while (_left <= center) {
        tmp[tempIndex++] = data[_left++];
    }
    while (left <= right) {
        data[left] = tmp[left++];
    }
}

他是先分然後在合併,合併的時候就相當於把兩個有序數組合併為一個有序陣列。我們看到上面程式碼在合併的時候申請了一個臨時陣列,每次合併的時候都要申請一個臨時陣列,這樣很浪費空間,其實我們只需要申請我們夠我們使用的就行了,不用申請太大的陣列,我們改一下

private static void mergeSort2(int array[], int left, int right) {
    if (left < right) {
        int center = (left + right) >> 1;
        mergeSort2(array, left, center);
        mergeSort2(array, center + 1, right);
        merge2(array, left, center, right);
    }
}
                       
public static void merge2(int[] data, int left, int center, int right) {
    int length = right - left + 1;
    //只需要申請夠我們使用的就行了
    int[] tmp = new int[length];
    int tempIndex = 0;
    //_left是前半部分開始的位置,_right是後半部分開始的位置
    int _left = left;
    int _right = center + 1;
    while (_left <= center && _right <= right) {
        if (data[_left] <= data[_right]) {
            tmp[tempIndex++] = data[_left++];
        } else {
            tmp[tempIndex++] = data[_right++];
        }
    }
    while (_right <= right) {
        tmp[tempIndex++] = data[_right++];
    }
    while (_left <= center) {
        tmp[tempIndex++] = data[_left++];
    }
    tempIndex = 0;
    while (tempIndex < length) {
        data[left + tempIndex] = tmp[tempIndex++];
    }
}

上面的程式碼都使用了遞迴,先分開在合併,我們來看下使用非遞迴來怎麼寫

public static void mergeSort3(int[] data) {
    int i = 1;
    while (i < data.length) {
        //原理很簡單,就是先兩個兩個合併,然後4個,然後8個……
        for (int j = 0; j + i < data.length; j += 2 * i) {
            merge3(data, j, j + i - 1, Math.min(j + 2 * i - 1, data.length - 1));
        }
        i = i << 1;
    }
}
                      
public static void merge3(int[] data, int left, int center, int right) {
    int length = right - left + 1;
    int[] tmp = new int[length];
    int tempIndex = 0;
    //_left是前半部分開始的位置,_right是後半部分開始的位置
    int _left = left;
    int _right = center + 1;
    while (_left <= center && _right <= right) {
        if (data[_left] <= data[_right]) {
            tmp[tempIndex++] = data[_left++];
        } else {
            tmp[tempIndex++] = data[_right++];
        }
    }
    while (_right <= right) {
        tmp[tempIndex++] = data[_right++];
    }
    while (_left <= center) {
        tmp[tempIndex++] = data[_left++];
    }
    tempIndex = 0;
    while (tempIndex < length) {
        data[left + tempIndex] = tmp[tempIndex++];
    }
}

六,堆排序

堆排序也可以理解為二叉樹排序,這裡的堆分為兩種,一種是大頂堆,一種是小頂堆,大頂堆就是根節點不小於他的兩個子節點,小頂堆就是根節點不大於他的兩個子節點,排序之前我們先要構建堆(也就是二叉樹),然後再從堆中一個個取資料,每次取完資料都要對堆進行一步調整,我們看一個圖來加深一下理解,先來看一下堆的構建過程

我們構建的是大頂堆,就是父節點不小於他的子節點(如果有子節點),堆構建完之後我們就可以排序了,因為我們構建的是大頂堆,所以根節點總是最大的,所以我們每次都把根節點拿走,然後再把最後一個節點放到根節點,但這樣又打破了堆的平衡,所以要往下調整……,重複上面步驟,直到堆為空位置。我們根據一張圖來看下堆排序的過程。

堆的排序就是每次把堆頂的元素拿走,再把最後一個元素放到堆頂,然後再往下調整。我們來看一下程式碼

private static void heapSort(int[] array) {
    int length = array.length;
    buildMaxHeap(array, length);
    for (int i = 0; i < length; i++) {
        swap(array, 0, length - 1 - i);
        maxHeapfy(array, 0, length - i - 1);
    }
}
                    
private static void maxHeapfy(int[] array, int i, int heapSize) {
    int left = i * 2 + 1;
    int right = i * 2 + 2;
    int largest = i;
    if (left < heapSize && array[left] > array[largest]) {
        largest = left;
    }
    if (right < heapSize && array[right] > array[largest]) {
        largest = right;
    }
    if (largest != i) {//把最大值給父節點
        swap(array, largest, i);
        maxHeapfy(array, largest, heapSize);
    }
}
                    
private static void buildMaxHeap(int[] array, int heapSize) {
    //從最後一個非葉子節點開始迴圈
    for (int i = (heapSize - 2) >> 1; i >= 0; i--) {
        maxHeapfy(array, i, heapSize);
    }
}
                    
public static void swap(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

七,基數排序

基數排序的方式可以採用最低位優先LSD(Least sgnificant digital)法或最高位優先MSD(Most sgnificant digital)法,LSD的排序方式由鍵值的最右邊開始,而MSD則相反,由鍵值的最左邊開始。我們這裡使用LSD法,原理就是一個數組我們首先根據他的個位進行排序,然後在根據十位,百位……,這裡最多排到多少位是根據他的最大值確定的,如果最大值有千位,我們必須要計算到千位,如果最多隻有十位,我們就計算到十位就可以了,每一位都排序完了之後,陣列也就排序成功了。假如我們有一組初始資料{2,16,97,113,56,211,789,8,0,29},我們使用一張圖來看下是怎麼排序的

他是先個位比較排序,然後再十位比較排序,然後再百位比較排序,等所有位都排序完了,整個陣列也就排序完成了,我們看下程式碼該怎麼寫

private static void radixSort1(int[] array) {
    int digitCount = 10;//從0到9最多10位數
    int maxCount = getBitCount1(getMaxNumbit1(array));
    int radix = 1;
    int[][] tempArray = new int[digitCount][array.length];
    for (int i = 0; i < maxCount; i++) {
        int[] count = new int[digitCount];
        for (int j = 0; j < array.length; j++) {
            int temp = ((array[j] / radix) % 10);
            tempArray[temp][count[temp]++] = array[j];
        }
        int index = 0;
        for (int j = 0; j < digitCount; j++) {
            if (count[j] == 0)
                continue;
            for (int k = 0; k < count[j]; k++) {
                array[index++] = tempArray[j][k];
            }
        }
        radix *= 10;
    }
}
                   
private static int getMaxNumbit1(int array[]) {
    int max = array[0];
    for (int i = 1, length = array.length; i < length; i++) {
        if (array[i] > max) {
            max = array[i];
        }
    }
    return max;
}
                   
private static int getBitCount1(int num) {
    int count = 1;
    int temp = num / 10;
    while (temp != 0) {
        count++;
        temp /= 10;
    }
    return count;
}

這就是基數排序,先排序每一位上的數字,等所有位上的數字都排序完了之後,整個陣列的排序也就全部完成了。但是上面程式碼還不是很完美,因為當出現負數的時候上面程式碼就沒法排序了,我們來想一下當出現負數的時候應該怎麼辦。如果出現負數的時候,我們可以讓每一位上的數字求餘之後再加上9,然後再放到臨時陣列中,因為求餘的最大負數是-9,加上9之後,就不會出現負數了,我們來看下程式碼

private static void radixSort2(int[] array) {
    int digitCount = 19;//從-9到9最多19位數
    int maxCount = getBitCount2(getMaxNumbit2(array));
    int radix = 1;
    int[][] tempArray = new int[digitCount][array.length];
    for (int i = 0; i < maxCount; i++) {
        int[] count = new int[digitCount];
        for (int j = 0; j < array.length; j++) {
            int temp = ((array[j] / radix) % 10) + 9;
            tempArray[temp][count[temp]++] = array[j];
        }
        int index = 0;
        for (int j = 0; j < digitCount; j++) {
            if (count[j] == 0)
                continue;
            for (int k = 0; k < count[j]; k++) {
                array[index++] = tempArray[j][k];
            }
        }
        radix *= 10;
    }
}
                  
private static int getMaxNumbit2(int array[]) {
    int max = array[0];
    int min = array[0];
    for (int i = 1, length = array.length; i < length; i++) {
        if (array[i] > max) {
            max = array[i];
        } else if (array[i] < min) {
            min = array[i];
        }
    }
    return max < -min ? -min : max;
}
                  
private static int getBitCount2(int num) {
    int count = 1;
    int temp = num / 10;
    while (temp != 0) {
        count++;
        temp /= 10;
    }
    return count;
}

我們來隨便找一段程式碼測試一下結果是否正確

int[] array2 = new int[10];
Random random = new Random();
for (int i = 0; i < array2.length; i++) {
    array2[i] = random.nextInt(1000) - 500;
}
System.out.println("排序前:" + Arrays.toString(array2));
radixSort2(array2);
System.out.println("排序後:" + Arrays.toString(array2));

我們看一下執行結果

排序前:[197, -75, -313, 474, 384, -202, -474, 401, 371, 163]
排序後:[-474, -313, -202, -75, 163, 197, 371, 384, 401, 474]

八,桶排序

桶排序是將陣列分散到有限的桶中,然後每個桶再分別排序,而每個桶的排序又可以使用其他排序方式進行排序,可以是桶排序也可以是其他排序。桶的大小可以隨便定,如果桶的數量足夠多就會變成我們後面介紹的計數排序,其實我們完全可以把桶固定在一個數量,根據陣列的大小來確定,也可以自己定,比如3個或者5個7個等,桶的大小確定之後,下一步就需要把陣列中的值一一存放到桶裡,小的值就會放到前面的桶裡,大的值就會放到後面的桶裡,中間的值就會放到中間的桶裡,然後再分別對每個桶進行單獨排序,最後再把所有桶的資料都合併到一起就會得到排序好的陣列,我們看下程式碼

private static void bucketSort1(int[] array, int bucketSize) {
    int arrayLength = array.length;
    int max = array[0];
    int min = array[0];
    for (int i = 0; i < arrayLength; i++) {
        if (array[i] > max)
            max = array[i];
        else if (array[i] < min)
            min = array[i];
    }
    //bucketSize表示每個桶存放資料的大小,bucketCount總共桶的數量
    int bucketCount = (max - min) / bucketSize + 1;
    List<List<Integer>> buckets = new ArrayList<>(bucketCount);
    for (int i = 0; i < bucketCount; i++) {
        buckets.add(new ArrayList<Integer>());
    }
               
    for (int i = 0; i < arrayLength; i++) {
        //根據value的大小存放到不同的桶裡,最終的結果是小的出現在前面的桶裡,
        //大的出現在後面的桶裡嗎,中間的也就在中間的桶裡了,然後再對每個桶分別
        //進行排序。
        buckets.get((array[i] - min) / bucketSize).add(array[i]);
    }
               
    int currentIndex = 0;
    for (int i = 0; i < buckets.size(); i++) {
        //取出每個桶的資料
        Integer[] bucketArray = new Integer[buckets.get(i).size()];
        bucketArray = buckets.get(i).toArray(bucketArray);
        //每一個桶進行排序,這裡面可以選擇其他排序演算法進行排序
        Arrays.sort(bucketArray);
        for (int j = 0; j < bucketArray.length; j++) {
            array[currentIndex++] = bucketArray[j];
        }
    }
}

這就是所謂的桶排序,首先要找到他的最大值和最小值,然後計算桶的數量,找出最小值是因為存放的時候要讓當前值減去最小值,否則當排序中有負數的時候存放到桶裡會報異常。

九,希爾排序

希爾排序也成縮小增量排序,原理是將待排序列劃分為若干組,每組都是不連續的,有間隔step,step可以自己定,但間隔step最後的值一定是1,也就說最後一步是前後兩兩比較。間隔為step的預設劃分為一組,先在每一組內進行排序,以使整個序列基本有序,然後再減小間隔step的值,重新分組再排序……不斷重複,直到間隔step小於1則停止。我們通過一張圖來看下希爾排序的原理。

上面的陣列,假設第一輪增量是4,然後是每間隔4個為一組排序,第二輪的時候增量變為2,第三輪的時候增量變為1。直到增量為1的時候排序才算完成,我們看下程式碼

public static void shellSort1(int[] array) {
    int length = array.length;
    int step = length >> 1;
    while (step >= 1) {
        for (int i = step; i < length; i++) {
            for (int j = i; j >= step; j -= step) {
                if (array[j] < array[j - step]) {
                    swap1(array, j, j - step);
                } else {
                    //如果大於,則不用繼續往前比較了,
                    // 因為前面的元素已經排好序
                    break;
                }
            }
        }
        step >>= 1;
    }
}
              
public static void swap1(int[] A, int i, int j) {
    if (i != j) {
        A[i] ^= A[j];
        A[j] ^= A[i];
        A[i] ^= A[j];
    }
}

其實上面程式碼我們還可以優化一下,因為如果數量很大並且step又比較小的時候,兩兩比較交換顯然不是很好。我們可以使用一個臨時變數temp,首先把待比較的變數儲存到temp中,然後往前找,如果前面的比他大,就會把前面的值挪到當前位置,然後再往前找,如果還比當前大那麼還挪,直到迴圈完為止,然後再把當前儲存的temp值放到最前面挪動的那個值。這個挪動和前面介紹的插入排序的挪動有點類似,只不過插入排序的挪動是一個個往前比較(二分法插入例外),而這個挪動是每間隔step進行比較然後確定是否挪動。我們來看下程式碼

private static void shellSort2(int[] arr) {
    int j;
    int len = arr.length;
    for (int step = len >> 1; step > 0; step >>= 1) {
        for (int i = step; i < len; i++) {
            int temp = arr[i];
            for (j = i; j >= step && temp < arr[j - step]; j -= step) {
                arr[j] = arr[j - step];
            }
            arr[j] = temp;
        }
    }
}

在上面希爾排序中我們使用的間隔是陣列長度的一半,這個間隔實際上是可以自己定的,但一定要保證間隔最後的一步是1即可,希爾排序中大家都比較認可的一種計算間隔的公式是step = step * 3 + 1;我們再來看一下程式碼

public static void shellSort3(int[] data) {
    int step = 1;
    while (step <= data.length / 3) {
        step = step * 3 + 1;
    }
    while (step > 0) {
        for (int i = step; i < data.length; i += step) {
            if (data[i] < data[i - step]) {
                int tmp = data[i];
                int j = i - step;
                while (j >= 0 && data[j] > tmp) {
                    data[j + step] = data[j];
                    j -= step;
                }
                data[j + step] = tmp;
            }
        }
        step = (step - 1) / 3;
    }
}

十,計數排序

計數排序是一個非基於比較的排序演算法,他首先要找到陣列的最大值和最小值然後再根據最大值和最小值申請頻率表,其實就是個陣列,每個數在陣列中出現的頻率。這裡陣列的每個值我們暫且以桶來表示,每個桶對應一個數在原陣列中出現的頻率,如果一個桶為1就表示和這個桶對應的這個數在原陣列中只出現一次,如果為2就表示出現兩次……,我們通過一張圖來看下計數排序

他是首先找到陣列的最大值和最小值,然後申請桶的大小,然後再一個個讀取陣列中的值存到桶中,最後再根據桶的順序把桶中的數字一個個讀取到陣列中。每個數存放到桶中的位置是當前數字前去最小值,因為這樣可以保證負數也能進行排序。我們看下的程式碼。

    public static void countSort1(int[] array) {
        int arrayLength = array.length;
        int max = array[0];
        int min = array[0];
        for (int i = 0; i < arrayLength; i++) {
            if (array[i] > max)
                max = array[i];
            else if (array[i] < min)
                min = array[i];
        }
           
        int bucketLength = max - min + 1;//桶的數量
        int[] tmp = new int[arrayLength];
        int[] buckets = new int[bucketLength];//桶
        for (int i = 0; i < arrayLength; i++) {
            buckets[array[i] - min]++;//落在某個桶裡就加1
        }
        // 從小到大排序
        for (int i = 1; i < bucketLength; i++) {
            //後面桶對前面桶的累加
            buckets[i] = buckets[i] + buckets[i - 1];
        }
        // 從大到小排序
//        for (int i = bucketLength - 1; i > 0; i--) {
//            buckets[i - 1] = buckets[i] + buckets[i - 1];
//        }
        //把原陣列儲存在臨時陣列中。
        System.arraycopy(array, 0, tmp, 0, arrayLength);
        for (int k = 0; k < arrayLength; k++) {
            //根據每個數值在桶中的順序重新儲存
            array[--buckets[tmp[k] - min]] = tmp[k];
        }
    }

十一,點陣圖排序

點陣圖排序也稱為bitmap排序,它主要用於海量資料去重和海量資料排序,假如說有10億個int型別且全部不相同的資料,給1G記憶體讓你排序,你怎麼排,如果全部載入到記憶體中,相當於40億個位元組,大概約等於4G記憶體。所以全部載入到記憶體肯定不行,如果我們使用點陣圖排序的話,我們用long型別表示,一個long佔用8個位元組也就是64位,所以如果我們使用點陣圖排序的話只會佔用約0.125G記憶體,記憶體佔用大大減少。但點陣圖排序有個缺點就是資料不能有重複的,如果有重複的會覆蓋掉,這也是點陣圖能在海量資料中去重的原因,我們看下點陣圖排序的程式碼

private static int[] bitmapSort1(int[] array) {
    int max = getMaxNumbit1(array);
    int N = max / 64 + 1;
    long[] bitmap = new long[N];
    for (int i = 0; i < array.length; i++)
        bitmap[array[i] / 64] |= 1L << (array[i] % 64);
    int k = 0;
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < 64; j++) {
            if ((bitmap[i] & (1L << j)) != 0) {
                array[k++] = i * 64 + j;
            }
        }
    }
    if (k < array.length)
        return Arrays.copyOfRange(array, 0, k);
    return array;
}
     
private static int getMaxNumbit1(int array[]) {
    int max = array[0];
    for (int i = 1, length = array.length; i < length; i++) {
        if (array[i] > max) {
            max = array[i];
        }
    }
    return max;
}

我們看到這是使用的是位表示,一個long型別佔8個位元組,但他可以表示64個數字,所以記憶體佔用會大大減少。最後有個k < array.length的判斷,是因為如果有重複的資料會覆蓋掉重複的,導致陣列變小。但這裡面還有個問題就是不能有負數出現,如果出現負數會報異常,我們也可以改一下讓負數也可以排序,看程式碼。

private static int[] bitmapSort2(int[] array) {
    int[] value = getMaxNumbit2(array);
    int N = (value[0] - value[1]) / 64 + 1;
    long[] bitmap = new long[N];
    for (int i = 0; i < array.length; i++)
        bitmap[(array[i] - value[1]) / 64] |= 1L << ((array[i] - value[1]) % 64);
    int k = 0;
    int[] temp = new int[array.length];
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < 64; j++) {
            if ((bitmap[i] & (1L << j)) != 0) {
                temp[k++] = i * 64 + j + value[1];
            }
        }
    }
    if (k < temp.length)
        return Arrays.copyOfRange(temp, 0, k);
    return temp;
}
    
private static int[] getMaxNumbit2(int array[]) {
    int max = array[0];
    int min = array[0];
    for (int i = 1, length = array.length; i < length; i++) {
        if (array[i] > max) {
            max = array[i];
        } else if (array[i] < min) {
            min = array[i];
        }
    }
    return new int[]{max, min};
}

我們來找幾行資料測試一下

int[] array2 = new int[20];
Random random = new Random();
for (int i = 0; i < array2.length; i++) {
    array2[i] = random.nextInt(1000) - 500;
}
System.out.println("排序前:" + Arrays.toString(array2));
int temp2[] = bitmapSort2(array2);
System.out.println("排序後:" + Arrays.toString(temp2));

再來看一下執行的結果

排序前:[-212, -71, -58, -427, 132, -223, -315, 153, 72, -270, 15, -268, 284, 93, 234, 488, -407, -168, 277, 174]
排序後:[-427, -407, -315, -270, -268, -223, -212, -168, -71, -58, 15, 72, 93, 132, 153, 174, 234, 277, 284, 488]

十二,Bogo排序

Bogo排序完全是一種搞笑的排序,到現在為止我還真沒有在什麼地方發現他使用過。他排序的原理是這樣的,就是隨機打亂陣列,然後再驗證陣列是否有序,如果無序再打亂再驗證,直到驗證完全有序為止。我們看下程式碼

private static final Random random = new Random();
          
private static void bogoSort(int[] array) {
    while (!isOrder(array)) {
        for (int i = 0; i < array.length; i++) {
            int randomPosition = random.nextInt(array.length);
            int temp = array[i];
            array[i] = array[randomPosition];
            array[randomPosition] = temp;
        }
    }
}
          
private static boolean isOrder(int[] array) {
    for (int i = 0; i < array.length - 1; i++) {
        if (array[i] > array[i + 1]) return false;
    }
    return true;
}

十三,雞尾酒排序

雞尾酒排序它是氣泡排序的一種,他和氣泡排序的不同之處在於,氣泡排序是往一個方向排的,而雞尾酒排序是往兩個方向排的,它是先從左往右把大的排到右邊,然後再從右往左把小的排到左邊,我們看下程式碼

public static void cocktailSort1(int[] array) {
    for (int i = 0; i < array.length / 2; i++) {
        //把大的挪到右邊
        for (int j = i; j < array.length - i - 1; j++) {
            if (array[j] > array[j + 1]) {
                swap1(array, j, j + 1);
            }
        }
        //把小的挪到前面
        for (int j = array.length - 1 - (i + 1); j > i; j--) {
            if (array[j] < array[j - 1]) {
                swap1(array, j, j - 1);
            }
        }
    }
}
         
public static void swap1(int[] A, int i, int j) {
    A[i] = A[i] + A[j];
    A[j] = A[i] - A[j];
    A[i] = A[i] - A[j];
}

每次迴圈的時候都是把剩餘最大的挪到右邊,剩餘最小的挪到左邊。我們也可以換一種寫法,分別用兩個指標表示,一個指向前面一個指向後面,然後兩個指標分別都往中間移,指標走過的地方都是已經排序好的,只要左邊指標小於右邊指標就一直走下去,我們看下程式碼

public static void cocktailSort2(int[] array) {
    int left = 0, right = array.length - 1;
    while (left < right) {
        for (int i = left; i < right; i++)
            if (array[i] > array[i + 1]) {
                swap2(array, i, i + 1);
            }
        right--;
        for (int i = right; i > left; i--)
            if (array[i - 1] > array[i]) {
                swap2(array, i, i - 1);
            }
        left++;
    }
}
        
public static void swap2(int[] A, int i, int j) {
    A[i] = A[i] + A[j];
    A[j] = A[i] - A[j];
    A[i] = A[i] - A[j];
}

十四,鴿巢排序

鴿巢排序(Pigeonhole sort),也被稱作基數分類,是一種不可避免遍歷每一個元素並且排序的情況下效率最好的一種排序演算法。但它只有在差值(或者可被對映在差值)很小的範圍內的數值排序的情況下實用,如果差值相差很大的話很浪費空間。他和計數排序很像,原理基本上都差不多,應該算是計數排序的一個變種吧。他們都是先申請一個數組,想當於桶,然後遍歷待排陣列,並標記待排資料在桶中相對應位置的數量。然後遍歷桶。不同的是計數排序最後先把原始資料存放到一個臨時陣列中,然後遍歷臨時陣列,並根據臨時陣列在桶中的位置,再重新存放到原始陣列中。而鴿巢排序最後是直接遍歷桶,如果有值就把他寫入到原始陣列中。如果待排資料數量小但相差很大的話,鴿巢排序最後要執行的次數很多,而計數排序最後要執行的次數比較少。我們看下程式碼。

private static void pigeonholeSort1(int[] array) {
    int max = max1(array);
    int bucket[] = new int[max + 1];
    for (int i = 0; i < array.length; ++i)
        bucket[array[i]]++;
    int j = 0;
    for (int i = 0; i < bucket.length; ++i)
        for (int k = 0; k < bucket[i]; ++k)
            array[j++] = i;
}
       
private static int max1(int[] array) {
    int max = array[0];
    for (int i = 0; i < array.length; i++) {
        if (array[i] > max)
            max = array[i];
    }
    return max;
}

這裡會有個問題,就是隻能對非負數進行排序,如果出現負數則會出現異常,我們在改一下,讓他正數負數都可以排序。

private static void pigeonholeSort2(int[] array) {
    int maxCount[] = getMaxNumbit2(array);
    int bucket[] = new int[maxCount[0] - maxCount[1] + 1];
    for (int i = 0; i < array.length; ++i)
        bucket[array[i] - maxCount[1]]++;
    int j = 0;
    for (int i = 0; i < bucket.length; ++i)
        for (int k = 0; k < bucket[i]; ++k)
            array[j++] = i + maxCount[1];
}
      
private static int[] getMaxNumbit2(int array[]) {
    int max = array[0];
    int min = array[0];
    for (int i = 1, length = array.length; i < length; i++) {
        if (array[i] > max) {
            max = array[i];
        } else if (array[i] < min) {
            min = array[i];
        }
    }
    return new int[]{max, min};
}

OK,這就是上面介紹的14中排序演算法。