常見排序算法總結 -- java實現
排序算法可以分爲兩大類:
- 非線性時間比較類排序:通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此稱爲非線性時間比較類排序。
- 線性時間非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間運行,因此稱爲線性時間非比較類排序。
相關概念
- 穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面。
- 不穩定:如果a原本在b的前面,而a=b,排序之後 a 可能會出現在 b 的後面。
- 時間複雜度:對排序數據的總的操作次數。反映當n變化時,操作次數呈現什麼規律。
- 空間複雜度:是指算法在計算機內執行時所需存儲空間的度量,它也是數據規模n的函數。
一、插入排序
1.1 直接插入排序
插入排序的基本操作就是將一個數據插入到已經排好序的有序數據中,從而得到一個新的、個數加一的有序數據,算法適用於少量數據的排序,時間複雜度爲O(n^2)。是穩定的排序方法。
直接插入排序的算法思路:
- 設置監視哨temp,將待插入記錄的值賦值給temp;
- 設置開始查找的位置j;
- 在數組arr中進行搜索,搜索中將第j個記錄後移,直至temp≥arr[j]爲止;
- 將temp插入arr[j+1]的位置上。
/**
* 直接插入排序
*/
public void insertSort(int[] arr) {
//外層循環確定待比較數值
//必須i=1,因爲開始從第二個數與第一個數進行比較
for (int i = 1; i < arr.length; i++) {
//待比較數值
int temp = arr[i];
int j = i - 1;
//內層循環爲待比較數值確定其最終位置
//待比較數值比前一位置小,應插往前插一位
for (; j >= 0 && arr[j] > temp; j--) {
//將大於temp的值整體後移一個單位
arr[j + 1] = arr[j];
}
//待比較數值比前一位置大,最終位置無誤
arr[j + 1] = temp;
}
}
1.2 希爾排序
希爾排序(Shell's Sort)是插入排序的一種,又稱“縮小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一種更高效的改進版本。希爾排序是非穩定排序算法。該方法因D.L.Shell於1959年提出而得名。
希爾排序的算法思路:
- 把數組按下標的一定增量分組;
- 對每組使用直接插入排序算法排序;
- 隨着增量逐漸減少,每組包含的值越來越多,當增量減至1時,整個文件被分成一組,算法便終止。
/**
* 希爾排序
*/
public void shellSort(int[] arr) {
int d = arr.length;
while (d >= 1) {
d = d / 2;
for (int x = 0; x < d; x++) {
//按下標的一定增量分組然後進行插入排序
for (int i = x + d; i < arr.length; i = i + d) {
int temp = arr[i];
int j;
for (j = i - d; j >= 0 && arr[j] > temp; j = j - d) {
//移動下標
arr[j + d] = arr[j];
}
arr[j + d] = temp;
}
}
}
}
二、交換排序
2.1 冒泡排序
在一組數據中,相鄰元素依次比較大小,最大的放後面,最小的冒上來。
冒泡排序算法的算法思路:
- 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
- 對每一對相鄰元素做同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。
- 針對所有的元素重複以上的步驟,除了最後一個。
- 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
/**
* 冒泡排序
*/
public void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// temp 臨時存儲 arr[j] 的值
int temp = arr[j];
//交換 arr[j] 和 arr[j+1] 的值
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
2.2 快速排序
快速排序(Quicksort)是對冒泡排序的一種改進。
通過一次排序將數組分成兩個子數組,其中一個數字的值都比另外一個數字的值小,然後再對這兩子數組分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
快速排序的算法思路:(分治法)
- 先從數列中取出一個數作爲中間值middle;
- 將比這個數小的數全部放在它的左邊,大於或等於它的數全部放在它的右邊;
- 對左右兩個小數列重複第二步,直至各區間只有1個數。
/**
* 快速排序
*
* @param arr 待排序數組
*/
public void quickSort(int[] arr) {
//查看數組是否爲空
if (arr.length > 0) {
sort(arr, 0, arr.length - 1);
}
}
/**
* @param arr 待排序數組
* @param low 開始位置
* @param high 結束位置
*/
private void sort(int[] arr, int low, int high) {
if (low < high) {
int mid = getMiddle(arr, low, high); //將numbers數組進行一分爲二
sort(arr, low, mid - 1); //對低字段表進行遞歸排序
sort(arr, mid + 1, high); //對高字段表進行遞歸排序
}
}
/**
* 查找出中軸(默認是最低位low)的在arr數組排序後所在位置
*
* @param arr 待排序數組
* @param low 開始位置
* @param high 結束位置
* @return 中軸所在位置
*/
private int getMiddle(int[] arr, int low, int high) {
int temp = arr[low]; //數組的第一個作爲中軸
while (low < high) {
while (low < high && arr[high] >= temp) {
high--;
}
arr[low] = arr[high];//比中軸小的記錄移到低端
while (low < high && arr[low] < temp) {
low++;
}
arr[high] = arr[low]; //比中軸大的記錄移到高端
}
arr[low] = temp; //中軸記錄到尾
return low; // 返回中軸的位置
}
三、選擇排序
3.1 簡單選擇排序
選擇排序(Selection-sort)是一種簡單直觀的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。
簡單選擇排序的算法思路:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾
- 以此類推,直到所有元素均排序完畢。
/**
* 簡單選擇排序
*/
public void selectSort(int[] arr) {
int minIndex = 0;
int temp;
for (int i = 0; i < arr.length - 1; i++) {
minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
// 找到當前循環最小值索引
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
temp = arr[i];
// 交換當前循環起點值和最小值索引位置的值
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
3.2 堆排序
堆排序(英語:Heap Sort)是指利用堆這種數據結構所設計的一種排序算法。堆是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。在堆的數據結構中,堆中的最大值總是位於根節點(在優先隊列中使用堆的話堆中的最小值位於根節點)。
堆排序的算法思路:
- 最大堆調整(Max Heapify):將堆的末端子節點作調整,某個節點的值最多和其父節點的值一樣大;
- 創建最大堆(Build Max Heap):將堆中的所有數據重新排序,堆中的最大元素存放在根節點中;
- 堆排序(HeapSort):移除位在第一個數據的根節點,並做最大堆調整的遞歸運算。
/**
* 堆排序
*/
public void heapSort(int[] arr) {
buildMaxHeap(arr);
//進行n-1次循環,完成排序
for (int i = arr.length - 1; i > 0; i--) {
//最後一個元素和第一個元素進行交換
int temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
// 篩選 R[0] 結點,得到i-1個結點的堆 將arr中前i-1個記錄重新調整爲大頂堆
heapAdjust(arr, 0, i);
}
}
/**
* 構建大頂堆
* <p>
* 將數組中最大的值放在根節點
*/
private void buildMaxHeap(int[] arr) {
for (int i = arr.length / 2; i >= 0; i--) {
heapAdjust(arr, i, arr.length - 1);
}
}
/**
* 堆調整
* <p>
* 將數組中最大的值放在根節點
*
* @param arr 待排序數組
* @param parent 父節點索引
* @param length 數組長度
*/
private void heapAdjust(int[] arr, int parent, int length) {
int temp = arr[parent]; //temp保存當前父節點
int child = 2 * parent + 1; //獲取左子節點
while (child < length) {
// 如果有右子結點,並且右子結點的值大於左子結點的值,則選取右子結點的值
if (child + 1 < length && arr[child] < arr[child + 1]) {
child++;
}
// 如果父結點的值已經大於子結點的值,則直接結束
if (temp >= arr[child]) {
break;
}
// 把子結點的值賦給父結點
arr[parent] = arr[child];
// 選取子結點的左子結點,繼續向下篩選
parent = child;
child = 2 * child + 1;
}
arr[parent] = temp;
}
四、歸併排序
4.1 二路歸併排序
歸併排序(mergeSort)是建立在歸併操作上的一種有效的排序算法,該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。
若將兩個有序表合併成一個有序表,稱爲二路歸併。例如:將2個有序數組合並。比較2個數組的第一個數,誰小就先取誰,取了後就在對應數組中刪除這個數。然後再進行比較,如果有數組爲空,那直接將另一個數組的數依次取出即可。
二路歸併排序的算法思路:
- 將數組分成A,B 兩個數組,如果這2個數組都是有序的,那麼就可以很方便的將這2個數組進行排序。
- 讓這2個數組有序,可以將A,B組各自再分成2個數組。依次類推,當分出來的數組只有1個數據時,可以認爲數組已經達到了有序。
- 然後再合併相鄰的2個數組。這樣通過先遞歸的分解數組,再合併數組就完成了歸併排序。
/**
* 二路歸併排序
*/
public void mergeSort(int[] arr) {
int[] temp = new int[arr.length]; //臨時數組
sort(arr, temp, 0, arr.length - 1);
}
/**
* @param arr 待排序數組
* @param left 開始位置
* @param right 結束位置
*/
private void sort(int[] arr, int[] temp, int left, int right) {
if (left >= right) {
return;
}
int mid = left + (right - left) / 2;
sort(arr, temp, left, mid);
sort(arr, temp, mid + 1, right);
merge(arr, temp, left, mid, right);
}
/**
* 將兩個有序表歸併成一個有序表
*
* @param arr 待排序數組
* @param temp 臨時數組
* @param leftStart 左邊開始下標
* @param leftEnd 左邊結束下標(mid)
* @param rightEnd 右邊結束下標
*/
private static void merge(int[] arr, int[] temp, int leftStart, int leftEnd, int rightEnd) {
int rightStart = leftEnd + 1;
int tempIndex = leftStart; // 從左邊開始算
int len = rightEnd - leftStart + 1; // 元素個數
while (leftStart <= leftEnd && rightStart <= rightEnd) {
if (arr[leftStart] <= arr[rightStart]) {
temp[tempIndex++] = arr[leftStart++];
} else {
temp[tempIndex++] = arr[rightStart++];
}
}
// 左邊如果有剩餘 將左邊剩餘的歸併
while (leftStart <= leftEnd) {
temp[tempIndex++] = arr[leftStart++];
}
// 右邊如果有剩餘 將右邊剩餘的歸併
while (rightStart <= rightEnd) {
temp[tempIndex++] = arr[rightStart++];
}
// 從臨時數組拷貝到原數組
for (int i = 0; i < len; i++) {
arr[rightEnd] = temp[rightEnd];
rightEnd--;
}
}
五、計數排序
計數排序(Counting sort)不是基於比較的排序算法,其核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。作爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有確定範圍的整數。
計數排序的算法思路:
- 求出待排序數組的最大值 max 和最小值 min。
- 實例化輔助計數數組temp,temp數組中每個下標對應arr中的一個元素,temp用來記錄每個元素出現的次數。
- 計算 arr 中每個元素在temp中的位置 position = arr[i] - min。
- 根據 temp 數組求得排序後的數組。
/**
* 計數排序
*/
public void countSort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
//找出數組中的最大最小值
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
int[] temp = new int[max];
//找出每個數字出現的次數
for (int i = 0; i < arr.length; i++) {
//每個元素在temp中的位置 position = arr[i] - min
int position = arr[i] - min;
temp[position]++;
}
int index = 0;
for (int i = 0; i < temp.length; i++) {
//temp[i] 大於0 表示有重複元素
while (temp[i]-- > 0) {
arr[index++] = i + min;
}
}
}
六、桶排序
桶排序 (Bucket sort)的工作原理是將數組分到有限數量的桶裏。每個桶再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序)。當要被排序的數組內的數值是均勻分配的時候,桶排序使用線性時間(Θ(n))。但桶排序並不是比較排序,他不受到 O(n log n) 下限的影響,桶排序可用於最大最小值相差較大的數據情況。
桶排序的算法思路:
- 找出待排序數組中的最大值max和最小值min;
- 我們使用動態數組ArrayList 作爲桶,桶裏放的元素也用 ArrayList 存儲。桶的數量爲 (max-min) / arr.length + 1;
- 遍歷數組 arr,計算每個元素 arr[i] 放的桶;
- 每個桶各自排序;
- 遍歷桶數組,把排序好的元素放進輸出數組。
/**
* 桶排序
*
* @param arr 待排序數組
*/
public static void bucketSort(int[] arr) {
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
//桶數
int bucketNum = (max - min) / arr.length + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for (int i = 0; i < bucketNum; i++) {
bucketArr.add(new ArrayList<>());
}
//將每個元素放入桶
for (int i = 0; i < arr.length; i++) {
int num = (arr[i] - min) / arr.length;
bucketArr.get(num).add(arr[i]);
}
//對每個桶進行排序
for (int i = 0; i < bucketNum; i++) {
Collections.sort(bucketArr.get(i));
}
int position = 0;
//合併桶
for (int i = 0; i < bucketNum; i++) {
for (int j = 0; j < bucketArr.get(i).size(); j++) {
arr[position++] = bucketArr.get(i).get(j);
}
}
}
七、基數排序
基數排序(radix sort)是桶排序的擴展,基本思想是將整數按位數切割成不同的數字,然後按每個位數分別比較。
基數排序法是屬於穩定性的排序,其時間複雜度爲O (nlog(r)m),其中r爲所採取的基數,而m爲堆數,在某些時候,基數排序法的效率高於其它的穩定性排序法。
基數排序的算法思路:
- 取得數組中的最大數,並取得位數;
- arr爲原始數組,從最低位開始取每個位組成radix數組;
- 對radix進行計數排序(利用計數排序適用於小範圍數的特點)。
/**
* 基數排序
*
* @param arr 待排序數組
*/
public void radixSort(int[] arr) {
int max = getMax(arr); // 數組arr中的最大值
for (int exp = 1; max / exp > 0; exp *= 10) {
//從個位開始,對數組arr按"exp指數"進行排序
countSort(arr, exp);
//bucketSort(arr, exp);
}
}
/**
* 獲取數組中最大值
*/
private int getMax(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
/**
* 對數組按照"某個位數"進行排序(計數排序)
* <p>
* 例如:
* 1、當exp=1 表示按照"個位"對數組進行排序
* 2、當exp=10 表示按照"十位"對數組進行排序
*
* @param arr 待排序數組
* @param exp 指數 對數組arr按照該指數進行排序
*/
private void countSort(int[] arr, int exp) {
int[] temp = new int[arr.length]; // 存儲"被排序數據"的臨時數組
int[] buckets = new int[10];
// 將數據出現的次數存儲在buckets[]中
for (int i = 0; i < arr.length; i++) {
buckets[(arr[i] / exp) % 10]++;
}
// 計算數據在temp[]中的位置 0 1 2 2 3 --> 0 1 3 5 8
for (int i = 1; i < 10; i++) {
buckets[i] += buckets[i - 1];
}
// 將數據存儲到臨時數組temp[]中
for (int i = arr.length - 1; i >= 0; i--) {
temp[buckets[(arr[i] / exp) % 10] - 1] = arr[i];
buckets[(arr[i] / exp) % 10]--;
}
// 將排序好的數據賦值給arr[]
for (int i = 0; i < arr.length; i++) {
arr[i] = temp[i];
}
}
/**
* 桶排序
*/
private void bucketSort(int[] arr, int exp) {
int[][] buckets = new int[10][arr.length]; //這是二維數組組成的桶
int[] counter = new int[10]; //此數組用來記錄0-9每個桶中的數字個數,計數器
for (int i = 0; i < arr.length; i++) {
int index = (arr[i] / exp) % 10; //得出相應位置(如個位、十位)上的數字
buckets[index][counter[index]] = arr[i]; //取出來放到桶裏
counter[index]++; //相應的計數器加1
}
int position = 0;
//合併桶
for (int i = 0; i < 10; i++) {
for (int j = 0; j < counter[i]; j++) {
arr[position++] = buckets[i][j];
}
}
}
源碼地址:https://github.com/zt19994/leetcode/tree/master/src/Learn/SortAlgorithm