Java 八大排序演算法總結
概述
排序有內部排序和外部排序,內部排序是資料記錄在記憶體中進行排序,而外部排序是因排序的資料很大,一次不能容納全部的排序記錄,在排序過程中需要訪問外存。我們這裡說說八大排序就是內部排序。
插入排序
思想:每步將一個待排序的記錄,按其順序碼大小插入到前面已經排序的子序列的合適位置,直到全部插入排序完為止。 關鍵問題:在前面已經排好序的序列中找到合適的插入位置。 方法:
- 直接插入排序
- 二分插入排序
- 希爾排序
直接插入排序
① 基本思想
每步將一個待排序的記錄,按其順序碼大小插入到前面已經排序的子序列的合適位置(從後向前找到合適位置後),直到全部插入排序完為止。
② 演算法實現
首先需要定義一個待排序的陣列:
int a[] = {3,1,5,7,2,4,9,6,10,8}; /** * 直接插入排序 * @param v */ public void insertSort(View v) { for (int i = 1; i < a.length; i++) { // 待插入元素 int temp = a[i]; int j; for (j = i - 1; j >= 0 && a[j] > temp; j--) { // 將大於temp的往後移動一位 a[j + 1] = a[j]; } a[j + 1] = temp; } }
- ③ 複雜度
如果碰見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。所以,相等元素的前後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,所以插入排序是穩定的。
- 時間複雜度:O(n^2)
- 空間複雜度:O(1)
二分插入排序
① 基本思想
二分法插入排序的思想和直接插入一樣,只是找合適的插入位置的方式不同,這裡是按二分法找到合適的位置,可以減少比較的次數。
② 演算法實現
/** * 二分插入排序 * @param v */ public void twoInsertSort(View v) { for (int i = 0; i < a.length; i++) { int temp = a[i]; int left = 0; int right = i - 1; int mid; while (left <= right) { mid = (left + right) / 2; if (temp < a[mid]) { right = mid - 1; } else { left = mid + 1; } } for (int j = i - 1; j >= left; j--) { a[j + 1] = a[j]; } if (left != i) { a[left] = temp; } } }
希爾排序
① 基本思想
先取一個小於n的整數d1作為第一個增量,把檔案的全部記錄分成d1個組。所有距離為d1的倍數的記錄放在同一個組中。先在各組內進行直接插入排序;然後,取第二個增量d2《d1重複上述的分組和排序,直至所取的增量dt=1(dt《dt-1《…《d2《d1),即所有記錄放在同一組中進行直接插入排序為止。該方法實質上是一種分組插入方法。
② 演算法實現
/**
* 希爾排序
* @param v
*/
public void shellSort(View v) {
int dk = a.length/2;
while(dk >= 1){
ShellInsertSort(a, dk);
dk = dk/2;
}
after.setText(getText(a));
}
private void ShellInsertSort(int[] a, int dk) {//類似插入排序,只是插入排序增量是1,這裡增量是dk,把1換成dk就可以了
for (int i = dk; i < a.length; i++) {
// 待插入元素
int temp = a[i];
int j;
for (j = i - dk; j >= 0 && a[j] > temp; j = j-dk) {
// 將大於temp的往後移動dk位
a[j + dk] = a[j];
}
a[j + dk] = temp;
}
}
③ 複雜度
希爾排序時效分析很難,關鍵碼的比較次數與記錄移動次數依賴於增量因子序列d的選取,特定情況下可以準確估算出關鍵碼的比較次數和記錄的移動次數。目前還沒有人給出選取最好的增量因子序列的方法。增量因子序列可以有各種取法,有取奇數的,也有取質數的,但需要注意:增量因子中除1 外沒有公因子,且最後一個增量因子必須為1。希爾排序方法是一個不穩定的排序方法。
- 時間複雜度:O(nlog2n)
- 空間複雜度:O(1)
選擇排序
思想:每趟從待排序的記錄序列中選擇關鍵字最小的記錄放置到已排序表的最前位置,直到全部排完。 關鍵問題:在剩餘的待排序記錄序列中找到最小關鍵碼記錄。 方法:
- 簡單選擇排序
- 二元選擇排序
- 堆排序
簡單選擇排序
① 基本思想
在要排序的一組數中,選出最小的一個數與第一個位置的數交換;然後在剩下的數當中再找最小的與第二個位置的數交換,如此迴圈到倒數第二個數和最後一個數比較為止。
② 演算法實現
/**
* 簡單選擇排序
* @param v
*/
public void selectSort(View v) {
int min;
for(int i = 0; i < a.length; i++){
min = i;
for(int j = i + 1; j < a.length; j++){//找到最小值下標
if(a[j] < a[min]){
min = j;
}
}
swap(a, i, min);
}
}
/**
* 元素交換
* @param
*/
public void swap(int[] data, int i, int j) {
if (i == j) {
return;
}
data[i] = data[i] + data[j];
data[j] = data[i] - data[j];
data[i] = data[i] - data[j];
}
③ 複雜度
- 時間複雜度:O(n2)
- 空間複雜度:O(1)
二元選擇排序
① 基本思想
簡單選擇排序,每趟迴圈只能確定一個元素排序後的定位。我們可以考慮改進為每趟迴圈確定兩個元素(當前趟最大和最小記錄)的位置,從而減少排序所需的迴圈次數。改進後對n個數據進行排序,最多隻需進行[n/2]趟迴圈即可。
② 演算法實現
/**
* 二元選擇排序
* @param v
*/
public void twoSelectSort(View v) {
int min, max;
for(int i = 0; i < a.length/2; i++){
min = i; max = i; //分別記錄最大和最小關鍵字記錄位置
for(int j = i + 1; j< a.length - i; j++){
if (a[j] > a[max]) {
max = j;
continue;
}
if (a[j] < a[min]) {
min = j;
}
}
swap(a, i, min); //最小值放到前面
if (i == max) {
max = min; //如果當前i就是max,第一次排序後max要調整為min(a[i]新的位置是a[min])
}
swap(a, a.length-1-i, max); //最大值放到後面
}
}
堆排序
① 基本思想
堆排序是一種樹形選擇排序,是對直接選擇排序的有效改進。 堆的定義下:具有n個元素的序列(k1,k2,…,kn),當且僅當滿足下面條件時稱之為堆。 在這裡只討論滿足後者條件的堆(大頂堆)。若以一維陣列儲存一個堆,則堆對應一棵完全二叉樹,且所有結點的值均大於等於其子女結點的值,根結點(堆頂元素)的值是最大的。
思想:初始時把要排序的數的序列看作是一棵順序儲存的二叉樹,調整它們的儲存序,使之成為一個堆,這時堆的根節點的數最大。然後將根節點與堆的最後一個節點交換。然後對前面(n-1)個數重新調整使之成為堆。依此類推,直到只有兩個節點的堆,並對它們作交換,最後得到有n個節點的有序序列。從演算法描述來看,堆排序需要兩個過程,一是建立堆,二是堆頂與堆的最後一個元素交換位置。所以堆排序有兩個函式組成。一是建堆的滲透函式,二是反覆呼叫滲透函式實現排序的函式。
初始序列:46,79,56,38,40,84 建堆: 交換,從堆中踢出最大數 依次類推:最後堆中剩餘的最後兩個結點交換,踢出一個,排序完成。
因此,實現堆排序需解決兩個問題:
- 如何將n 個待排序的數建成堆;
- 輸出堆頂元素後,怎樣調整剩餘n-1 個元素,使其成為一個新堆。
首先討論第二個問題:輸出堆頂元素後,對剩餘n-1元素重新建成堆的調整過程。 調整大頂堆的方法: 1)設有m 個元素的堆,輸出堆頂元素後,剩下m-1 個元素。將堆底元素送入堆頂(最後一個元素與堆頂進行交換),堆被破壞,其原因僅是根結點不滿足堆的性質。 2)將根結點與左、右子樹中較大元素的進行交換。 3)若與左子樹交換:如果左子樹堆被破壞,即左子樹的根結點不滿足堆的性質,則重複方法 (2). 4)若與右子樹交換,如果右子樹堆被破壞,即右子樹的根結點不滿足堆的性質。則重複方法 (2). 5)繼續對不滿足堆性質的子樹進行上述交換操作,直到葉子結點,堆被建成。 稱這個自根結點到葉子結點的調整過程為篩選。
再討論對n 個元素初始建堆的過程。 建堆方法:對初始序列建堆的過程,就是一個反覆進行篩選的過程。 1)n 個結點的完全二叉樹,則最後一個結點是第n/2個結點的子樹。 2)篩選從第n/2個結點為根的子樹開始,該子樹成為堆。 3)之後向前依次對各結點為根的子樹進行篩選,使之成為堆,直到根結點。
② 演算法實現
/**
* 堆排序
* @param v
*/
public void heapSort(View v) {
for (int i = 0; i < a.length; i++) {
createMaxHeap(a, a.length - 1 - i);
swap(a, 0, a.length - 1 - i);
}
}
public void createMaxHeap(int[] data, int lastIndex) {
// 從lastIndex處節點(最後一個節點)的父節點開始
for (int i = (lastIndex - 1) / 2; i >= 0; i--) {
// 儲存當前正在判斷的節點
int k = i;
// 如果當前k節點的子節點存在
while (2 * k + 1 <= lastIndex) {
// biggerIndex總是記錄較大節點的值,先賦值為當前節點的左子節點的索引
int biggerIndex = 2 * k + 1;
// 若當前節點的右子節點存在
if (biggerIndex + 1 <= lastIndex) {
if (data[biggerIndex] < data[biggerIndex + 1]) {
// 若右子節點值比左子節點值大,則biggerIndex記錄的是右子節點的索引
biggerIndex++;
}
}
// 如果k節點的值小於其較大的子節點的值
if (data[k] < data[biggerIndex]) {
// 交換兩者的值
swap(data, k, biggerIndex);
// 將biggerIndex賦予k,開始while迴圈的下一次迴圈,重新保證k節點的值大於其左右子節點的值
k = biggerIndex;
} else {
break;
}
}
}
}
③ 複雜度
設樹深度為k,。從根到葉的篩選,元素比較次數至多2(k-1)次,交換記錄至多k 次。所以,在建好堆後,排序過程中的篩選次數不超過下式:
而建堆時的比較次數不超過4n 次,因此堆排序最壞情況下,時間複雜度也為:O(nlogn )。
- 時間複雜度:O(nlogn )
- 空間複雜度:O(1)
交換排序
方法:
- 氣泡排序
- 快速排序
氣泡排序
① 基本思想
在要排序的一組數中,對當前還未排好序的範圍內的全部數,自上而下對相鄰的兩個數依次進行比較和調整,讓較大的數往下沉,較小的往上冒。即:每當兩相鄰的數比較後發現它們的排序與排序要求相反時,就將它們互換。
② 演算法實現
/**
* 氣泡排序
* @param v
*/
public void bubbleSort(View v) {
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < a.length - i - 1; j++) {
// 每遍歷一次都把最大的數沉到最底下去了
if (a[j] > a[j + 1]) {
swap(a, j, j + 1);
}
}
}
}
③ 複雜度
- 時間複雜度:O(n2)
- 空間複雜度:O(1)
改進的氣泡排序
① 基本思想
傳統氣泡排序中每一趟排序操作只能找到一個最大值或最小值,我們考慮利用在每趟排序中進行正向和反向兩遍冒泡的方法一次可以得到兩個最終值(最大者和最小者) , 從而使排序趟數幾乎減少了一半。
② 演算法實現
/**
* 氣泡排序改進
* @param v
*/
public void bubbleSort2(View v) {
int low = 0;
int high= a.length -1; //設定變數的初始值
int i;
while (low < high) {
for (i = low; i < high; i++) //正向冒泡,找到最大者
if (a[i]> a[i + 1]) {
swap(a, i, i + 1);
}
--high;//修改high值, 前移一位
for (i = high; i > low; i--) //反向冒泡,找到最小者
if (a[i]<a[i-1]) {
swap(a, i, i - 1);
}
++low;//修改low值,後移一位
}
}
快速排序
① 基本思想
選擇一個基準元素,通常選擇第一個元素或者最後一個元素,通過一趟掃描,將待排序列分成兩部分,一部分比基準元素小,一部分大於等於基準元素,此時基準元素在其排好序後的正確位置,然後再用同樣的方法遞迴地排序劃分的兩部分。
② 演算法實現
/**
* 快速排序
* @param
*/
public void quickSort(View v) {
quickSort(a, 0 , a.length - 1);
after.setText(getText(a));
}
private void quickSort(int[] a,int low, int high) {
if(low < high){ //如果不加這個判斷遞迴會無法退出導致堆疊溢位異常
int middle = getMiddle(a, low, high);
quickSort(a, 0, middle-1); //遞迴對低子表遞迴排序
quickSort(a, middle + 1, high); //遞迴對高子表遞迴排序
}
}
public int getMiddle(int[] a, int low, int high){
int key = a[low];//基準元素,排序中會空出來一個位置
while(low < high){
while(low < high && a[high] >= key){//從high開始找比基準小的,與low換位置
high--;
}
a[low]=a[high];
while(low < high && a[low] <= key){//從low開始找比基準大,放到之前high空出來的位置上
low++;
}
a[high] = a[low];
}
a[low] = key;//此時low=high 是基準元素的位置,也是空出來的那個位置
return low;
}
③ 複雜度
快速排序是通常被認為在同數量級(O(nlog2n))的排序方法中平均效能最好的。但若初始序列按關鍵碼有序或基本有序時,快速排序反而蛻化為氣泡排序。為改進之,通常以“三者取中法”來選取基準記錄,即將排序區間的兩個端點與中點三個記錄關鍵碼居中的調整為支點記錄。快速排序是一個不穩定的排序方法。
- 時間複雜度:O(nlog2n)
- 空間複雜度:O(nlog2n)
改進的快速排序
① 基本思想
在本改進演算法中,只對長度大於k的子序列遞迴呼叫快速排序,讓原序列基本有序,然後再對整個基本有序序列用插入排序演算法排序。實踐證明,改進後的演算法時間複雜度有所降低,且當k取值為 8 左右時,改進演算法的效能最佳。
② 演算法實現
/**
* 快速排序改進
* @param
*/
public void quickSort2(View v) {
quickSort2(a, 0, a.length - 1, 8);//先呼叫改進演算法Qsort使之基本有序,k=8
//再用插入排序對基本有序序列排序
for(int i = 1; i < a.length; i++){
int temp = a[i];
int j;
for (j = i - 1; j >= 0 && a[j] > temp; j--) {
// 將大於temp的往後移動一位
a[j + 1] = a[j];
}
a[j + 1] = temp;
}
}
private void quickSort2(int[] a,int low, int high, int k) {
if(high -low > k) { //長度大於k時遞迴, k為指定的數
int pivot = partition(a, low, high); // 呼叫的Partition演算法保持不變
quickSort2(a, low, pivot - 1, k);
quickSort2(a, pivot + 1, high, k);
}
}
private int partition(int a[], int low, int high) {
int privotKey = a[low]; //基準元素
while(low < high){ //從表的兩端交替地向中間掃描
while(low < high && a[high] >= privotKey) { //從high 所指位置向前搜尋,至多到low+1 位置。將比基準元素小的交換到低端
high--;
}
swap(a, low, high);
while(low < high && a[low] <= privotKey ) {
low++;
}
swap(a, low, high);
}
return low;
}
歸併排序
① 基本思想
歸併(Merge)排序法是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分為若干個子序列,每個子序列是有序的。然後再把有序子序列合併為整體有序序列。
② 演算法實現
/**
* 歸併排序
* @param
*/
public void mergeSort(View v) {
mergeSort(a, 0 , a.length - 1);
}
public void mergeSort(int[] a, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
// 左邊
mergeSort(a, low, mid);
// 右邊
mergeSort(a, mid + 1, high);
// 左右歸併
merge(a, low, mid, high);
}
}
public void merge(int[] a, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;// 左指標
int j = mid + 1;// 右指標
int k = 0;
// 把較小的數先移到新陣列中
while (i <= mid && j <= high) {
if (a[i] < a[j]) {
temp[k++] = a[i++];
} else {
temp[k++] = a[j++];
}
}
// 把左邊剩餘的數移入陣列
while (i <= mid) {
temp[k++] = a[i++];
}
// 把右邊邊剩餘的數移入陣列
while (j <= high) {
temp[k++] = a[j++];
}
// 把新陣列中的數覆蓋a陣列
for (int k2 = 0; k2 < temp.length; k2++) {
a[low + k2] = temp[k2];
}
}
③ 複雜度
- 時間複雜度:O(nlog2n)
- 空間複雜度:O(n)
桶排序/基數排序
① 基本思想
將所有待比較數值(正整數)統一為同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列。
② 演算法實現
/**
* 桶排序/基數排序
* @param
*/
public void radixSort(View v) {
// 找到最大數,確定要排序幾趟
int max = 0;
for (int i = 0; i < a.length; i++) {
if (max < a[i]) {
max = a[i];
}
}
// 判斷位數
int times = 0;
while (max > 0) {
max = max / 10;
times++;
}
// 建立十個佇列
List<ArrayList> queue = new ArrayList<>();
for (int i = 0; i < 10; i++) {
ArrayList queue1 = new ArrayList();
queue.add(queue1);
}
// 進行times次分配和收集
for (int i = 0; i < times; i++) {
// 分配
for (int j = 0; j < a.length; j++) {
int x = a[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
ArrayList queue2 = queue.get(x);
queue2.add(a[j]);
queue.set(x, queue2);
}
// 收集
int count = 0;
for (int j = 0; j < 10; j++) {
while (queue.get(j).size() > 0) {
ArrayList<Integer> queue3 = queue.get(j);
a[count] = queue3.get(0);
queue3.remove(0);
count++;
}
}
}
}
③ 複雜度
- 時間複雜度:O(d(n+r))
- 空間複雜度:O(n+r)
複雜度總結
時間複雜度:
(1)平方階(O(n2))排序
- 直接插入
- 直接選擇
- 氣泡排序
(2)線性對數階(O(nlog2n))排序
- 希爾排序
- 堆排序
- 快速排序
- 歸併排序
(3)線性階(O(n))排序
- 基數排序
說明: 當原表有序或基本有序時,直接插入排序和氣泡排序將大大減少比較次數和移動記錄的次數,時間複雜度可降至O(n); 而快速排序則相反,當原表基本有序時,將蛻化為氣泡排序,時間複雜度提高為O(n2); 原表是否有序,對簡單選擇排序、堆排序、歸併排序和基數排序的時間複雜度影響不大。
穩定性:
排序演算法的穩定性:若待排序的序列中,存在多個具有相同關鍵字的記錄,經過排序, 這些記錄的相對次序保持不變,則稱該演算法是穩定的;若經排序後,記錄的相對 次序發生了改變,則稱該演算法是不穩定的。
穩定性的好處:排序演算法如果是穩定的,那麼從一個鍵上排序,然後再從另一個鍵上排序,第一個鍵排序的結果可以為第二個鍵排序所用。基數排序就是這樣,先按低位排序,逐次按高位排序,低位相同的元素其順序再高位也相同時是不會改變的。另外,如果排序演算法穩定,可以避免多餘的比較。
(1)穩定的排序演算法
- 直接插入排序
- 氣泡排序
- 歸併排序
- 基數排序
(2)不是穩定的排序演算法
- 希爾排序
- 簡單選擇排序
- 堆排序
- 快速排序
排序演算法選擇:
1)當n較大,則應採用時間複雜度為O(nlog2n)的排序方法:快速排序、堆排序或歸併排序。
- 快速排序:是目前基於比較的內部排序中被認為是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短;
- 歸併排序:它有一定數量的資料移動,所以我們可能過與插入排序組合,先獲得一定長度的序列,然後再合併,在效率上將有所提高。
2) 當n較大,記憶體空間允許,且要求穩定性 =》歸併排序 3) 當n較小,可採用直接插入或直接選擇排序。
- 直接插入排序:當元素分佈有序,直接插入排序將大大減少比較次數和移動記錄的次數。
- 直接選擇排序 :元素分佈有序,如果不要求穩定性,選擇直接選擇排序
5)一般不使用或不直接使用傳統的氣泡排序。 6)基數排序 它是一種穩定的排序演算法,但有一定的侷限性:
- 關鍵字可分解
- 記錄的關鍵字位數較少,如果密集更好
-
如果是數字時,最好是無符號的,否則將增加相應的映射覆雜度,可先將其正負分開排序
時間複雜度來說:
(1)平方階(O(n2))排序 各類簡單排序:直接插入、直接選擇和氣泡排序; (2)線性對數階(O(nlog2n))排序 快速排序、堆排序和歸併排序; (3)O(n1+§))排序,§是介於0和1之間的常數。
希爾排序 (4)線性階(O(n))排序 基數排序,此外還有桶、箱排序。
說明:
當原表有序或基本有序時,直接插入排序和氣泡排序將大大減少比較次數和移動記錄的次數,時間複雜度可降至O(n);
而快速排序則相反,當原表基本有序時,將蛻化為氣泡排序,時間複雜度提高為O(n2);
原表是否有序,對簡單選擇排序、堆排序、歸併排序和基數排序的時間複雜度影響不大。
穩定性:
排序演算法的穩定性:若待排序的序列中,存在多個具有相同關鍵字的記錄,經過排序, 這些記錄的相對次序保持不變,則稱該演算法是穩定的;若經排序後,記錄的相對 次序發生了改變,則稱該演算法是不穩定的。 穩定性的好處:排序演算法如果是穩定的,那麼從一個鍵上排序,然後再從另一個鍵上排序,第一個鍵排序的結果可以為第二個鍵排序所用。基數排序就是這樣,先按低位排序,逐次按高位排序,低位相同的元素其順序再高位也相同時是不會改變的。另外,如果排序演算法穩定,可以避免多餘的比較;
穩定的排序演算法:氣泡排序、插入排序、歸併排序和基數排序
不是穩定的排序演算法:選擇排序、快速排序、希爾排序、堆排序
選擇排序演算法準則:
每種排序演算法都各有優缺點。因此,在實用時需根據不同情況適當選用,甚至可以將多種方法結合起來使用。
選擇排序演算法的依據
影響排序的因素有很多,平均時間複雜度低的演算法並不一定就是最優的。相反,有時平均時間複雜度高的演算法可能更適合某些特殊情況。同時,選擇演算法時還得考慮它的可讀性,以利於軟體的維護。一般而言,需要考慮的因素有以下四點:
1.待排序的記錄數目n的大小;
2.記錄本身資料量的大小,也就是記錄中除關鍵字外的其他資訊量的大小;
3.關鍵字的結構及其分佈情況;
4.對排序穩定性的要求。
設待排序元素的個數為n.
1)當n較大,則應採用時間複雜度為O(nlog2n)的排序方法:快速排序、堆排序或歸併排序序。
快速排序:是目前基於比較的內部排序中被認為是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短; 堆排序 : 如果記憶體空間允許且要求穩定性的,
歸併排序:它有一定數量的資料移動,所以我們可能過與插入排序組合,先獲得一定長度的序列,然後再合併,在效率上將有所提高。
2) 當n較大,記憶體空間允許,且要求穩定性 =》歸併排序
3)當n較小,可採用直接插入或直接選擇排序。
直接插入排序:當元素分佈有序,直接插入排序將大大減少比較次數和移動記錄的次數。
直接選擇排序 :元素分佈有序,如果不要求穩定性,選擇直接選擇排序
5)一般不使用或不直接使用傳統的氣泡排序。
6)基數排序 它是一種穩定的排序演算法,但有一定的侷限性: 1、關鍵字可分解。 2、記錄的關鍵字位數較少,如果密集更好
3、如果是數字時,最好是無符號的,否則將增加相應的映射覆雜度,可先將其正負分開排序。