排序算法中涉及到了兩個概念:git
原地排序:根據算法對內存的消耗狀況,能夠將算法分爲原地排序和非原地排序,原地排序特指空間複雜度爲 O(1) 的排序。github
排序算法的穩定性:例如排序一個數組 [1, 5, 3, 7, 4, 9, 5],數組中有兩個 5,排序以後是 [1, 3, 4, 5, 5, 7, 9],若是排序以後的兩個 5 的先後順序沒有發生變化,那麼稱這個排序是穩定的,反之則是不穩定的。算法
冒泡排序是很經典的排序算法了,相鄰的兩個數據依次進行比較並交換位置。遍歷一遍數組後,則有一個數據排序完成,而後再遍歷 n 次,排序完成。示意圖以下:shell
代碼實現:api
public class BubbleSort { private static void bubbleSort(int[] data){ int length = data.length; for (int i = length - 1; i > 0; i --) { //判斷是否有數據交換,若是沒有則提早退出 boolean flag = false; for (int j = 0; j < i; j ++) { if (data[j] > data[j + 1]){ int temp = data[j]; data[j] = data[j + 1]; data[j + 1] = temp; flag = true; } } if (!flag){ break; } } } }
將要排序的數據分爲了已排序區間和未排序區間,每次從未排序區間找到最小值,而後將其放到已排序區間的末尾,循環遍歷未排序區間則排序完成。數組
示意圖以下:數據結構
代碼實現:dom
public class SelectionSort { public static void selectionSort(int[] data){ int length = data.length; for (int i = 0; i < length - 1; i++) { int min = i; for (int j = i + 1; j < length; j++) { if (data[min] > data[j]){ min = j; } } int temp = data[min]; data[min] = data[i]; data[i] = temp; } } }
和選擇排序相似,插入排序也將數據分爲了已排序區間和未排序區間,遍歷未排序區間,每次取一個數據,將其插入到已排序區間的合適位置,讓已排序區間一直保持有序,直到未排序區間遍歷完排序則完成。函數
示意圖以下:性能
代碼實現:
public class InsertionSort { public static void insertionSort(int[] data){ int length = data.length; for (int i = 0; i < length - 1; i++) { int val = data[i + 1]; int j = i + 1; while (j > 0 && data[j - 1] > val){ data[j] = data[j - 1]; j --; } data[j] = val; } } }
插入排序爲何比冒泡排序更經常使用?
這兩種排序的時間複雜度都是同樣的,最好狀況是 O(n),最壞狀況是 O(n2),可是在實際的生產中,插入排序使用更多,緣由在於二者數據交換的次數不一樣。冒泡排序須要進行三次交換,插入排序只要一次:
//冒泡排序數據交換 if (data[j] > data[j + 1]){ int temp = data[j]; data[j] = data[j + 1]; data[j + 1] = temp; flag = true; } //插入排序數據交換 while (j > 0 && data[j - 1] > val){ data[j] = data[j - 1]; j --; }
在數據量較大的時候,二者性能差距就體現出來了。
希爾排序實際上是插入排序的一種優化,其思路是將排序的數組按照必定的增量將數據分組,每一個分組用插入排序進行排序,而後增量逐步減少,當增量減少爲1的時候,算法便終止,因此希爾排序又叫作「縮小增量排序」。
示意圖以下:
圖中的示例,每次依次將數組分爲若干組,每組分別進行插入排序。代碼實現以下:
public class ShellSort { public static void shellSort(int[] data) { int length = data.length; int step = length / 2; while (step >= 1){ for (int i = step; i < length; i++) { int val = data[i]; int j = i - step; for (; j >= 0; j -= step){ if (data[j] > val){ data[j + step] = data[j]; } else { break; } } data[j + step] = val; } step = step / 2; } } }
歸併排序使用到了分治思想,分治思想即將大的問題分解成小的問題,小的問題解決了,大的問題也就解決了。蘊含分治思想的問題,通常可使用遞歸技巧來實現。
歸併排序的思路是:首先將數組分解,局部進行排序,而後將排序的結果進行合併,這樣整個數組就有序了,你能夠結合下圖理解:
代碼實現:
public class MergeSort { public static void mergeSort(int[] data){ mergeInternally(data, 0, data.length - 1); } private static void mergeInternally(int[] data, int p, int r){ if (p >= r){ return; } int q = (p + r) / 2; //分治遞歸 mergeInternally(data, p, q); mergeInternally(data, q + 1, r); //結果合併 merge(data, p, q, r); } private static void merge(int[] data, int p, int q, int r){ int[] temp = new int[r - p + 1]; int k = 0; int i = p; int j = q + 1; //比較併合並 while (i <= q && j <= r){ if (data[i] < data[j]){ temp[k ++] = data[i ++]; } else { temp[k ++] = data[j ++]; } } //合併可能出現的剩餘元素 int start = i; int end = q; if (j <= r){ start = j; end = r; } while (start <= end){ temp[k ++] = data[start ++]; } //拷貝回原數組 if (r - p + 1 >= 0) { System.arraycopy(temp, 0, data, p, r - p + 1); } } }
快速排序也用到了分治的思想,只不過它和歸併排序的思路恰好是相反的,快速排序使用數組中一個數據做爲分區點(通常能夠選取數組第一個或最後一個元素),比分區點小的,放在左側,比分區點大的,放在右側。而後左右兩側的數據再次選擇分區點,循環進行這個操做,直到排序完成。
示意圖以下(圖中是以第一個元素做爲分區點):
代碼實現:
public class QuickSort { public static void quickSort(int[] data){ quickSortInternally(data, 0, data.length - 1); } private static void quickSortInternally(int[] data, int p, int r){ if (p >= r){ return; } int q = partition(data, p, r); quickSortInternally(data, p, q - 1); quickSortInternally(data, q + 1, r); } /** * 獲取分區點函數 */ private static int partition(int [] data, int p, int q){ int pivot = data[q]; int i = 0; int j = 0; while (j < q){ if (data[j] <= pivot){ swap(data, i, j); i ++; } j ++; } swap(data, i, q); return i; } /** * 交換數組兩個元素 */ private static void swap(int[] data, int i, int j){ int temp = data[i]; data[i] = data[j]; data[j] = temp; } }
基於堆的排序比較經常使用,時間複雜度爲 O(nlogn),而且是原地排序,主要的步驟分爲建堆和排序。
建堆
思路是從堆中第一個非葉子節點,依次從上往下進行堆化,以下圖:
排序
建堆完成以後,假設堆中元素個數爲 n,堆頂元素便是最大的元素,這時候直接將堆頂元素和堆中最後一個元素進行交換,而後將剩餘的 n - 1 個元素構建成新的堆,依次類推,直到堆中元素減小至 1,則排序完成。示意圖以下:
代碼實現:
public class HeapSort { /** * 排序 */ public void heapSort(int[] data){ int length = data.length; if (length <= 1){ return; } buildHeap(data); while (length > 0){ swap(data, 0, -- length); heapify(data, length, 0); } } /** * 建堆 */ private void buildHeap(int[] data){ int length = data.length; for (int i = (length - 2) / 2; i >= 0; i --) { heapify(data, length, i); } } /** * 堆化函數 */ private void heapify(int[] data, int size, int i){ while (true){ int max = i; if ((2 * i + 1) < size && data[i] < data[2 * i + 1]) { max = 2 * i + 1; } if ((2 * i + 2) < size && data[max] < data[2 * i + 2]) { max = 2 * i + 2; } if (max == i){ break; } swap(data, i, max); i = max; } } /** * 交換數組中兩個元素 */ private void swap(int[] data, int i, int j){ int temp = data[i]; data[i] = data[j]; data[j] = temp; } }
桶排序並非基於數據比較的,所以比較的高效,時間複雜度接近 O(n),可是相應地,應用的條件十分苛刻。其思路很是的簡單:將要排序的數據分到各個有序的桶內,數據在桶內進行排序,而後按序取出,整個數據就是有序的了。
最好狀況下,數據被均勻的分到各個桶中,最壞狀況是數據全都被分到一個桶中。
下面是一個桶排序的示例:
public class BucketSort { /** * 測試場景:數組中有10000個數據,範圍在(0-100000) * 使用100個桶,每一個桶存放的數據範圍爲:0-999, 1000-1999, 2000-2999,依次類推 */ public static void bucketSort(int[] data){ //新建100個桶 int bucketSize = 100; ArrayList<ArrayList<Integer>> buckets = new ArrayList<>(bucketSize); for (int i = 0; i < bucketSize; i++) { buckets.add(new ArrayList<>()); } //遍歷數據,將數據放到桶中 for (int i : data) { buckets.get(i / 1000).add(i); } //在桶內部進行排序 int k = 0; for (int i = 0; i < bucketSize; i++) { ArrayList<Integer> list = buckets.get(i); Integer[] num = list.toArray(new Integer[1]); Arrays.sort(num); //拷貝到data中 for (int n : num) { data[k++] = n; } } } //測試 public static void main(String[] args) { Random random = new Random(); int[] data = new int[10000]; for (int i = 0; i < data.length; i++) { data[i] = random.nextInt(100000); } BucketSort.bucketSort(data); System.out.println(Arrays.toString(data)); } }
計數排序實際上是一種特殊的桶排序,適用於數據的區間不是很大的狀況。
例如給 10 萬人按照年齡進行排序,咱們知道年齡的區間並非很大,最小的 0 歲,最大的能夠假設爲 120 歲,那麼咱們能夠新建 121 個桶,掃描一遍數據,將年齡相同的放到一個桶中,而後按序從桶中將數據取出,這樣數據就有序了。
計數排序的基本思路以下:
代碼實現:
public class CountingSort { private static void countingSort(int[] data){ int length = data.length; //找到數組的最大值 int max = data[0]; for (int i : data){ if (max < i){ max = i; } } //新建一個計數數組,大小爲max+1 //count數組的下標對應data的值,存儲的值爲對應data值的個數 int[] count = new int[max + 1]; for (int i : data){ count[i] ++; } //根據count數組取出數據 int k = 0; for (int i = 0; i < count.length; i++) { while (count[i] != 0){ data[k ++] = i; count[i] --; } } } }
基數排序適用於位數較多的數字或者字符串,思路是將排序的數據按位拆分,每一位單獨按照穩定的排序算法進行比較,以下圖:
圖中的示例,以每一個數字爲下標,建了 10 個 「桶」,每一個桶是一個隊列(也能夠是數組),而後將要排序的數據按位加入到隊列中,而後出隊,比較完每一位,則排序完成。
代碼實現:
public class RadixSort { private static void radixSort(int[] data) { int maxDigit = maxDigit(data); //新建並初始化10個桶 Queue<Integer>[] queues = new LinkedList[10]; for (int i = 0; i < queues.length; i++) { queues[i] = new LinkedList<>(); } for (int i = 0, mod = 1; i < maxDigit; i ++, mod *= 10) { for (int n : data){ int m = (n / mod) % 10; queues[m].add(n); } int count = 0; for (Queue<Integer> queue : queues) { while (queue.size() > 0) { data[count++] = queue.poll(); } } } } /** * 獲取數組最大位數 */ private static int maxDigit(int[] data){ int maxDigit = data[0]; for (int i : data){ if (maxDigit < i){ maxDigit = i; } } return String.valueOf(maxDigit).length(); } }
在個人 Github 上面有更加詳細的數據結構與算法代碼