1. 十大排序演算法
其中 冒泡,選擇,歸併,快速,希爾,堆排序屬於比較排序
穩定性理解
如果相等的兩個元素,在排序前後的相對位置保持不變,那麼這是穩定的排序演算法。
-
排序前:5,1,3(a),4,7,3(b)
-
穩定的排序:1,3(a),3(b),4,5,7
-
不穩定的排序:1,3(b),3(a),4,5,7
原地演算法(In-place Algorithm)理解
定義:不依賴額外的資源或依賴少數的額外資源(空間複雜度較低),僅依靠輸出覆蓋輸入(例如直接對輸入的陣列進行操作)
2. 工具類
用於提供測試資料與測試程式碼正確性
2.1 斷言工具類
public class Asserts {
public static void test(boolean value) {
try {
if (!value) throw new Exception("測試未通過");
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.2 Integers工具類
public class Integers {
/** 生成隨機數 */
public static Integer[] random(int count, int min, int max) {
if (count <= 0 || min > max) return null;
Integer[] array = new Integer[count];
int delta = max - min + 1;
for (int i = 0; i < count; i++) {
array[i] = min + (int)(Math.random() * delta);
}
return array;
}
/** 合併兩個陣列 */
public static Integer[] combine(Integer[] array1, Integer[] array2) {
if (array1 == null || array2 == null) return null;
Integer[] array = new Integer[array1.length + array2.length];
for (int i = 0; i < array1.length; i++) {
array[i] = array1[i];
}
for (int i = 0; i < array2.length; i++) {
array[i + array1.length] = array2[i];
}
return array;
}
public static Integer[] same(int count, int unsameCount) {
if (count <= 0 || unsameCount > count) return null;
Integer[] array = new Integer[count];
for (int i = 0; i < unsameCount; i++) {
array[i] = unsameCount - i;
}
for (int i = unsameCount; i < count; i++) {
array[i] = unsameCount + 1;
}
return array;
}
/**
* 生成頭部和尾部是升序的陣列
* disorderCount:希望多少個資料是無序的
*/
public static Integer[] headTailAscOrder(int min, int max, int disorderCount) {
Integer[] array = ascOrder(min, max);
if (disorderCount > array.length) return array;
int begin = (array.length - disorderCount) >> 1;
reverse(array, begin, begin + disorderCount);
return array;
}
/**
* 生成中間是升序的陣列
* disorderCount:希望多少個資料是無序的
*/
public static Integer[] centerAscOrder(int min, int max, int disorderCount) {
Integer[] array = ascOrder(min, max);
if (disorderCount > array.length) return array;
int left = disorderCount >> 1;
reverse(array, 0, left);
int right = disorderCount - left;
reverse(array, array.length - right, array.length);
return array;
}
/**
* 生成頭部是升序的陣列
* disorderCount:希望多少個資料是無序的
*/
public static Integer[] headAscOrder(int min, int max, int disorderCount) {
Integer[] array = ascOrder(min, max);
if (disorderCount > array.length) return array;
reverse(array, array.length - disorderCount, array.length);
return array;
}
/**
* 生成尾部是升序的陣列
* disorderCount:希望多少個資料是無序的
*/
public static Integer[] tailAscOrder(int min, int max, int disorderCount) {
Integer[] array = ascOrder(min, max);
if (disorderCount > array.length) return array;
reverse(array, 0, disorderCount);
return array;
}
/** 升序生成陣列 */
public static Integer[] ascOrder(int min, int max) {
if (min > max) return null;
Integer[] array = new Integer[max - min + 1];
for (int i = 0; i < array.length; i++) {
array[i] = min++;
}
return array;
}
/** 降序生成陣列 */
public static Integer[] descOrder(int min, int max) {
if (min > max) return null;
Integer[] array = new Integer[max - min + 1];
for (int i = 0; i < array.length; i++) {
array[i] = max--;
}
return array;
}
/** 反轉陣列 */
private static void reverse(Integer[] array, int begin, int end) {
int count = (end - begin) >> 1;
int sum = begin + end - 1;
for (int i = begin; i < begin + count; i++) {
int j = sum - i;
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
/** 複製陣列 */
public static Integer[] copy(Integer[] array) {
return Arrays.copyOf(array, array.length);
}
/** 判斷陣列是否升序 */
public static boolean isAscOrder(Integer[] array) {
if (array == null || array.length == 0) return false;
for (int i = 1; i < array.length; i++) {
if (array[i - 1] > array[i]) return false;
}
return true;
}
/** 列印陣列 */
public static void println(Integer[] array) {
if (array == null) return;
StringBuilder string = new StringBuilder();
for (int i = 0; i < array.length; i++) {
if (i != 0) string.append("_");
string.append(array[i]);
}
System.out.println(string);
}
}
2.3 時間測試工具類
public class Times {
private static final SimpleDateFormat fmt = new SimpleDateFormat("HH:mm:ss.SSS");
public interface Task {
void execute();
}
public static void test(String title, Task task) {
if (task == null) return;
title = (title == null) ? "" : ("【" + title + "】");
System.out.println(title);
System.out.println("開始:" + fmt.format(new Date()));
long begin = System.currentTimeMillis();
task.execute();
long end = System.currentTimeMillis();
System.out.println("結束:" + fmt.format(new Date()));
double delta = (end - begin) / 1000.0;
System.out.println("耗時:" + delta + "秒");
System.out.println("-------------------------------------");
}
}
2.4 Sort抽象父類
public abstract class Sort<T extends Comparable<T>> implements Comparable<Sort<T>> {
/** 目標陣列 */
protected T[] array;
/** 比較次數 */
private int cmpCount;
/** 交換次數 */
private int swapCount;
/** 執行時間 */
private long time;
/** 小數格式化 */
private DecimalFormat fmt = new DecimalFormat("#.00");
/** 預處理 */
public void sort(T[] array) {
if (array == null || array.length < 2) return;
this.array = array;
long begin = System.currentTimeMillis();
sort();
time = System.currentTimeMillis() - begin;
}
/** 目標方法 */
protected abstract void sort();
/**
* 比較陣列下標對應的值
*
* 返回值等於0,代表 array[index1] == array[index2]
* 返回值小於0,代表 array[index1] < array[index2]
* 返回值大於0,代表 array[index1] > array[index2]
*/
protected int cmp(int index1, int index2) {
cmpCount++;
return array[index1].compareTo(array[index2]);
}
/** 比較值 */
protected int cmp(T value1, T value2) {
cmpCount++;
return value1.compareTo(value2);
}
/** 交換值 */
protected void swap(int index1, int index2) {
swapCount++;
T tmp = array[index1];
array[index1] = array[index2];
array[index2] = tmp;
}
/** 穩定性測試 */
@SuppressWarnings("unchecked")
private boolean isStable() {
Student[] students = new Sort.Student[20];
for (int i = 0; i < students.length; i++) {
//(0,10) (10,10) (20,10) (30,10)
students[i] = new Student(i * 10, 10);
}
sort((T[]) students);//只會對年齡進行排序
for (int i = 1; i < students.length; i++) {
int score = students[i].score;
int prevScore = students[i - 1].score;
if (score != prevScore + 10) return false;
}
return true;
}
private static class Student implements Comparable<Student>{
Integer score;
Integer age;
public Student(Integer score, Integer age) {
this.score = score;
this.age = age;
}
@Override
public int compareTo(Student o) {
return age - o.age;
}
}
/** 排序方式 */
@Override
public int compareTo(Sort o) {
int result = (int)(time - o.time);
if(result != 0) return result;
result = cmpCount - o.cmpCount;
if(result != 0) return result;
return swapCount - o.swapCount;
}
@Override
public String toString() {
return "【" + getClass().getSimpleName() + "】\n"
+ "交換次數 ==> " + numberString(swapCount) + "\n"
+ "比較次數 ==> " + numberString(cmpCount) + "\n"
+ "執行時間 ==> " + time * 0.001 + "s" + "\n"
+ "穩定性 ==> " + isStable() + "\n"
+ "=================================";
}
/** 數字格式化 */
private String numberString(int number) {
if (number < 10000) return "" + number;
if (number < 100000000) {
return fmt.format(number / 10000.0) + "萬";
}
return fmt.format(number / 100000000.0) + "億";
}
}
3. 氣泡排序(Bubble Sort)
3.1 執行流程
- 從頭開始比較每一對相鄰元素,如果第一個比第二個大就交換它們的位置。執行完一輪後最末尾哪個元素就是最大的元素
- 忽略第一步找到的最大元素,重複執行第一步,直到全部元素有序
3.2 基本實現
public void sort() {
for (int eIndex = array.length - 1; eIndex > 0; eIndex--) {
for (int i = 1; i <= eIndex; i++) {
if (cmp(i, i - 1) < 0) {
swap(i, i - 1);
}
}
}
}
3.4 優化一
優化方案:如果序列已經完全有序,可以提前終止氣泡排序
缺點:只有當完全有序時才會提前終止氣泡排序,概率很低
public void sort() {
for (int eIndex = array.length - 1; eIndex > 0; eIndex--) {
boolean sorted = true;
for (int i = 1; i <= eIndex; i++) {
if (cmp(i,i - 1) < 0) {
swap(i, i - 1);
sorted = false;
}
}
if (sorted) break;
}
}
3.5 優化二
優化方案:如果序列尾部已經區域性有序,可以記錄最後一次交換的位置,減少比較次數
public class BubbleSort<T extends Comparable<T>> extends Sort<T> {
/**
* 優化方式二:如果序列尾部已經區域性有序,可以記錄最後依次交換的位置,減少比較次數
* 為什麼這裡sortedIndex為1(只要保證 eIndex-- > 0 即可)?
* => 如果sortedIndex為eIndex,當陣列第一次就完全有序時,就退回到最初的版本了
* => 如果sortedIndex為1,當陣列第一次就完全有序時,一輪掃描就結束了!
*
*/
@Override
public void sort() {
for (int eIndex = array.length - 1; eIndex > 0; eIndex--) {
int sortedIndex = 1; //記錄最後一次交換的下標位置
for (int i = 1; i <= eIndex; i++) {
if (cmp(i, i - 1) < 0) {
swap(i, i - 1);
sortedIndex = i;
}
}
eIndex = sortedIndex;
}
}
}
3.6 演算法優劣
-
最壞,平均時間複雜度:O(n^2),最好時間複雜度:O(n)
-
空間複雜度:O(1)
-
屬於穩定排序
注意:稍有不慎,穩定的排序演算法也能被寫成不穩定的排序演算法,如下氣泡排序是不穩定的
public void sort() {
for (int eIndex = array.length - 1; eIndex > 0; eIndex--) {
for (int i = 1; i <= eIndex; i++) {
if (cmp(i, i - 1) <= 0) {
swap(i, i - 1);
}
}
}
}
- 屬於原地演算法
4. 選擇排序(Selection Sort)
4.1 執行流程
- 從序列中找出最大的哪個元素,然後與最末尾的元素交換位置。執行完一輪後最末尾那個元素就是最大的元素
- 忽略第一步找到的最大元素,重複執行第一步
這裡以選最小元素為例
4.2 基本實現
public class SelectionSort<T extends Comparable<T>> extends Sort<T> {
@Override
public void sort() {
for (int eIndex = array.length - 1; eIndex > 0; eIndex--) {
int maxIndex = 0;
for (int i = 1; i <= eIndex; i++) {
//注意:為了穩定性,這裡要寫 <=
if (cmp(maxIndex, i) <= 0) {
maxIndex = i;
}
}
if(maxIndex != eIndex) swap(maxIndex, eIndex);
}
}
}
4.3 演算法優劣
- 選擇排序的交換次數要遠少於氣泡排序,平均效能優於氣泡排序
- 最好,最壞,平均時間複雜度均為O(n^2),空間複雜度為O(1),屬於不穩定排序
選擇排序是否還有優化的空間? => 使用堆來選擇最大值
5. 堆排序(Heap Sort)
堆排序可以認為是對選擇排序的一種優化
5.1 執行流程
- 對序列進行原地建堆(heapify)
- 重複執行以下操作,直到堆的元素數量為1
- 交換堆頂元素與尾元素
- 堆的元素數量減1
- 對0位置進行一次siftDown操作
5.2 基本實現
public class HeapSort<T extends Comparable<T>> extends Sort<T> {
/** 記錄堆資料 */
private int heapSize;
@Override
protected void sort() {
// 原地建堆(直接使用陣列建堆)
heapSize = array.length;
for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
siftDown(i);
}
while (heapSize > 1) {
// 交換堆頂元素和尾部元素
swap(0, --heapSize);
// 對0位置進行siftDown(恢復堆的性質)
siftDown(0);
}
}
/** 堆化 */
private void siftDown(int index) {
T element = array[index];
int half = heapSize >> 1;
while (index < half) { // index必須是非葉子節點
// 預設是左邊跟父節點比
int childIndex = (index << 1) + 1;
T child = array[childIndex];
int rightIndex = childIndex + 1;
// 右子節點比左子節點大
if (rightIndex < heapSize &&
cmp(array[rightIndex], child) > 0) {
child = array[childIndex = rightIndex];
}
// 大於等於子節點
if (cmp(element, child) >= 0) break;
array[index] = child;
index = childIndex;
}
array[index] = element;
}
}
5.2 演算法優劣
-
最好,最壞,平均時間複雜度:O(nlog^n)
-
空間複雜度:O(1)
-
屬於不穩定排序
5.3. 冒泡,選擇,堆排序比較
@SuppressWarnings({"rawtypes","unchecked"})
public class SortTest {
public static void main(String[] args) {
Integer[] arr1 = Integers.random(10000, 1, 20000);
testSort(arr1,
new SelectionSort(),
new HeapSort(),
new BubbleSort());
}
static void testSort(Integer[] arr,Sort... sorts) {
for (Sort sort: sorts) {
Integer[] newArr = Integers.copy(arr);
sort.sort(newArr);
//檢查排序正確性
Asserts.test(Integers.isAscOrder(newArr));
}
Arrays.sort(sorts);
for (Sort sort: sorts) {
System.out.println(sort);
}
}
}
6. 插入排序(Insertion Sort)
6.1 執行流程
-
在執行過程中,插入排序會將序列分為兩部分(頭部是已經排好序的,尾部是待排序的)
-
從頭開始掃描每一個元素,每當掃描到一個元素,就將它插入到頭部適合的位置,使得頭部資料依然保持有序
6.2 基本實現
public class InsertionSort<T extends Comparable<T>> extends Sort<T> {
@Override
protected void sort() {
for (int i = 1; i < array.length; i++) {
int cur = i;
while(cur > 0 && cmp(cur,cur - 1) < 0) {
swap(cur,cur - 1);
cur--;
}
}
}
}
6.3 逆序對(Inversion)
什麼是逆序對? => 陣列 [2,3,8,6,1] 的逆序對為:<2,1> < 3,1> <8,1> <8,6> <6,1>
插入排序的時間複雜度與逆序對的數量成正比關係
時間複雜度最高如下:O(n^2)
6.4 優化一
優化思路 => 將交換改為挪動
-
先將待插入元素備份
-
頭部有序資料中比待插入元素大的,都朝尾部方向挪動1個位置
-
將待插入元素放到最終合適位置
注意:逆序對越多,該優化越明顯
public class InsertionSort<T extends Comparable<T>> extends Sort<T> {
@Override
protected void sort() {
for (int i = 1; i < array.length; i++) {
int cur = i;
T val = array[cur];
while(cur > 0 && cmp(val,array[cur - 1]) < 0) {
array[cur] = array[cur - 1];//優化重點在這裡
cur--;
}
array[cur] = val;
}
}
}
6.5 優化二
優化思路 => 將交換改為二分搜尋(較少比較次數)
二分搜尋理解
如何確定一個元素在陣列中的位置?(假設陣列裡全是整數)
-
如果是無序陣列,從第 0 個位置開始遍歷搜尋,平均時間複雜度:O(n)
-
如果是有序陣列,可以使用二分搜尋,最壞時間複雜度:O(log^n)
思路
- 如下,假設在 [begin, end) 範圍內搜尋某個元素 v,mid == (begin + end) / 2
- 如果 v < mid,去 [begin,mid) 範圍內二分搜尋
- 如果 v > mid,去 [mid + 1,end) 範圍內二分搜尋
- 如果 v == mid,直接返回 mid
例項
/** 二分搜尋-基本實現
* 查詢val在有序陣列arr中的位置,找不到就返回-1
*/
private static int indexOf(Integer[] arr,int val) {
if(arr == null || arr.length == 0) return -1;
int begin = 0;
//注意這裡end設計為arr.length便於求數量(end - begin)
int end = arr.length;
while (begin < end) {
int mid = (begin + end) >> 1;
if(val < arr[mid]) {
end = mid;
} else if(val > arr[mid]) {
begin = mid + 1;
} else {
return mid;
}
}
return -1;
}
二分搜尋(Binary Search)優化實現
- 之前的插入排序程式碼,在元素 val 的插入過程中,可以先二分搜尋出合適的插入位置,然後將元素 val 插入
- 適合於插入排序的二分搜尋必須滿足:要求二分搜尋返回的插入位置是第1個大於 val 的元素位置
- 如果 val 是 5 ,返回 2
- 如果 val 是 1,返回 0
- 如果 val 是15,返回 7
- 如果 val 是 8,返回 5
- 實現思路
- 假設在 [begin,end) 範圍內搜尋某個元素 val,mid == (begin + end) / 2
- 如果val < mid,去 [begin,mid) 範圍內二分搜尋
- 如果val >= mid,去 [mid + 1,end) 範圍內二分搜尋
- 當 begin == end == x,x 就是待插入位置
- 例項
/**
* 二分搜尋-適用於插入排序
* 查詢val在有序陣列arr中可以插入的位置
* 規定:要求二分搜尋返回的插入位置是第1個大於 val 的元素位置
*/
private static int search(Integer[] arr,int val) {
if(arr == null || arr.length == 0) return -1;
int begin = 0;
int end = arr.length;
while (begin < end) {
int mid = (begin + end) >> 1;
if(val < arr[mid]) {
end = mid;
} else {
begin = mid + 1;
}
}
return begin;
}
插入排序最終實現
注意:使用了二分搜尋後,只是減少了比較次數,但插入排序的平均時間複雜度依然是O(n^2)
public class InsertionSort<T extends Comparable<T>> extends Sort<T> {
/** 優化 => 二分搜尋 */
@Override
protected void sort() {
for (int begin = 1; begin < array.length; begin++) {
//這裡為什麼傳索引而不是傳值?
// => 傳索引還可以知道前面已經排好序的陣列區間:[0,i)
insert(begin,search(begin));
}
}
/** 將source位置的元素插入到dest位置 */
private void insert(int source,int dest) {
//將[dest,source)範圍內的元素往右邊挪動一位
T val = array[source];
for (int i = source; i > dest; i--) {
array[i] = array[i - 1];
}
//插入
array[dest] = val;
}
private int search(int index) {
T val = array[index];
int begin = 0;
int end = index;
while (begin < end) {
int mid = (begin + end) >> 1;
if(cmp(val,array[mid]) < 0) {
end = mid;
} else {
begin = mid + 1;
}
}
return begin;
}
}
6.6 演算法優劣
- 最壞,平均時間複雜度為 O(n^2),最好時間複雜度為 O(n)
- 空間複雜度為 O(1)
- 屬於穩定排序
7. 歸併排序(Merge Sort)
7.1 執行流程
- 不斷的將當前序列平均分割成 2 個子序列,直到不能再分割(序列中只剩一個元素)
- 不斷的將 2 個子序列合併成一個有序序列,直到最終只剩下 1 個有序序列
7.2 思路
merge
大致想法
細節
- 需要 merge 的 2 組序列存在於同一個陣列中,並且是挨在一起的
- 為了更好的完成 merge 操作,最好將其中 1 組序列備份出來,比如 [begin,mid)
- 基本實現
- 情況一:左邊先結束 => 左邊一結束整個歸併就結束
- 情況二:右邊先結束 => 右邊一結束就直接將左邊按順序挪到右邊去
7.3 基本實現
@SuppressWarnings("unchecked")
public class MergeSort<T extends Comparable<T>> extends Sort<T> {
private T[] leftArr;
@Override
protected void sort() {
leftArr = (T[]) new Comparable[array.length >> 1];
sort(0, array.length);
}
/** 對 [begin,end) 位置的元素進行歸併排序 */
private void sort(int begin, int end) {
if (end - begin < 2) return;
int mid = (begin + end) >> 1;
sort(begin, mid);
sort(mid, end);
merge(begin, mid, end);
}
/** 將 [begin,mid) 和 [mid,end) 範圍的序列合併成一個有序序列 */
private void merge(int begin, int mid, int end) {
int li = 0, le = mid - begin;
int ri = mid, re = end;
int ai = begin;
//備份左邊陣列
for (int i = 0; i < le; i++) {
leftArr[i] = array[begin + i];
}
//如果左邊還沒有結束(情況一)
while (li < le) {
//當 ri < re 不成立,就會一直leftArr挪動(情況二)
if (ri < re && cmp(array[ri],leftArr[li]) < 0) {
array[ai++] = array[ri++];
} else { //注意穩定性
array[ai++] = leftArr[li++];
}
}
}
}
7.4 演算法優劣
複雜度分析
T(n) = sort() + sort() + merge()
=> T(n) = T(n/2) + T(n/2) + O(n)
=> T(n) = 2T(n/2) + O(n)
//由於sort()是遞迴呼叫,用T表示,由於T(n/2)不好估算,現在要理清T(n)與O(n)之間的關係
T(1) = O(1)
T(n)/n = T(n/2) / (n/2) + O(1)
//令S(n) = T(n)/n
S(1) = O(1)
S(n) = S(n/2) + O(1)
= S(n/4) + O(2)
= S(n/8) + O(3)
= S( n/(2^k) ) + O(k)
= S(1) + O(log^n)
= O(lon^n)
T(n) = n*S(n) = O(nlog^n)
=> 歸併排序時間複雜度:O(nlog^n)
常見遞推式
總結
-
由於歸併排序總是平均分割子序列,所以最好,最壞,平均時間複雜度都是:O(nlog^n)
-
空間複雜度:O(n/2 + log^n) = O(n),n/2用於臨時存放左側陣列,log^n用於遞迴呼叫
-
屬於穩定排序