所謂排序,就是根據排序碼的遞增或者遞減順序把數據元素依次排列起來,使一組任意排列的元素變爲一組按其排序碼線性有序的元素。本文將介紹八種最爲經典經常使用的內部排序算法的基本思想與實現,包括插入排序(直接插入排序,希爾排序)、選擇排序(直接選擇排序,堆排序)、交換排序(冒泡排序,快速排序)、歸併排序、分配排序(基數排序),並給出各類算法的時間複雜度、空間複雜度和穩定性。git
喜歡的朋友記得點點贊和關注,支持下哦算法
本文將介紹八種最爲經典經常使用的內部排序算法,包括插入排序(直接插入排序,希爾排序)、選擇排序(直接選擇排序,堆排序)、交換排序(冒泡排序,快速排序)、歸併排序、分配排序(基數排序)。實際上,不管是基本排序方法(直接插入排序,直接選擇排序,冒泡排序),仍是高效排序方法(快速排序,堆排序,歸併排序)等,它們各有所長,都擁有特定的使用場景。所以,在實際應用中,咱們必須根據實際任務的特色和各類排序算法的特性來作出最合適的選擇。通常地,咱們衡量一個算法的指標包括:shell
筆者將在本文着重探討上述八種排序算法的思想和實現,並就各算法根據以上指標進行分析和歸類,以便進一步熟悉它們各自的應用場景。數組
插入排序的基本思想:每步將一個待排序元素,按其排序碼大小插入到前面已經排好序的一組元素中,直到元素所有插入爲止。在這裏,咱們介紹三種具體的插入排序算法:直接插入排序,希爾排序與折半插入排序。bash
一、直接插入排序優化
直接插入排序的思想:當插入第i(i>=1)個元素時,前面的V[0],…,V[i-1]等i-1個 元素已經有序。這時,將第i個元素與前i-1個元素V[i-1],…,V[0]依次比較,找到插入位置即將V[i]插入,同時原來位置上的元素向後順移。在這裏,插入位置的查找是順序查找。ui
直接插入排序是一種穩定的排序算法,其實現以下:spa
/**
*
* Title: 插入排序中的直接插入排序,依賴於初始序列
*
* Description: 在有序序列中不斷插入新的記錄以達到擴大有序區到整個數組的目的
*
* 時間複雜度:最好情形O(n),平均情形O(n^2),最差情形O(n^2)
*
* 空間複雜度:O(1)
*
* 穩 定 性:穩定
*
* 內部排序(在排序過程當中數據元素徹底在內存)
*
*/
public class StraightInsertionSort {
public static int[] insertSort( int[] target )
{
if ( target != null && target.length != 1 ) /* 待排序數組不爲空且長度大於1 */
{
for ( int i = 1; i < target.length; i++ ) /* 不斷擴大有序序列,直到擴展到整個數組 */
{
for ( int j = i; j > 0; j-- ) /* 向有序序列中插入新的元素 */
{
if ( target[j] < target[j - 1] ) /* 交換 */
{
int temp = target[j];
target[j] = target[j - 1];
target[j - 1] = temp;
}
}
}
}
return(target);
}
}
複製代碼
二、希爾排序指針
希爾排序的思想:設待排序序列共n個元素,首先取一個整數gap<n做爲間隔,將所有元素分爲間隔爲gap的gap個子序列並對每個子序列進行直接插入排序。而後,縮小間隔gap,重複上述操做,直至gap縮小爲1,此時全部元素位於同一個序列且有序。因爲剛開始時,gap較大,每一個子序列元素較少,排序速度較快;待到排序後期,gap變小,每一個子序列元素較多,但大部分元素基本有序,因此排序速度仍較快。通常地,gap取 (gap/3 + 1)。code
希爾排序是一種不穩定的排序方法,其實現以下:
/**
*
* Title: 插入排序中的希爾排序,依賴於初始序列
*
* Description: 分別對間隔爲gap的gap個子序列進行直接插入排序,不斷縮小gap,直至爲 1
* 剛開始時,gap較大,每一個子序列元素較少,排序速度較快;
*
* 待到排序後期,gap變小,每一個子序列元素較多,但大部分元素基本有序,因此排序速度仍較快。
*
* 時間複雜度:O(n) ~ O(n^2)
*
* 空間複雜度:O(1)
*
* 穩 定 性:不穩定
*
* 內部排序(在排序過程當中數據元素徹底在內存)
*/
public class ShellSort {
public static void shellSort( int[] target )
{
if ( target != null && target.length != 1 )
{
int gap = target.length;
while ( gap > 1 ) /* gap爲int型,自動取整 */
{
gap = gap / 3 + 1;
for ( int i = gap; i < target.length; i++ )
{
int j = i - gap;
while ( j >= 0 )
{
if ( target[j + gap] < target[j] )
{
swap( target, j, j + gap );
j -= gap;
}else{
break;
}
}
}
}
}
}
public static void swap( int[] target, int i, int j )
{
int temp = target[i];
target[i] = target[j];
target[j] = temp;
}
}
複製代碼
三、折半插入排序
折半插入排序的思想:當插入第i(i>=1)個元素時,前面的V[0],…,V[i-1]等i-1個 元素已經有序。這時,折半搜索第i個元素在前i-1個元素V[i-1],…,V[0]中的插入位置,而後直接將V[i]插入,同時原來位置上的元素向後順移。與直接插入排序不一樣的是,折半插入排序比直接插入排序明顯減小了關鍵字之間的比較次數,可是移動次數是沒有改變。因此,折半插入排序和插入排序的時間複雜度相同都是O(N^2),但其減小了比較次數,因此該算法仍然比直接插入排序好。折半插入排序是一種穩定的排序算法,其實現以下:
/**
*
* Title: 插入排序中的折半插入排序,依賴於初始序列
*
* Description: 折半搜索出插入位置,並直接插入;與直接插入搜索的區別是,後者的搜索要快於順序搜索
*
* 時間複雜度:折半插入排序比直接插入排序明顯減小了關鍵字之間的比較次數,可是移動次數是沒有改變。因此,
*
* 折半插入排序和插入排序的時間複雜度相同都是O(N^2),在減小了比較次數方面它確實至關優秀,因此該算法仍然比直接插入排序好。
*
* 空間複雜度:O(1)
*
* 穩 定 性:穩定
*
* 內部排序(在排序過程當中數據元素徹底在內存)
*
*/
public class BinaryInsertSort {
public static int[] binaryInsertSort( int[] target )
{
if ( target != null && target.length > 1 )
{
for ( int i = 1; i < target.length; i++ )
{
int left = 0;
int right = i - 1;
int mid;
int temp = target[i];
if ( temp < target[right] ) /* 當前值小於有序序列的最大值時,開始查找插入位置 */
{
while ( left <= right )
{
mid = (left + right) / 2;
if ( target[mid] < temp )
{
left = mid + 1; /* 縮小插入區間 */
}else if ( target[mid] > temp )
{
right = mid - 1; /* 縮小插入區間 */
}else{ /* 待插入值與有序序列中的target[mid]相等,保證穩定性的處理 */
left = left + 1;
}
}
/* left及其後面的數據順序向後移動,並在left位置插入 */
for ( int j = i; j > left; j-- )
{
target[j] = target[j - 1];
}
target[left] = temp;
}
}
}
return(target);
}
}
複製代碼
選擇排序的基本思想:每一趟 (例如第i趟,i = 0,1,…)在後面第n-i個待排序元素中選出最小元素做爲有序序列的第i個元素,直到第n-1趟結束後,全部元素有序。在這裏,咱們介紹兩種具體的選擇排序算法:直接選擇排序與堆排序。
一、直接選擇排序
直接選擇排序的思想:
第一次從R[0]~R[n-1]中選取最小值,與R[0]交換,第二次從R1~R[n-1]中選取最小值,與R1交換,….,第i次從R[i-1]~R[n-1]中選取最小值,與R[i-1]交換,…..,第n-1次從R[n-2]~R[n-1]中選取最小值,與R[n-2]交換,總共經過n-1次,獲得一個按排序碼從小到大排列的有序序列
直接選擇排序是一種不穩定的排序算法,其實現以下:
/**
*
* Title: 選擇排序中的直接選擇排序,依賴於初始序列
*
* Description: 每一趟 (例如第i趟,i = 0,1,...)在後面第n-i個待排序元素中選出最小元素做爲有序序列的第i個元素
*
* 時間複雜度:最好情形O(n^2),平均情形O(n^2),最差情形O(n^2)
*
* 空間複雜度:O(1)
*
* 穩 定 性:不穩定
*
* 內部排序(在排序過程當中數據元素徹底在內存)
*
*/
public class StraightSelectSort {
public static int[] selectSort( int[] target )
{
if ( target != null && target.length != 1 )
{
for ( int i = 0; i < target.length; i++ )
{
int min_index = i;
for ( int j = i + 1; j < target.length; j++ )
{
if ( target[min_index] > target[j] )
{
min_index = j;
}
}
if ( target[min_index] != target[i] ) /* 致使不穩定的因素:交換 */
{
int min = target[min_index];
target[min_index] = target[i];
target[i] = min;
}
}
}
return(target);
}
}
複製代碼
二、堆排序
堆排序的核心是堆調整算法。首先根據初始輸入數據,利用堆調整算法shiftDown()造成初始堆;而後,將堆頂元素與堆尾元素交換,縮小堆的範圍並從新調整爲堆,如此往復。
堆排序是一種不穩定的排序算法,其實現以下:
/**
*
* Title: 堆排序(選擇排序),升序排序(最大堆),依賴於初始序列
*
* Description: 現將給定序列調整爲最大堆,而後每次將堆頂元素與堆尾元素交換並縮小堆的範圍,直到將堆縮小至1
*
* 時間複雜度:O(nlgn)
*
* 空間複雜度:O(1)
*
* 穩 定 性:不穩定
*
* 內部排序(在排序過程當中數據元素徹底在內存)
*
*/
public class HeapSort {
public static int[] heapSort( int[] target )
{
if ( target != null && target.length > 1 )
{
/* 調整爲最大堆 */
int pos = (target.length - 2) / 2;
while ( pos >= 0 )
{
shiftDown( target, pos, target.length - 1 );
pos--;
}
/* 堆排序 */
for ( int i = target.length - 1; i > 0; i-- )
{
int temp = target[i];
target[i] = target[0];
target[0] = temp;
shiftDown( target, 0, i - 1 );
}
return(target);
}
return(target);
}
/**
*
* @description 自上而下調整爲最大堆
*
* @param target
*
* @param start
*
* @param end
*
*/
private static void shiftDown( int[] target, int start, int end )
{
int i = start;
int j = 2 * start + 1;
int temp = target[i];
while ( j <= end ) /* 迭代條件 */
{
if ( j < end && target[j + 1] > target[j] ) /* 找出較大子女 */
{
j = j + 1;
}
if ( target[j] <= temp ) /* 父親大於子女 */
{
break;
} else {
target[i] = target[j];
i = j;
j = 2 * j + 1;
}
}
target[i] = temp;
}
}
複製代碼
交換排序的基本思想:根據序列中兩個元素的比較結果來對換這兩個記錄在序列中的位置,也就是說,將鍵值較大的記錄向序列的尾部移動,鍵值較小的記錄向序列的前部移動。
一、冒泡排序
冒泡排序的思想:根據序列中兩個元素的比較結果來對換這兩個記錄在序列中的位置,將鍵值較大的記錄向序列的尾部移動,鍵值較小的記錄向序列的前部移動。所以,每一趟都將較小的元素移到前面,較大的元素天然就逐漸沉到最後面了,也就是說,最大的元素最後才能肯定,這就是冒泡。冒泡排序是一種穩定的排序算法,其實現以下:
/**
*
* Title: 交換排序中的冒泡排序 ,通常情形下指的是優化後的冒泡排序,最多進行n-1次比較,依賴於初始序列
*
* Description:由於越大的元素會經由交換慢慢"浮"到數列的頂端(最後位置),最大的數最後才肯定下來,因此稱爲冒泡排序
*
* 時間複雜度:最好情形O(n),平均情形O(n^2),最差情形O(n^2)
*
* 空間複雜度:O(1)
*
* 穩 定 性:穩定
*
* 內部排序(在排序過程當中數據元素徹底在內存)
*
*/
public class BubbleSort {
/**
*
* @description 樸素冒泡排序(共進行n-1次比較)
*
* @author rico
*
*/
public static int[] bubbleSort( int[] target )
{
int n = target.length;
if ( target != null && n != 1 )
{
/* 最多須要進行n-1躺,每一趟將比較小的元素移到前面,比較大的元素天然就逐漸沉到最後面了,這就是冒泡 */
for ( int i = 0; i < n - 1; i++ )
{
for ( int j = n - 1; j > i; j-- )
{
if ( target[j] < target[j - 1] )
{
int temp = target[j];
target[j] = target[j - 1];
target[j - 1] = temp;
}
}
System.out.println( Arrays.toString( target ) );
}
}
return(target);
}
/**
*
* @description 優化冒泡排序
*/
public static int[] optimizeBubbleSort( int[] target )
{
int n = target.length;
if ( target != null && n != 1 )
{
/* 最多須要進行n-1躺,每一趟將比較小的元素移到前面,比較大的元素天然就逐漸沉到最後面了,這就是冒泡 */
for ( int i = 0; i < n - 1; i++ )
{
boolean exchange = false;
for ( int j = n - 1; j > i; j-- )
{
if ( target[j] < target[j - 1] )
{
int temp = target[j];
target[j] = target[j - 1];
target[j - 1] = temp;
exchange = true;
}
}
System.out.println( Arrays.toString( target ) );
if ( !exchange ) /* 若 i 到 n-1 這部分元素已經有序,則直接返回 */
{
return(target);
}
}
}
return(target);
}
}
複製代碼
二、快速排序
快速排序的思想:
經過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的全部數據都比另一部分的全部數據都要小(劃分過程),而後再按此方法對這兩部分數據分別進行快速排序(快速排序過程),整個排序過程能夠遞歸進行,以此達到整個數據變成有序序列。
快速排序是一種不穩定的排序算法。
/**
*
* Title: 交換排序中的快速排序,目前應用最爲普遍的排序算法,是一個遞歸算法,依賴於初始序列
*
* Description:快速排序包括兩個過程:劃分 和 快排
*
* "劃分"是指將原序列按基準元素劃分兩個子序列
*
* "快排"是指分別對子序列進行快排
*
* 就平均計算時間而言,快速排序是全部內部排序方法中最好的一個
*
* 對大規模數據排序時,快排是快的;對小規模數據排序時,快排是慢的,甚至慢於簡單選擇排序等簡單排序方法
*
* 快速排序依賴於原始序列,所以其時間複雜度從O(nlgn)到O(n^2)不等
*
* 時間複雜度:最好情形O(nlgn),平均情形O(nlgn),最差情形O(n^2)
*
* 遞歸所消耗的棧空間
*
* 空間複雜度:O(lgn)
*
* 可選任一元素做爲基準元素
*
* 穩 定 性:不穩定
*
* 內部排序(在排序過程當中數據元素徹底在內存)
*
*/
public class QuickSort {
/**
*
* @description 快排算法(遞歸算法):在遞去過程當中就把問題解決了
*
* @param target
*
* @param left
*
* @param right
*
* @return
*
*/
public static int[] quickSort( int[] target, int left, int right )
{
if ( right > left ) /* 遞歸終止條件 */
{
int base_index = partition( target, left, right ); /* 原序列劃分後基準元素的位置 */
quickSort( target, left, base_index - 1 ); /* 對第一個子序列快速排序,不包含基準元素! */
quickSort( target, base_index + 1, right ); /* 對第二個子序列快速排序,不包含基準元素! */
return(target);
}
return(target);
}
/**
*
* @description 序列劃分,以第一個元素爲基準元素
*
* @param target 序列
*
* @param left 序列左端
*
* @param right 序列右端
*
* @return
*
*/
public static int partition( int[] target, int left, int right )
{
int base = target[left]; /* 基準元素的值 */
int base_index = left; /* 基準元素最終應該在的位置 */
for ( int i = left + 1; i <= right; i++ ) /* 從基準元素的下一個元素開始 */
{
if ( target[i] < base ) /* 若其小於基準元素 */
{
base_index++; /* 若其小於基準元素,則基準元素最終位置後移;不然不用移動 */
if ( base_index != i ) /* 相等狀況意味着i以前的元素都小於base,只須要換一次便可,不須要次次都換 */
{
int temp = target[base_index];
target[base_index] = target[i];
target[i] = temp;
}
}
}
/* 將基準元素就位 */
target[left] = target[base_index];
target[base_index] = base;
System.out.println( Arrays.toString( target ) );
return(base_index); /* 返回劃分後基準元素的位置 */
}
}
複製代碼
歸併排序包含兩個過程:」歸」和」並」。
歸併排序算法是一個典型的遞歸算法,所以也是概念上最爲簡單的排序算法與快速排序算法相比,歸併排序算法不依賴於初始序列,而且是一種穩定的排序算法,但須要與原序列同樣大小的輔助存儲空間。
/**
*
* Title: 歸併排序 ,概念上最爲簡單的排序算法,是一個遞歸算法 Description:歸併排序包括兩個過程:歸 和 並
*
* "歸"是指將原序列分紅半子序列,分別對子序列進行遞歸排序 "並"是指將排好序的各子序列合併成原序列
*
* 歸併排序的主要問題是:須要一個與原待排序數組同樣大的輔助數組空間
*
* 歸併排序不依賴於原始序列,所以其最好情形、平均情形和最差情形時間複雜度都同樣 時間複雜度:最好情形O(n),平均情形O(n^2),最差情形O(n^2)
*
* 空間複雜度:O(n) 穩 定 性:穩定 內部排序(在排序過程當中數據元素徹底在內存)
*
*/
public class MergeSort {
/**
*
* @description 歸併排序算法(遞歸算法):遞去分解,歸來合併
*
* @param target
*
* 待排序序列
*
* @param left
*
* 待排序序列起始位置
*
* @param right
*
* 待排序序列終止位置
*
* @return
*
*/
public static void mergeSort( int[] target )
{
int[] copy = Arrays.copyOf( target, target.length ); /* 空間複雜度O(n) */
mergeSort( target, copy, 0, target.length - 1 );
}
public static void mergeSort( int[] target, int[] copy, int left, int right )
{
if ( right > left ) /* 遞歸終止條件 */
{
int mid = (left + right) / 2;
mergeSort( target, copy, left, mid ); /* 歸併排序第一個子序列 */
mergeSort( target, copy, mid + 1, right ); /* 歸併排序第二個子序列 */
merge( target, copy, left, mid, right ); /* 合併子序列成原序列 */
}
}
/**
*
* @description 兩路歸併算法
*
* @param target
*
* 用於存儲歸併結果
*
* @param left
*
* 第一個有序表的第一個元素所在位置
*
* @param mid
*
* 第一個有序表的最後一個元素所在位置
*
* @param right
*
* 第二個有序表的最後一個元素所在位置
*
* @return
*
*/
public static void merge( int[] target, int[] copy, int left, int mid,
int right )
{
/* s1,s2是檢查指針,index 是存放指針 */
int s1 = left;
int s2 = mid + 1;
int index = left;
/* 兩個表都未檢查完,兩兩比較 */
while ( s1 <= mid && s2 <= right )
{
if ( copy[s1] <= copy[s2] ) /* 穩定性 */
{
target[index++] = copy[s1++];
} else {
target[index++] = copy[s2++];
}
}
/* 若第一個表未檢查完,複製 */
while ( s1 <= mid )
{
target[index++] = copy[s1++];
}
/* 若第二個表未檢查完,複製 */
while ( s2 <= right )
{
target[index++] = copy[s2++];
}
/* 更新輔助數組 copy */
for ( int i = left; i <= right; i++ )
{
copy[i] = target[i];
}
}
複製代碼
分配排序的基本思想:用空間換時間。在整個排序過程當中,無須比較關鍵字,而是經過用額外的空間來」分配」和」收集」來實現排序,它們的時間複雜度可達到線性階:O(n)。
基數排序包括兩個過程:
首先,將目標序列各元素分配到各個桶中(分配過程); 而後,將各個桶中的元素按先進先出的順序再放回去(收集過程),如此往復,一共須要進行d趟,d爲元素的位數。
/**
*
* Title: 分配排序中的基數排序,不依賴於初始序列
*
* Description: 不是在對元素進行比較的基礎上進行排序,而是採用 "分配 + 收集" 的辦法
*
*
*
* 首先,將目標序列各元素分配到各個桶中;
*
* 其次,將各個桶中的元素按先進先出的順序再放回去
*
* 如此往復...
*
*
*
* 時間複雜度:O(d*(r+n))或者 O(dn),d 的大小通常會受到 n的影響
*
* 空間複雜度:O(rd + n)或者 O(n)
*
* 穩 定 性:穩定
*
* 內部排序(在排序過程當中數據元素徹底在內存)
*
*/
public class RadixSort {
/**
*
* @description 分配 + 收集
*
* @param target 待排序數組
*
* @param r 基數
*
* @param d 元素的位數
*
* @param n 待排序元素個數
*
* @return
*
*/
public static int[] radixSort( int[] target, int r, int d, int n )
{
if ( target != null && target.length != 1 )
{
int[][] bucket = new int[r][n]; /* 一共有基數r個桶,每一個桶最多放n個元素 */
int digit; /* 獲取元素對應位上的數字,即裝入那個桶 */
int divisor = 1; /* 定義每一輪的除數,1, 10, 100, ... */
int[] count = new int[r]; /* 統計每一個桶中實際存放元素的個數 */
for ( int i = 0; i < d; i++ ) /* d 位的元素,須要通過分配、收集d次便可完成排序 */
{ /* 分配 */
for ( int ele : target )
{
digit = (ele / divisor) % 10; /* 獲取元素對應位上的數字(巧妙!!!) */
bucket[digit][count[digit]++] = ele; /* 將元素放入對應桶,桶中元素數目加1 */
}
/* 收集 */
int index = 0; /* 目標數組的下標 */
for ( int j = 0; j < r; j++ )
{
int k = 0; /* 用於按照先進先出順序獲取桶中元素 */
while ( k < count[j] )
{
target[index++] = bucket[j][k++]; /* 按照先進先出依次取出桶中的元素 */
}
count[j] = 0; /* 計數器歸零 */
}
divisor *= 10; /* 用於獲取元素對應位數字 */
}
}
return(target);
}
}
複製代碼