每次循環都比較先後兩個元素的大小,若是前者大於後者,則將二者進行交換。這樣作會將每次循環中最大的元素替換到末尾,逐漸造成有序集合。將每次循環中的最大元素逐漸由隊首轉移到隊尾的過程形似「冒泡」過程,故所以得名。java
一個優化冒泡排序的方法就是若是在一次循環的過程當中沒有發生交換,則能夠當即退出當前循環,由於此時已經排好序了(也就是時間複雜度最好狀況下是的由來)。node
public int[] bubbleSort(int[] array) { if (array == null || array.length < 2) { return array; } for (int i = 0; i < array.length - 1; i++) { boolean flag = false; for (int j = 0; j < array.length - 1 - i; j++) { if (array[j] > array[j + 1]) { //這裏交換兩個數據並無使用中間變量,而是使用異或的方式來實現 array[j] = array[j] ^ array[j + 1]; array[j + 1] = array[j] ^ array[j + 1]; array[j] = array[j] ^ array[j + 1]; flag = true; } } if (!flag) { break; } } return array; }
每次循環都會找出當前循環中最小的元素,而後和這次循環中的隊首元素進行交換。git
public int[] selectSort(int[] array) { if (array == null || array.length < 2) { return array; } for (int i = 0; i < array.length; i++) { int minIndex = i; for (int j = i + 1; j < array.length; j++) { if (array[j] < array[minIndex]) { minIndex = j; } } if (minIndex > i) { array[i] = array[i] ^ array[minIndex]; array[minIndex] = array[i] ^ array[minIndex]; array[i] = array[i] ^ array[minIndex]; } } return array; }
插入排序的精髓在於每次都會在先前排好序的子集合中插入下一個待排序的元素,每次都會判斷待排序元素的上一個元素是否大於待排序元素,若是大於,則將元素右移,而後判斷再上一個元素與待排序元素...以此類推。直到小於等於比較元素時就是找到了該元素的插入位置。這裏的等於條件放在哪裏很重要,由於它是決定插入排序穩定與否的關鍵。算法
public int[] insertSort(int[] array) { if (array == null || array.length < 2) { return array; } for (int i = 1; i < array.length; i++) { int temp = array[i]; int j = i - 1; while (j >= 0 && array[j] > temp) { array[j + 1] = array[j]; j--; } array[j + 1] = temp; } return array; }
希爾排序能夠認爲是插入排序的改進版本。首先按照初始增量來將數組分紅多個組,每一個組內部使用插入排序。而後縮小增量來從新分組,組內再次使用插入排序...重複以上步驟,直到增量變爲1的時候,這個時候整個數組就是一個分組,進行最後一次完整的插入排序便可結束。shell
在排序開始時的增量較大,分組也會較多,可是每一個分組中的數據較少,因此插入排序會很快。隨着每一輪排序的進行,增量和分組數會逐漸變小,每一個分組中的數據會逐漸變多。但由於以前已經通過了多輪的分組排序,而此時的數組會趨近於一個有序的狀態,因此這個時候的排序也是很快的。而對於數據較多且趨向於無序的數據來講,若是隻是使用插入排序的話效率就並不高。因此整體來講,希爾排序的執行效率是要比插入排序高的。數組
希爾排序執行示意圖:dom
具體的實現代碼以下:優化
public int[] shellSort(int[] array) { if (array == null || array.length < 2) { return array; } int gap = array.length >>> 1; while (gap > 0) { for (int i = gap; i < array.length; i++) { int temp = array[i]; int j = i - gap; while (j >= 0 && array[j] > temp) { array[j + gap] = array[j]; j = j - gap; } array[j + gap] = temp; } gap >>>= 1; } return array; }
堆排序的過程是首先構建一個大頂堆,大頂堆首先是一棵徹底二叉樹,其次它保證堆中某個節點的值老是不大於其父節點的值。ui
由於大頂堆中的最大元素確定是根節點,因此每次取出根節點即爲當前大頂堆中的最大元素,取出後剩下的節點再從新構建大頂堆,再取出根節點,再從新構建…重複這個過程,直到數據都被取出,最後取出的結果即爲排好序的結果。spa
public class MaxHeap { /** * 排序數組 */ private int[] nodeArray; /** * 數組的真實大小 */ private int size; private int parent(int index) { return (index - 1) >>> 1; } private int leftChild(int index) { return (index << 1) + 1; } private int rightChild(int index) { return (index << 1) + 2; } private void swap(int i, int j) { nodeArray[i] = nodeArray[i] ^ nodeArray[j]; nodeArray[j] = nodeArray[i] ^ nodeArray[j]; nodeArray[i] = nodeArray[i] ^ nodeArray[j]; } private void siftUp(int index) { //若是index處節點的值大於其父節點的值,則交換兩個節點值,同時將index指向其父節點,繼續向上循環判斷 while (index > 0 && nodeArray[index] > nodeArray[parent(index)]) { swap(index, parent(index)); index = parent(index); } } private void siftDown(int index) { //左孩子的索引比size小,意味着索引index處的節點有左孩子,證實此時index節點不是葉子節點 while (leftChild(index) < size) { //maxIndex記錄的是index節點左右孩子中最大值的索引 int maxIndex = leftChild(index); //右孩子的索引小於size意味着index節點含有右孩子 if (rightChild(index) < size && nodeArray[rightChild(index)] > nodeArray[maxIndex]) { maxIndex = rightChild(index); } //若是index節點值比左右孩子值都大,則終止循環 if (nodeArray[index] >= nodeArray[maxIndex]) { break; } //不然進行交換,將index指向其交換的左孩子或右孩子,繼續向下循環,直到葉子節點 swap(index, maxIndex); index = maxIndex; } } private void add(int value) { nodeArray[size] = value; size++; //構建大頂堆 siftUp(size - 1); } private void extractMax() { //將堆頂元素和最後一個元素進行交換 swap(0, size - 1); //此時並無刪除元素,而只是將size-1,剩下的元素從新構建成大頂堆 size--; //從新構建大頂堆 siftDown(0); } public int[] heapSort(int[] array) { if (array == null || array.length < 2) { return array; } nodeArray = new int[array.length]; for (int value : array) { add(value); } for (int i = 0; i < array.length; i++) { extractMax(); } return nodeArray; } }
上面的經典實現中,若是須要變更節點時,都會來一次父子節點的互相交換操做(包括刪除節點時首先作的要刪除節點和最後一個節點之間的交換操做也是如此)。若是仔細思考的話,就會發現這實際上是多餘的。在須要交換節點的時候,只須要siftUp操做時的父節點或siftDown時的孩子節點從新移到當前須要比較的節點位置上,而比較節點是不須要移動到它們的位置上的。此時直接進入到下一次的判斷中,重複siftUp或siftDown過程,直到最後找到了比較節點的插入位置後,纔會將其插入進去。這樣作的好處是能夠省去一半的節點賦值的操做,提升了執行的效率。同時這也就意味着,須要將要比較的節點做爲參數保存起來,而在ScheduledThreadPoolExecutor源碼中也正是這麼實現的(《較真兒學源碼系列-ScheduledThreadPoolExecutor(逐行源碼帶你分析做者思路)》)。
歸併排序使用的是分治的思想,首先將數組不斷拆分,直到最後拆分紅兩個元素的子數組,將這兩個元素進行排序合併,再向上遞歸。不斷重複這個拆分和合並的遞歸過程,最後獲得的就是排好序的結果。
合併的過程是將兩個指針指向兩個子數組的首位元素,兩個元素進行比較,較小的插入到一個temp數組中,同時將該數組的指針右移一位,繼續比較該數組的第二個元素和另外一個元素…重複這個過程。這樣temp數組保存的即是這兩個子數組排好序的結果。最後將temp數組複製回原數組的位置處便可。
public int[] mergeSort(int[] array) { if (array == null || array.length < 2) { return array; } return mergeSort(array, 0, array.length - 1); } private int[] mergeSort(int[] array, int left, int right) { if (left < right) { //這裏沒有選擇「(left + right) / 2」的方式,是爲了防止數據溢出 int mid = left + ((right - left) >>> 1); // 拆分子數組 mergeSort(array, left, mid); mergeSort(array, mid + 1, right); // 對子數組進行合併 merge(array, left, mid, right); } return array; } private void merge(int[] array, int left, int mid, int right) { int[] temp = new int[right - left + 1]; // p1和p2爲須要對比的兩個數組的指針,k爲存放temp數組的指針 int p1 = left, p2 = mid + 1, k = 0; while (p1 <= mid && p2 <= right) { if (array[p1] <= array[p2]) { temp[k++] = array[p1++]; } else { temp[k++] = array[p2++]; } } // 把剩餘的數組直接放到temp數組中 while (p1 <= mid) { temp[k++] = array[p1++]; } while (p2 <= right) { temp[k++] = array[p2++]; } // 複製回原數組 for (int i = 0; i < temp.length; i++) { array[i + left] = temp[i]; } }
快速排序的核心是要有一個基準數據temp,通常取數組的第一個位置元素。而後須要有兩個指針left和right,分別指向數組的第一個和最後一個元素。
首先從right開始,比較right位置元素和基準數據。若是大於等於,則將right指針左移,比較下一位元素;若是小於,就將right指針處數據賦給left指針處(此時left指針處數據已保存進temp中),left指針+1,以後開始比較left指針處數據。
拿left位置元素和基準數據進行比較。若是小於等於,則將left指針右移,比較下一位元素;而若是大於就將left指針處數據賦給right指針處,right指針-1,以後開始比較right指針處數據…重複這個過程。
直到left和right指針相等時,說明這一次比較過程完成。此時將先前存放進temp中的基準數據賦值給當前left和right指針共同指向的位置處,便可完成這一次排序操做。
以後遞歸排序基礎數據的左半部分和右半部分,遞歸的過程和上面講述的過程是同樣的,只不過數組範圍再也不是原來的所有數組了,而是如今的左半部分或右半部分。當所有的遞歸過程結束後,最終結果即爲排好序的結果。
快速排序執行示意圖:
正如上面所說的,通常取第一個元素做爲基準數據,但若是當前數據爲從大到小排列好的數據,而如今要按從小到大的順序排列,則數據分攤不均勻,時間複雜度會退化爲,而不是正常狀況下的。此時採起一個優化手段,即取最左邊、最右邊和最中間的三個元素的中間值做爲基準數據,以此來避免時間複雜度爲的狀況出現,固然也能夠選擇更多的錨點或者隨機選擇的方式來進行選取。
還有一個優化的方法是:像快速排序、歸併排序這樣的複雜排序方法在數據量大的狀況下是比選擇排序、冒泡排序和插入排序的效率要高的,可是在數據量小的狀況下反而要更慢。因此咱們能夠選定一個閾值,這裏選擇爲47(和源碼中使用的同樣)。當須要排序的數據量小於47時走插入排序,大於47則走快速排序。
private static final int THRESHOLD = 47; public int[] quickSort(int[] array) { if (array == null || array.length < 2) { return array; } return quickSort(array, 0, array.length - 1); } private int[] quickSort(int[] array, int start, int end) { // 若是當前須要排序的數據量小於等於THRESHOLD則走插入排序的邏輯,不然繼續走快速排序 if (end - start <= THRESHOLD - 1) { return insertSort(array); } // left和right指針分別指向array的第一個和最後一個元素 int left = start, right = end; /* 取最左邊、最右邊和最中間的三個元素的中間值做爲基準數據,以此來儘可能避免每次都取第一個值做爲基準數據、 時間複雜度可能退化爲O(n^2)的狀況出現 */ int middleOf3Indexs = middleOf3Indexs(array, start, end); if (middleOf3Indexs != start) { swap(array, middleOf3Indexs, start); } // temp存放的是array中須要比較的基準數據 int temp = array[start]; while (left < right) { // 首先從right指針開始比較,若是right指針位置處數據大於temp,則將right指針左移 while (left < right && array[right] >= temp) { right--; } // 若是找到一個right指針位置處數據小於temp,則將right指針處數據賦給left指針處 if (left < right) { array[left] = array[right]; left++; } // 而後從left指針開始比較,若是left指針位置處數據小於temp,則將left指針右移 while (left < right && array[left] <= temp) { left++; } // 若是找到一個left指針位置處數據大於temp,則將left指針處數據賦給right指針處 if (left < right) { array[right] = array[left]; right--; } } // 當left和right指針相等時,此時循環跳出,將以前存放的基準數據賦給當前兩個指針共同指向的數據處 array[left] = temp; // 一次替換後,遞歸交換基準數據左邊的數據 if (start < left - 1) { array = quickSort(array, start, left - 1); } // 以後遞歸交換基準數據右邊的數據 if (right + 1 < end) { array = quickSort(array, right + 1, end); } return array; } private int middleOf3Indexs(int[] array, int start, int end) { int mid = start + ((end - start) >>> 1); if (array[start] < array[mid]) { if (array[mid] < array[end]) { return mid; } else { return array[start] < array[end] ? end : start; } } else { if (array[mid] > array[end]) { return mid; } else { return array[start] < array[end] ? start : end; } } } private void swap(int[] array, int i, int j) { array[i] = array[i] ^ array[j]; array[j] = array[i] ^ array[j]; array[i] = array[i] ^ array[j]; }
以上的七種排序算法都是比較排序,也就是基於元素之間的比較來進行排序的。而下面將要介紹的三種排序算法是非比較排序,首先是計數排序。
計數排序會建立一個臨時的數組,裏面存放每一個數出現的次數。好比一個待排序的數組是[3, 3, 5, 2, 7, 4, 2],那麼這個臨時數組中記錄的數據就是[2, 2, 1, 1, 0, 1]。表示2出現了兩次、3出現了兩次、4出現了一次、5出現了一次、6出現了零次、7出現了一次。那麼最後只須要遍歷這個臨時數組中的計數值就能夠了。
public int[] countingSort(int[] array) { if (array == null || array.length < 2) { return array; } //記錄待排序數組中的最大值 int max = array[0]; //記錄待排序數組中的最小值 int min = array[0]; for (int i : array) { if (i > max) { max = i; } if (i < min) { min = i; } } int[] temp = new int[max - min + 1]; //記錄每一個數出現的次數 for (int i : array) { temp[i - min]++; } int index = 0; for (int i = 0; i < temp.length; i++) { //當輸出一個數以後,當前位置的計數就減一,直到減到0爲止 while (temp[i]-- > 0) { array[index++] = i + min; } } return array; }
從上面的實現中能夠看到,計數排序僅適合數據跨度不大的場景。若是最大值和最小值之間的差距比較大,生成的臨時數組就會比較長。好比說一個數組是[2, 1, 3, 1000],最小值是1,最大值是1000。那麼就會生成一個長度爲1000的臨時數組,可是其中絕大部分的空間都是沒有用的,因此這就會致使空間複雜度變得很高。
計數排序是穩定的排序算法,但在上面的實現中並無體現出這一點,上面的實現沒有維護相同元素之間的前後順序。因此須要作些變換:將臨時數組中從第二個元素開始,每一個元素都加上前一個元素的值。仍是拿以前的[3, 3, 5, 2, 7, 4, 2]數組來舉例。計完數後的臨時數組爲[2, 2, 1, 1, 0, 1],此時作上面的變換,每一個數都累加前面的一個數,結果爲[2, 4, 5, 6, 6, 7]。這個時候臨時數組的含義就再也不是每一個數出現的次數了,此時記錄的是每一個數在最後排好序的數組中應該要存放的位置+1(若是有重複的就記錄最後一個)。對於上面的待排序數組來講,最後排好序的數組應該爲[2, 2, 3, 3, 4, 5, 7]。也就是說,此時各個數最後一次出現的索引位爲:1, 3, 4, 5, 6,分別都+1後就是2, 4, 5, 6, 7,這不就是上面作過變換以後的數組嗎?(沒有出現過的數字無論它)因此,此時從後往前遍歷原數組中的每個值,將其減去最小值後,找到其在變換後的臨時數組中的索引,也就是找到了最後排好序的數組中的位置了。固然,每次找到臨時數組中的索引後,這個位置的數須要-1。這樣若是後續有重複的該數字的話,就會插入到當前位置的前一個位置了。由此也說明了遍歷必須是從後往前遍歷,以此來維護相同數字之間的前後順序。
public int[] stableCountingSort(int[] array) { if (array == null || array.length < 2) { return array; } //記錄待排序數組中的最大值 int max = array[0]; //記錄待排序數組中的最小值 int min = array[0]; for (int i : array) { if (i > max) { max = i; } if (i < min) { min = i; } } int[] temp = new int[max - min + 1]; //記錄每一個數出現的次數 for (int i : array) { temp[i - min]++; } //將temp數組進行轉換,記錄每一個數在最後排好序的數組中應該要存放的位置+1(若是有重複的就記錄最後一個) for (int j = 1; j < temp.length; j++) { temp[j] += temp[j - 1]; } int[] sortedArray = new int[array.length]; //這裏必須是從後往前遍歷,以此來保證穩定性 for (int i = array.length - 1; i >= 0; i--) { sortedArray[temp[array[i] - min] - 1] = array[i]; temp[array[i] - min]--; } return sortedArray; }
上面的計數排序在數組最大值和最小值之間的差值是多少,就會生成一個多大的臨時數組,也就是生成了一個這麼多的桶,而每一個桶中就只插入一個數據。若是差值比較大的話,會比較浪費空間。那麼我能不能在一個桶中插入多個數據呢?固然能夠,而這就是桶排序的思路。桶排序相似於哈希表,經過必定的映射規則將數組中的元素映射到不一樣的桶中,每一個桶內進行內部排序,最後將每一個桶按順序輸出就好了。桶排序執行的高效與否和是不是穩定的取決於哈希散列的算法以及內部排序的結果。須要注意的是,這個映射算法並非常規的映射算法,要求是每一個桶中的全部數都要比前一個桶中的全部數都要大。這樣最後輸出的纔是一個排好序的結果。好比說第一個桶中存1-30的數字,第二個桶中存31-60的數字,第三個桶中存61-90的數字...以此類推。下面給出一種實現:
public int[] bucketSort(int[] array) { if (array == null || array.length < 2) { return array; } //記錄待排序數組中的最大值 int max = array[0]; //記錄待排序數組中的最小值 int min = array[0]; for (int i : array) { if (i > max) { max = i; } if (i < min) { min = i; } } //計算桶的數量(能夠自定義實現) int bucketNumber = (max - min) / array.length + 1; List<Integer>[] buckets = new ArrayList[bucketNumber]; //計算每一個桶存數的範圍(能夠自定義實現或者不用實現) int bucketRange = (max - min + 1) / bucketNumber; for (int value : array) { //計算應該放到哪一個桶中(能夠自定義實現) int bucketIndex = (value - min) / (bucketRange + 1); //延遲初始化 if (buckets[bucketIndex] == null) { buckets[bucketIndex] = new ArrayList<>(); } //放入指定的桶 buckets[bucketIndex].add(value); } int index = 0; for (List<Integer> bucket : buckets) { //對每一個桶進行內部排序,我這裏使用的是快速排序,也可使用別的排序算法,固然也能夠繼續遞歸去作桶排序 quickSort(bucket); if (bucket == null) { continue; } //將不爲null的桶中的數據按順序寫回到array數組中 for (Integer integer : bucket) { array[index++] = integer; } } return array; }
基數排序不是根據一個數的總體來進行排序的,而是將數的每一位上的數字進行排序。好比說第一輪排序,我拿到待排序數組中全部數個位上的數字來進行排序;第二輪排序我拿到待排序數組中全部數十位上的數字來進行排序;第三輪排序我拿到待排序數組中全部數百位上的數字來進行排序...以此類推。每一輪的排序都會累加上一輪全部前幾位上排序的結果,最終的結果就會是一個有序的數列。
基數排序通常是對全部非負整數進行排序的,可是也能夠有別的手段來去掉這種限制(好比都加一個固定的數或者都乘一個固定的數,排完序後再恢復等等)。基數排序和桶排序很像,桶排序是按數值的區間進行劃分,而基數排序是按數的位數進行劃分。同時這兩個排序都是須要依靠其餘排序算法來實現的(若是不算遞歸調用桶排序自己的話)。基數排序每一輪的內部排序會使用到計數排序來實現,由於每一位上的數字無非就是0-9,是一個小範圍的數,因此使用計數排序很合適。
基數排序執行示意圖:
具體的實現代碼以下:
public int[] radixSort(int[] array) { if (array == null || array.length < 2) { return array; } //記錄待排序數組中的最大值 int max = array[0]; for (int i : array) { if (i > max) { max = i; } } //獲取最大值的位數 int maxDigits = 0; while (max != 0) { max /= 10; maxDigits++; } //用來計數排序的臨時數組 int[] temp = new int[10]; //用來存放每輪排序後的結果 int[] sortedArray = new int[array.length]; for (int d = 1; d <= maxDigits; d++) { //每次循環開始前都要清空temp數組中的值 replaceArray(temp, null); //記錄每一個數出現的次數 for (int a : array) { temp[getNumberFromDigit(a, d)]++; } //將temp數組進行轉換,記錄每一個數在最後排好序的數組中應該要存放的位置+1(若是有重複的就記錄最後一個) for (int j = 1; j < temp.length; j++) { temp[j] += temp[j - 1]; } //這裏必須是從後往前遍歷,以此來保證穩定性 for (int i = array.length - 1; i >= 0; i--) { int index = getNumberFromDigit(array[i], d); sortedArray[temp[index] - 1] = array[i]; temp[index]--; } //一輪計數排序事後,將此次排好序的結果賦值給原數組 replaceArray(array, sortedArray); } return array; } private final static int[] sizeTable = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000}; /** * 獲取指定位數上的數字是多少 */ private int getNumberFromDigit(int number, int digit) { if (digit < 0) { return -1; } return (number / sizeTable[digit - 1]) % 10; } private void replaceArray(int[] originalArray, int[] replaceArray) { if (replaceArray == null) { for (int i = 0; i < originalArray.length; i++) { originalArray[i] = 0; } } else { for (int i = 0; i < originalArray.length; i++) { originalArray[i] = replaceArray[i]; } } }
排序算法 | 時間複雜度 | 空間複雜度 | 穩定性 | ||
---|---|---|---|---|---|
平均狀況 | 最好狀況 | 最壞狀況 | |||
冒泡排序 | 穩定 | ||||
選擇排序 | 不穩定 | ||||
插入排序 | 穩定 | ||||
希爾排序 | 取決於增量的選擇 | 不穩定 | |||
堆排序 | 不穩定 | ||||
歸併排序 | 穩定 | ||||
快速排序 | 不穩定 | ||||
計數排序 | 穩定 | ||||
桶排序 | 取決於桶散列的結果和內部排序算法的時間複雜度 | 穩定 | |||
基數排序 | 穩定 |
(其中:
在幾十年的計算機科學發展中,誕生了不少優秀的算法,你們都在爲了能開發出更高效的算法而努力。可是在這其中也誕生了一些僅供娛樂的搞笑算法,猴子排序就是其中的一種。
猴子排序的實現很簡單,隨機找出兩個元素進行交換,直到隨機交換到最後能正確排好序的時候纔會中止。
public int[] bogoSort(int[] array) { if (array == null || array.length < 2) { return array; } Random random = new Random(); while (!inOrder(array)) { for (int i = 0; i < array.length; i++) { int swapPosition = random.nextInt(i + 1); if (swapPosition == i) { continue; } array[i] = array[i] ^ array[swapPosition]; array[swapPosition] = array[i] ^ array[swapPosition]; array[i] = array[i] ^ array[swapPosition]; } } return array; } private boolean inOrder(int[] array) { for (int i = 0; i < array.length - 1; i++) { if (array[i] > array[i + 1]) { return false; } } return true; }
原創不易,未得准許,請勿轉載,翻版必究