本文統統是升序。
一、十一種排序算法
1、排序算法間的比較
排序算法名稱 | 時間複雜度 | 空間複雜度 | 穩定性 | ||
---|---|---|---|---|---|
最好 | 平均 | 最壞 | |||
冒泡排序 | O(N) | O(N2) | O(N2) | O(1) | 穩定 |
選擇排序 | O(N2) | O(N2) | O(N2) | O(1) | 不穩定 |
排堆序 | O(N)+O(N logN) | O(N)+O(N logN) | O(N)+O(N logN) | O(1) | 不穩定 |
插入排序 | O(N) | O(N2) | O(N2) | O(1) | 穩定 |
希爾排序 | O(N) | 取決於步長序列 | 取決於步長序列 | O(1) | 不穩定 |
歸併排序 | O(N)=2O(N/2)+O(N) 即 O(N logN) | O(N logN) | O(N logN) | O(N)---(遞歸+N/2空間的新數組) | 穩定 |
快速排序 | O(N)=2O(N/2)+O(N) 即 O(N logN) | O(N logN) | O(N)=O(N-1)+O(N) 即 O(N2) | O(logN)---(遞歸) | 不穩定 |
計數排序 | O(N+K)---(K=max-min+1) | O(N+K) | O(N+K) | O(N+K) | 穩定 |
基數排序 | dxO(N+K)---(d是整數位數) | dxO(N+K)---(K既是整數範圍也是進制數) | dxO(N+K) | O(N+K)---(已複用計數數組和目標數組) | 穩定 |
桶排序 | O(N+K)---(大概) | O(N+K) | O(N+K) | O(N+M)---(M是桶的數量) | 穩定 |
休眠排序 | O(N)---(這種耗時捨我其誰) | O(N)---(滑稽臉) | O(N)---(滑稽臉) | O(N) | 不穩定 |
2、排序算法的分類
① 基於比較的排序
冒泡排序、選擇排序、推排序、插入排序、希爾排序、歸併排序、快速排序
② 非比較的排序
計數排序、基數排序、桶排序
③ 史上最強排序
休眠排序當之無愧穩居第一。
二、冒泡排序
1、思路
冒泡其實就是將前一個元素和後一個元素比較,如果前面元素比後面大就交換兩個位置,那一輪比較後其實最大的數就在數組末尾了,執行n-1次循環。今天問了不少人冒泡排序怎麼寫,外面兩層for循環都是清一色這樣的:
for(int count =0;count<array.length-1;count++) {
for(int i=0;i<array.length-1-count;i++) {
這樣其實就不方便進行尾部優化了。
如何優化?①最簡單的優化方式其實就是設個flag,如果一次循環中沒有發生交換,那就不需要再執行下一輪循環了,數組已經有序了。
② 還一種優化方式就是,每次都記錄下最後一次冒泡的索引,下次執行循環的時候只需要比較到那個索引位置就可以了
2、代碼
未優化代碼:
public class BubbleSort1<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - 1 - i; j++) {
if (compareByIndex(i, i + 1) > 0) {
swapByIndex(i, i + 1);
}
}
}
}
}
優化代碼①:
public class BubbleSort2<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
for (int i = 0; i < array.length - 1; i++) {
boolean isAscOrder = true;
for (int j = 0; j < array.length - 1 - i; j++) {
if (compareByIndex(i, i + 1) > 0) {
swapByIndex(i, i + 1);
isAscOrder = false;
}
}
if (isAscOrder) //如果冒泡的時候發現數組已經有序了就不再執行循環
return;
}
}
}
優化代碼②:
public class BubbleSort3<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
for (int end = array.length - 1; end > 0; end--) {
int lastIndex = 0;
for (int begin = 0; begin < end; begin++) {
if (compareByIndex(begin, begin + 1) > 0) {
swapByIndex(begin, begin + 1);
lastIndex = begin + 1; //記錄最後進行交換的較大索引(外層循環還要減減)
}
}
end = lastIndex;
}
}
}
可以看到實現的時候如果值相等並沒有交換,冒泡排序完全可以實現爲穩定的排序。
三、選擇排序
1、思路
思路很簡單,每次循環從數組中選出最大值,將最大值和數組尾部元素交換位置,執行n-1次循環。
2、代碼
public class SelectionSort<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
for (int end = array.length - 1; end > 0; end--) {
int maxIndex = 0;
for (int begin = 1; begin <= end; begin++) {
if (compareByIndex(maxIndex, begin) < 0)
maxIndex = begin;
}
swapByIndex(maxIndex, end); //每次都將最大值和數組尾部元素交換
}
}
}
選擇排序是不穩定的排序,當最大值正好是索引爲0的元素,並且此時數組中有多個最大值的時候,第一次排序直接改變了相對位置。
四、堆排序
1、思路
很簡單,既然是升序直接建個最大堆,每次都取出堆頂元素和數組末尾元素交換,堆頂進行下濾,並將堆的末尾元素剔除,直到堆中只剩下一個元素爲止。和選擇排序思路一樣,可以看成是選擇排序的升級版。
2、代碼
public class HeapSort<E extends Comparable> extends Sort<E> {
private int size = 0;
/**
* 建立堆
*
* @return
*/
private E[] heapBatch() {
size = array.length;
for (int i = (size >> 1) - 1; i >= 0; i--) //從最後個非葉子結點開始下濾
slipDown(i);
return array;
}
/**
* 下濾
*
* @param index 執行下濾的索引
*/
private void slipDown(int index) {
E element = array[index];
while (index < (size >> 1)) {
int maxChildIndex = (index << 1) + 1;
E maxChild = array[maxChildIndex];
if (maxChildIndex + 1 < size && compare(maxChild, array[maxChildIndex + 1]) < 0) {
maxChildIndex++;
maxChild = array[maxChildIndex];
}
if (compare(maxChild, element) > 0)
array[index] = maxChild;
else
return;
index = maxChildIndex;
}
array[index] = element;
}
@Override
protected void sort() {
heapBatch();
do {
swapByIndex(0, --size);
slipDown(0);
} while (size > 1);
}
}
堆排序和選擇排序思想一樣,所以也是不穩定的排序算法。
五、插入排序
1、思路
和撲克牌差不多,尾部插入。一輪循環摸一次牌,摸到新牌的時候需要依次和前面已經排好順序的牌進行比較,如果新的牌比前面的牌大就交換,否則就摸下一張牌,直到結束。
如何優化?①一張張牌一次交換可以優化爲牌的挪動,先從已經排好序的牌中遍歷找到第一張比摸到的新牌大的牌並記錄其位置,將從新牌位置到記錄位置上的所有牌都往後挪動一個位置,將新牌直接放到之前記錄的位置上。
②之前查找位置的時候是逐個遍歷的,因爲遍歷的數組已經是有序了,可用二分查找查出新牌插入的位置,之後也是挪動,優化了查詢次數,再將新牌放到二分查找查出的位置上。
2、代碼
未優化代碼:
public class InsertionSort1<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
for (int begin = 1; begin < array.length; begin++) {
for (int index = begin - 1; index >= 0; index--) {
if (compareByIndex(index, index + 1) > 0) //前面的數比後面數大就不斷交換
swapByIndex(index, index + 1);
else
break;
}
}
}
}
優化代碼①:
public class InsertionSort2<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
for (int begin = 1; begin < array.length; begin++) {
E v = array[begin];
int index;
for (index = begin - 1; index >= 0; index--) {
if (compare(array[index], v) > 0) //前面的數比待插入的數大就不斷往後挪
array[index + 1] = array[index];
else
break;
}
array[index + 1] = v; //跳出循環的時候已經自減過一了, 需要加回來
}
}
}
優化代碼②:
public class InsertionSort3<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
for (int begin = 1; begin < array.length; begin++) {
E v = array[begin];
int index = searchIndex(begin, v);
for (int temp = begin - 1; temp >= index; temp--)
array[temp + 1] = array[temp];
array[index] = v;
}
}
/**
* [begin, end]
*/
public int indexOf1(E[] integers, E element) {
int begin = 0, end = integers.length - 1;
while (begin <= end) {
int mid = (begin + end) / 2;
if (compare(integers[mid], element) < 0)
begin = mid + 1;
else if (compare(integers[mid], element) > 0)
end = mid - 1;
else
return mid;
}
return -1;
}
/**
* [begin, end)
*/
public int indexOf2(E[] integers, E element) {
int begin = 0, end = integers.length;
while (begin < end) {
int mid = (begin + end) / 2;
if (compare(integers[mid], element) < 0)
begin = mid + 1;
else if (compare(integers[mid], element) > 0)
end = mid;
else
return mid;
}
return -1;
}
/**
* 從左邊已經排好序的數組中查找元素的插入位置
*
* @param index 尾索引
* @return 元素的插入位置
*/
private int searchIndex(int index, E element) {
int begin = 0, end = index;
while (begin < end) {
int mid = (begin + end) >> 1;
if (compare(array[mid], element) > 0)
end = mid;
else //如果中間元素小於等於待查找元素就去右邊查找
begin = mid + 1;
}
return begin;
}
}
二分查找的時候用的是左閉右開區間,參考代碼中給的indexOf2函數
,這樣方便優化。可以看到插入排序也可以實現爲穩定的排序,二分搜索的時候兩元素一旦相等就還要去右邊尋找,元素的相對位置並沒有變化。
六、希爾排序
1、思路
根據步長遞減序列,依次將數組分爲n列,對每一列元素進行排序,最後步長必須是1,再對步長爲1的序列排序後即希爾排序算法結束。其實每次根據步長對數組進行排序之後會減少數組中逆序對的數量,因此希爾排序底層可以用插入排序來實現。當然希爾排序也可以看成是插入排序的升級版。
2、代碼
public class ShellSort<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
List<Integer> stepSequence = generateSedgewickStepSequence();
for (Integer step : stepSequence) {
sort(step);
}
}
private void sort(int step) {
for (int i = 0; i < step; i++) {
//新牌索引 = 所在行索引 * step + i
for (int begin = i + step; begin < array.length; begin += step) { //從第二個數開始插入
int index = begin;
E v = array[begin];
for (index = begin - step; index >= 0; index -= step) {
if (compare(array[index], v) > 0)
array[index + step] = array[index];
else
break;
}
array[index + step] = v;
}
}
}
/**
* 希爾步長
*/
private List<Integer> generateShellStepSequence() {
List<Integer> stepSequence = new LinkedList<>();
Integer i = array.length;
do {
i = (i >> 1);
stepSequence.add(i);
} while (i > 0);
return stepSequence;
}
/**
* 最優步長
*/
private List<Integer> generateSedgewickStepSequence() {
List<Integer> stepSequence = new LinkedList<>();
int k = 0, step = 0;
do {
if (k % 2 == 0) {
step = 9 * ((1 << k) - (1 << (k >> 1))) + 1;
} else {
step = (8 << k) - (6 << ((k + 1) >> 1)) + 1;
}
if (step > array.length)
break;
stepSequence.add(0, step);
k++;
} while (true);
return stepSequence;
}
}
希爾排序雖然底層用的是插入排序,但是因爲根據步長序列分了組,相等的值無法保證順序一致性,因此希爾排序是不穩定的排序算法。
七、歸併排序
1、思路
不斷將待排序的數組平均拆分成兩個子序列,直到只有一個元素,再將相鄰的子序列不斷合併成有序的序列,直到最後只剩下一個序列,排序結束。明顯需要用到遞歸,寫遞歸要搞清楚遞歸結束的條件!這裏很簡單,明顯就是子序列中只剩下一個元素則直接返回了。
2、代碼
public class MergeSort<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
sort(0, array.length);
}
private void sort(int begin, int end) {
if (end - begin < 2) //拆分的序列中只有一個元素直接返回
return;
int mid = (begin + end) >> 1;
sort(begin, mid); // [begin, mid)
sort(mid, end); // [mid, end)
merge(begin, mid, end);
}
/**
* 將兩個有序的子序列合併成一個大序列
*/
private void merge(int begin, int mid, int end) {
E[] leftPart = (E[]) new Comparable[mid - begin];
for (int i = 0; i < leftPart.length; i++)
leftPart[i] = array[begin + i];
int leftBegin = begin, leftEnd = mid, rightBegin = mid, rightEnd = end, nowIndex = begin;
while (leftBegin < leftEnd) {
if (rightBegin < rightEnd && compareByIndex(leftBegin, rightBegin) > 0)
array[nowIndex++] = array[rightBegin++];
else //左邊元素小於等於右邊元素就取左邊元素
array[nowIndex++] = array[leftBegin++];
}
}
}
可從代碼看出歸併排序完全可以實現爲穩定的排序,即只有右邊元素大於左邊元素的時候,才合併右邊的元素,否則都是取左邊的元素(包括相等)。
八、快速排序
1、思路
思想很簡單,每次從數組中選取一個軸點(不一定非要是第一個元素),第一次排序後比軸點元素小的都在軸點左邊,比軸點元素大的都在軸點右邊,相等隨意放(因爲到最後結束的時候肯定每個元素都曾經軸點了,因此保持不了快排算法的穩定性)。緊接着對軸點左邊的進行快排,右邊的進行快排。遞歸可以實現,還是要先搞清楚遞歸結束的條件,其實還是數組中只有一個元素就返回。
2、代碼
public class QuickSort<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
sort(0, array.length);
}
private int pivotIndex(int begin, int end) {
end--; //選取其他元素作爲軸點元素只需要和begin上的元素交換位置即可
E v = array[begin];
boolean flag = true; //默認先從右邊遍歷
while (begin < end) {
if (flag) {
if (compare(array[end], v) > 0) //右邊的元素比軸點元素大
end--;
else {
array[begin++] = array[end];
flag = false;
}
} else { //需要添加else不然數組可能會越界
if (compare(array[begin], v) < 0) //左邊的元素比軸點元素小
begin++;
else {
array[end--] = array[begin];
flag = true;
}
}
}
array[begin] = v;
return begin;
}
private void sort(int begin, int end) {
if (end - begin < 2) //所有元素都是軸點就返回
return;
int mid = pivotIndex(begin, end);
sort(begin, mid);
sort(mid + 1, end);
}
}
快速排序本質就是將所有元素都變成軸點,所以不可能保證排序算法的穩定性。
九、計數排序
1、思路
用一個能包含最大值索引的數組記錄每個元素出現的次數(直接將數字當作索引),再根據次數對數組元素進行排序。
如何優化?不僅計算數組最大值還是計算最小值,new數組的時候將空間壓縮,而且記錄元素出現次數的數組中不僅包括自己出現的次數還要加上前面所有元素出現過的次數,最後從右往左遍歷起始的數組,根據記錄次數的數組計算出索引,再放到數組的相應位置即可。
2、代碼
未優化代碼:
public class CountSort1<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
Integer[] array = (Integer[]) this.array;
int max = array[0];
int index = 0;
for (index = 1; index < array.length; index++) {
if (compare(max, array[index]) < 0)
max = array[index];
}
int[] newArray = new int[max + 1]; //int數組new出來默認都是0
for (index = 0; index < array.length; index++)
newArray[array[index]]++;
int current = 0;
for (index = 0; index < newArray.length; index++) {
if (newArray[index] != 0) {
int count = newArray[index];
while (count-- > 0)
array[current++] = index;
}
}
this.array = (E[]) array;
}
}
優化代碼:
public class CountSort2<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
Integer[] array = (Integer[]) this.array;
int max = array[0];
int min = array[0];
int index = 0;
for (index = 1; index < array.length; index++) {
if (compare(max, array[index]) < 0)
max = array[index];
if (compare(min, array[index]) > 0)
min = array[index];
}
int[] counts = new int[max - min + 1]; //int數組new出來默認都是0
for (index = 0; index < array.length; index++)
counts[array[index] - min]++;
for (index = 1; index < counts.length; index++)
counts[index] += counts[index - 1];
Integer[] newArray = new Integer[array.length];
for (index = array.length - 1; index >= 0; index--) {
int newIndex = --counts[array[index] - min];
newArray[newIndex] = array[index];
}
this.array = (E[]) newArray;
}
}
十、基數排序
1、思路
依次對個位數、十位數、百位數、千位數、萬位數等等(從低位到高位)進行計數排序即可。
2、代碼
public class RadixSort<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
Integer[] array = (Integer[]) this.array;
int max = array[0];
int index = 0;
for (index = 1; index < array.length; index++) {
if (compare(max, array[index]) < 0)
max = array[index];
}
for (int radix = 1; radix <= max; radix *= 10) {
sortByRadix(radix);
}
}
private void sortByRadix(int radix) {
Integer[] array = (Integer[]) this.array;
int index = 0;
int[] counts = new int[10]; //直接用0-9計數
for (index = 0; index < array.length; index++)
counts[array[index] * radix % 10]++;
for (index = 1; index < counts.length; index++)
counts[index] += counts[index - 1];
Integer[] newArray = new Integer[array.length];
for (index = array.length - 1; index >= 0; index--) {
int newIndex = --counts[array[index] * radix % 10];
newArray[newIndex] = array[index];
}
this.array = (E[]) newArray;
}
}
十一、桶排序
1、思路
有點分而治之的思想。首先創建一定數量的桶(可用數組、鏈表作爲桶),按照一定的規則將數組中的元素均勻分配到對應的桶裏面,然後分別對每個桶進行單獨排序,最後將所有非空桶的元素合併成有序序列,桶排序結束。
2、代碼
這裏以將整形數據除以10得到的值作爲索引放入桶裏面。
public class BucketSort<E extends Comparable> extends Sort<E> {
@Override
protected void sort() {
Integer[] array = (Integer[]) this.array;
int max = array[0];
int index = 0;
for (index = 1; index < array.length; index++) {
if (compare(max, array[index]) < 0)
max = array[index];
}
List<Integer>[] buckets = new ArrayList[array.length];
int current = 0;
for (index = 0; index < array.length; index++) {
List<Integer> bucket = buckets[array[index] / 10];
if (bucket == null) {
bucket = new ArrayList<>();
}
bucket.add(array[index]);
buckets[array[index] / 10] = bucket;
}
for (index = 0; index < buckets.length; index++) {
if (buckets[index] == null)
continue;
buckets[index].sort(null);
for (Integer i : buckets[index])
array[current++] = i;
}
}
}
十二、休眠排序
1、思路
每個元素都新開一條線程。。。通過線程休眠時間的長短來排序。
2、代碼
public class ThreadSort implements Runnable {
private static int current = 0;
@Override
public void run() {
return;
}
public static void main(String[] args) {
Integer[] array = {2400, 500, 201, 489, 897, 320};
Integer[] newArray = new Integer[array.length];
int max = array[0];
for (int index = 0; index < array.length; index++) {
final int indexFinal = index;
if (max < array[index])
max = array[index];
new Thread(() -> {
try {
Thread.sleep(array[indexFinal]);
newArray[ThreadSort.current++] = array[indexFinal];
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start(); //start方法纔是纔是開啓新線程
}
try {
Thread.sleep(max); //需等待開啓的線程關閉後在遍歷數組
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int index = 0; index < newArray.length; index++)
System.out.print(newArray[index] + " ");
}
}