常用的內部排序

1 排序概述

       一旦將一個雜亂無章的記錄重排成一組有序記錄,就能夠快速地從這組記錄中找到目標記錄。因此通常來說,排序的目的就是快速查找。
       對於一個排序算法來說,一般從如下三個方面來衡量算法的優劣。
  1. 時間複雜度:主要是分析關鍵字的比較次數和記錄的移動次數。
  2. 空間複雜度:分析排序算法中需要多少輔助內存。
  3. 穩定性:若兩個記錄A和B的關鍵字值相等,但排序後A、B的先後次序保持不變,則成這種排序算法是穩定的;反之,就是不穩定的。
       如果整個排序過程不需要藉助於外部存儲器(如磁盤等),所有排序操作都在內存中完成,這種排序就稱爲內部排序。如果參與排序的數據元素非常多,數據量非常大,計算機無法把整個排序過程放在內存中完成,必須藉助於外部存儲器(如磁盤等),這種排序就被稱爲外部排序

2 選擇排序法

       常用的選擇排序方法有兩種:直接選擇排序和堆排序。直接選擇排序簡單直觀,但性能略差;堆排序是一種較爲高效的選擇排序方法,但實現起來略微複雜。

2、1 直接選擇排序

       直接選擇排序的思路很簡單,它需要經過n-1趟排序。
       第1趟比較: 程序將記錄定位在第1個數據上,拿第1個數據依次和它後面的每個數據進行比較,如果第1個數據大於後面某個數據,就交換它們······以此類推。經過第1趟比較,這樣數據中最小的數據將被選出,它被排在第1位。
       ······
       按照此規則一共進行n-1趟比較,這組數據中第n-1小(第2大)的數據被選出,被排在第n-1位,剩下的就是最大的數據排在最後。
       直接選擇排序的優點是算法簡單,容易實現。
       直接選擇排序的缺點是每趟排序只能確定一個元素,n個數據需要進行n-1趟排序。
import java.util.Arrays;


class DataWrap implements Comparable<DataWrap> {
	int data;
	String flag;
	
	public DataWrap(int data , String flag) {
		this.data = data;
		this.flag = flag;
	}
	
	public String toString() {
		return data + flag;
	}

	@Override
	public int compareTo(DataWrap dw) {
		return this.data > dw.data ? 1 : (this.data == dw.data ? 0 : -1);
	}
}


public class SelectSort {
	public static void selectSort(DataWrap[] data) {
		System.out.println("開始排序");
		int len = data.length;
		for ( int i = 0 ; i < len - 1 ; i ++ ) {
			for ( int j = i ; j < len ; j ++ ) {
				if ( data[i].compareTo(data[j]) > 0 ) {
					DataWrap tmp = data[i];
					data[i] = data[j];
					data[j] = tmp;
				}
			}
			System.out.println("第" + (i + 1) + "趟排序:" + Arrays.toString(data));
		}
	}

	public static void main(String[] args) {
		DataWrap[] data = {
			new DataWrap(21, ""),
			new DataWrap(30, ""),
			new DataWrap(49, ""),
			new DataWrap(30, "*"),
			new DataWrap(16, ""),
			new DataWrap(9, ""),
		};
		System.out.println("排序之前:" + Arrays.toString(data));
		selectSort(data);
		System.out.println("排序之後:" + Arrays.toString(data));
	}
}
輸出結果爲:
排序之前:[21, 30, 49, 30*, 16, 9]
開始排序
第1趟排序:[9, 30, 49, 30*, 21, 16]
第2趟排序:[9, 16, 49, 30*, 30, 21]
第3趟排序:[9, 16, 21, 49, 30, 30*]
第4趟排序:[9, 16, 21, 30, 49, 30*]
第5趟排序:[9, 16, 21, 30, 30*, 49]
排序之後:[9, 16, 21, 30, 30*, 49]
       其實從程序和結果中不難看出,直接選擇排序每趟比較最多只需要交換一次就夠:主要找到本趟比較重最小的數據,然後拿它和本趟比較中第1位的數據交換。針對上面發現的問題,對直接選擇排序進行改進,改進後的算法如下所示。
import java.util.Arrays;

public class SelectSort2 {
	public static void selectSort(DataWrap[] data) {
		System.out.println("開始排序:");
		int len = data.length;
		for ( int i = 0 ; i < len - 1 ; i ++ ) {
			int minIndex = i;
			for ( int j = i + 1 ; j < len ; j ++ ) {
				if ( data[minIndex].compareTo(data[j]) > 0 ) {
					minIndex = j;
				}
			}
			if (minIndex != i) {
				DataWrap tmp = data[i];
				data[i] = data[minIndex];
				data[minIndex] = tmp;
			}
			System.out.println("第" + (i + 1) + "趟排序:" + Arrays.toString(data));
		}
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
			new DataWrap(21, ""),
			new DataWrap(30, ""),
			new DataWrap(49, ""),
			new DataWrap(30, "*"),
			new DataWrap(16, ""),
			new DataWrap(9, ""),
		};
		System.out.println("排序之前:" + Arrays.toString(data));
		selectSort(data);
		System.out.println("排序之後:" + Arrays.toString(data));
	}
}
輸出結果爲:
排序之前:[21, 30, 49, 30*, 16, 9]
開始排序:
第1趟排序:[9, 30, 49, 30*, 16, 21]
第2趟排序:[9, 16, 49, 30*, 30, 21]
第3趟排序:[9, 16, 21, 30*, 30, 49]
排序之後:[9, 16, 21, 30*, 30, 49]

總結:對於直接選擇排序算法而言,假設有n個數據,數據交換次數最多有n-1次,但程序比較的次數較多。總體來說,其時間效率爲O(n²)。直接選擇排序算法的空間效率爲O(1)。從程序運行的結果來看,直接選擇排序是不穩定的。

2、2 堆排序

       假設有n個數據元素的序列k0, k1,…, kn-1,當且僅當滿足如下關係時,可以將這組數據稱爲小頂堆(小根堆)。
ki≤k2i+1且ki≤k2i+2(其中i=0,2,…,(n-1)/2)
       或者,滿足如下關係,可以將這組數據稱爲大頂堆(大根堆)。
ki≥k2i+1且ki≥k2i+2(其中i=0,2,…,(n-1)/2)
       對於滿足小根堆的數據序列k0, k1,…, kn-1,如果將它們順序排成一棵完全二叉樹,則此樹的特點是,書中所有節點的值都是小於其左、右子節點的值,此樹的根節點的值必然最小。反之,大根堆的根節點的值必然最大。同時,通過定義可以得知,小根堆的任意子樹也是小根堆,大根堆的任意子樹也是大根堆。
       
       對於堆排序的關鍵在於建堆,它按照如下步驟完成排序。
  1. 將索引0~n-1處的全部數據建成大根(或小根)堆,就可以選擇出這組數據中的最大(或最小)值。將所建的大根(或小根)堆的根節點與這組數據的倒數第1個節點交換,就使得這組數據中的最大(或最小)值排在最後。
  2. 將索引0~n-2處的全部數據建成大根(或小根)堆,就可以選擇出這組數據中的最大(或最小)值。將所建的大根(或小根)堆的根節點與這組數據的倒數第2個節點交換,就使得這組數據中的最大(或最小)值排在最後。
      ......
       通過上面的介紹可以發現,堆排序的步驟就是重複執行以下兩步。
  1. 建堆。
  2. 拿堆的根節點和最後一個節點交換。
       由此可見,對於包含n個數據元素的數據組來說,堆排序需要經過n-1次建堆,每次建堆的作用就是選出該堆的最大值或最小值。堆排序本質上依然是一種選擇排序。堆排序與直接選擇排序的差別在於,堆排序可以通過樹形結構保存部分比較結果,可減少比較次數,從而提高效率。
       接下來的關鍵就是建堆的過程。建堆其實比較簡單,不斷重複如下步驟即可(以建大根堆爲例)。
  1. 從最後一個非葉子節點開始,比較該節點和它兩個子節點的值;如果某個子節點的值大於父節點的值,就把父節點和較大的子節點交換。
  2. 向前逐步調整直到根節點,即保證每個父節點的值都大於等於其左、右子節點的值,建堆完成。
       例如,有如下數組數據:9,79,46,30,58,49。下面介紹完整的建堆過程。
       1、先將其轉換爲完成二叉樹,轉換得到的完全二叉樹如下所示。

       2、完全二叉樹的最後一個非葉子節點,也就是最後一個父節點。最後一個節點的索引爲len-1,那麼最後一個非葉子節點的索引應該爲(len-2)/2。也就是從索引爲2的節點開始,如果其子節點的值大於它本身的值,則把它和較大的子節點進行交換,即將索引爲2的節點和索引爲5的元素交換,交換後的結果如下所示。

       3、向前處理前一個非葉子節點(索引爲(len-2)/2-1),也就是處理索引爲1的節點,此時79>30、79>58,因此無需交換。
       4、向前處理前一個非葉子節點,也就是處理索引爲0的節點,此時9<79,因此需要交換,交換後的結果如下所示。

       5、如果某個節點和它的某個子節點交換後,該子節點又有自己點,那麼系統還需要再次對該子節點進行判斷。例如,索引爲0的節點和索引爲1的節點交換後,索引爲1的節點還有子節點,因此程序必須再次保證索引爲1的節點的值大於等於其左、右子節點的值,因此還需要交換一次,交換後的結果如下所示。

       下面程序實現堆排序
import java.util.Arrays;

public class HeapSort {
	public static void heapSort(DataWrap[] data) {
		System.out.println("開始排序");
		int len = data.length;
		for ( int i = 0 ; i < len - 1 ; i ++ ) {
			buildMaxHeap(data , len -1 - i);  //建大根堆
			swap(data , 0 , len -1 - i);  //交換堆頂和最後一個元素
			System.out.println("第" + (i + 1) + "趟排序:" + Arrays.toString(data));
		}
	}
	
	private static void buildMaxHeap(DataWrap[] data , int lastIndex) {
		for ( int i = (lastIndex - 1) / 2 ; i >= 0 ; i -- ) {  //從最後一個節點的父節點開始
			int k = i;
			while (k * 2 + 1 <= lastIndex) {  //如果當前k節點的子節點存在
				int biggerIndex = k * 2 + 1;
				if(biggerIndex < lastIndex){  //如果當前k節點的右節點存在
					if(data[biggerIndex].compareTo(data[biggerIndex + 1]) < 0) {
						biggerIndex++;
					}
				}
				if(data[k].compareTo(data[biggerIndex]) < 0 ) {
					swap(data , k , biggerIndex);
					k = biggerIndex;  //重新保證k節點的值大於等於其左、右子節點的值
				}
				else {
					break;
				}
			}
		}
	}
	
	private static void swap(DataWrap[] data , int i , int j) {
		DataWrap tmp = data[i];
		data[i] = data[j];
		data[j] = tmp;
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
				new DataWrap(21, ""),
				new DataWrap(30, ""),
				new DataWrap(49, ""),
				new DataWrap(30, "*"),
				new DataWrap(21, "*"),
				new DataWrap(16, ""),
				new DataWrap(9, ""),
			};
		System.out.println("排序之前:" + Arrays.toString(data));
		heapSort(data);
		System.out.println("排序之後:" + Arrays.toString(data));
	}
}
輸出結果爲:
排序之前:[21, 30, 49, 30*, 21*, 16, 9]
開始排序
第1趟排序:[9, 30, 21, 30*, 21*, 16, 49]
第2趟排序:[16, 30*, 21, 9, 21*, 30, 49]
第3趟排序:[16, 21*, 21, 9, 30*, 30, 49]
第4趟排序:[9, 16, 21, 21*, 30*, 30, 49]
第5趟排序:[9, 16, 21, 21*, 30*, 30, 49]
第6趟排序:[9, 16, 21, 21*, 30*, 30, 49]
排序之後:[9, 16, 21, 21*, 30*, 30, 49]

總結:對於堆排序算法而言,假設有n個數據,需要進行n-1次建堆,每次建堆時間耗時log2n,則其時間效率爲O(n*log2n)。堆排序算法的空間效率爲O(1)。從程序運行的結果來看,堆排序是不穩定的。

3 交換排序法

       交換排序的主要操作是對數組中的數據不斷地進行交換操作。交換排序主要有冒泡排序和快速排序,這兩種排序都是應用極廣的排序算法。

3、1 冒泡排序

       對於包含n個數據的一組記錄,在最壞的情況下,冒泡排序需要進行n-1趟排序。
       第一趟:依次比較0和1、1和2、...、n-2和n-1索引處的元素,如果發現前一個數據大於後一個數據,則交換它們。經過第一趟的比較,最大的元素排到了最後。
       第二趟:依次比較0和1、1和2、...、n-3和n-2索引處的元素,如果發現前一個數據大於後一個數據,則交換它們。經過第二趟的比較,第二大的元素排到了最後。
       ......
       第n-1趟:依次比較0和1索引出的元素,如果發現前一個數據大於後一個數據,則交換它們。經過n-1趟的比較,第n-1大的元素排到了第二位。
       實際上,冒泡排序的每趟交換後,不經能將當前最大值排到最後的位置,還能部分理順前面的其他元素;一旦某趟沒有交換髮生,即可提前結束排序。
import java.util.Arrays;

public class BubbleSort {
	public static void bubbleSort(DataWrap[] data) {
		System.out.println("開始排序");
		int len = data.length;
		for ( int i = 0 ; i < len - 1 ; i ++ ) {
			boolean flag = false;
			for ( int j = 0 ; j < len - 1 - i ; j ++ ) {
				if (data[j].compareTo(data[j + 1]) > 0 ) {
					DataWrap tmp = data[j];
					data[j] = data[j + 1];
					data[j + 1] = tmp;
					flag = true;
				}
			}
			System.out.println("第" + (i + 1) + "趟排序:" + Arrays.toString(data));
			if (!flag) {
				break;
			}
		}
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
			new DataWrap(9, ""),
			new DataWrap(16, ""),
			new DataWrap(21, "*"),
			new DataWrap(23, ""),
			new DataWrap(30, ""),
			new DataWrap(49, ""),
			new DataWrap(21, ""),
			new DataWrap(30, "*")
		};
		System.out.println("排序之前:" + Arrays.toString(data));
		bubbleSort(data);
		System.out.println("排序之後:" + Arrays.toString(data));
	}
}
輸出結果爲:
排序之前:[9, 16, 21*, 23, 30, 49, 21, 30*]
開始排序
第1趟排序:[9, 16, 21*, 23, 30, 21, 30*, 49]
第2趟排序:[9, 16, 21*, 23, 21, 30, 30*, 49]
第3趟排序:[9, 16, 21*, 21, 23, 30, 30*, 49]
第4趟排序:[9, 16, 21*, 21, 23, 30, 30*, 49]
排序之後:[9, 16, 21*, 21, 23, 30, 30*, 49]

總結:對於冒泡排序算法而言,其時間效率是不確定的,在最好的情況下,初始數據序列已經處於有序狀態,執行1趟冒泡即可,做n-1趟比較,無須進行任何交換;但在最壞的情況下,初始數據序列處於完全逆序狀態,算法要執行n-1趟冒泡,第i趟(1<i<n)做了n-i次比較,執行n-i-1次對象交換。此時的比較總次數爲n*(n-1)/2,記錄移動總次數爲n*(n-1)*3/2。冒泡排序算法的空間效率爲O(1)。從程序運行的結果來看,冒泡排序是穩定的。

3、2 快速排序

       快速排序是一個速度非常快的交換排序算法,它的基本思路很簡單:從待排序的數據序列中任取一個數據(如第一個數據)作爲分界值,所有比它小的數據元素一律放在左邊,所有比它大的數據元素一律放在右邊。經過這樣一趟下來,該序列形成左、右兩個子序列,左邊序列中的數據元素都比分界值小,右邊序列中的數據元素都比分界值大。接下來對左、右兩個子序列進行遞歸,對兩個子序列重新選擇中心元素並依照此規則調整,直到每個子序列的元素只剩一個,排序完成。
       從上面的算法分析可以看出,實現快速排序的關鍵在於第一趟要做的事情,如下所示。
  1. 選出指定的分界值。
  2. 將所有比分界值小的數據元素放在左邊。
  3. 將所有比分界值大的數據元素放在右邊。
       問題在於,如何實現上面的第2和第3步?這是就要用到交換的思想了,其思路如下。
  1. 定義一個i變量,i變量從左邊的第一個索引開始,找大於分界值的元素的索引,並用i來記錄它。
  2. 定義一個j變量,j變量從右邊的第一個索引開始,找小於分界值的元素的索引,並用j來記錄它。
  3. 如果i<j,則交換i,j兩個索引處的元素。
       重複執行以上1~3步,直到i≥j,可以判斷j左邊的數據元素都小於分界值,j右邊的數據元素都大於分界值,最後將分界值和j索引處的元素交換即可。
import java.util.Arrays;

public class QuickSort {
	private static void swap(DataWrap[] data , int i , int j ) {
		DataWrap tmp;
		tmp = data[i];
		data[i] = data[j];
		data[j] = tmp;
	}
	
	private static void subSort(DataWrap[] data , int start , int end) {
		if (start < end) {
			DataWrap base = data[start];
			int i = start;
			int j = end  + 1;
			while (true) {
				while (i < end && data[++i].compareTo(base) <= 0);
				while (j > start && data[--j].compareTo(base) >= 0);
				if (i < j) {
					swap(data , i , j);
				}
				else {
					break;
				}
			}
			swap(data , start , j);
			subSort(data, start, j - 1);
			subSort(data, j + 1, end);
		}
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
			new DataWrap(9, ""),
			new DataWrap(-16, ""),
			new DataWrap(21, ""),
			new DataWrap(23, ""),
			new DataWrap(-30, ""),
			new DataWrap(-49, ""),
			new DataWrap(21, "*"),
			new DataWrap(30, ""),
			new DataWrap(13, ""),
		};
		System.out.println("排序之前:" + Arrays.toString(data));
		subSort(data, 0, data.length - 1);
		System.out.println("排序之後:" + Arrays.toString(data));
	}
}
輸出結果爲:
排序之前:[9, -16, 21, 23, -30, -49, 21*, 30, 13]
排序之後:[-49, -30, -16, 9, 13, 21, 21*, 23, 30]

總結:快速排序的時間效率很好,因爲它每趟能確定的元素呈指數增長。快速排序需要使用遞歸,而遞歸使用棧,因此它的空間效率爲O(n*log2n)。快速排序中包含跳躍式交換,因此是不穩定的排序。

4 插入排序法

       插入排序也是一類非常常見的排序方法,它主要包含直接插入排序、shell排序和折半插入排序等幾種常見的排序方法。

4、1 直接插入排序

       直接插入排序的思路非常簡單:依次將待排序的數據元素按其關鍵字值的大小插入前面的有序序列。
       細化來說,對於一個有n個元素的數據序列,排序需要進行n-1趟插入操作,如下所示。
       第一趟:將第2個元素插入前面的有序子序列中,此時前面只有一個元素,當然是有序的。
       第二趟:將第3個元素插入前面的有序子序列中,前面兩個元素時有序的。
       ......
       第n-1趟:將第n個元素插入前面的有序子序列中,前面n-1個元素時有序的。
import java.util.Arrays;

public class InsertSort {
	public static void insertSort(DataWrap[] data) {
		int len = data.length;
		for ( int i = 1 ; i < len ; i ++ ) {
			DataWrap tmp = data[i];
			if (data[i].compareTo(data[i - 1]) < 0) {
				int j = i - 1;
				for ( ; j >= 0 && data[j].compareTo(tmp) > 0 ; j -- ) {
					data[j + 1] = data[j];
				}
				data[j + 1] = tmp;
			}
			System.out.println("第" + i + "趟排序:" + Arrays.toString(data));
		}
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
			new DataWrap(9, ""),
			new DataWrap(-16, ""),
			new DataWrap(21, "*"),
			new DataWrap(23, ""),
			new DataWrap(-30, ""),
			new DataWrap(-49, ""),
			new DataWrap(21, ""),
			new DataWrap(30, "*"),
			new DataWrap(30, "")
		};
		System.out.println("排序之前:" + Arrays.toString(data));
		insertSort(data);
		System.out.println("排序之後:" + Arrays.toString(data));
	}
}
輸出結果爲:
排序之前:[9, -16, 21*, 23, -30, -49, 21, 30*, 30]
第1趟排序:[-16, 9, 21*, 23, -30, -49, 21, 30*, 30]
第2趟排序:[-16, 9, 21*, 23, -30, -49, 21, 30*, 30]
第3趟排序:[-16, 9, 21*, 23, -30, -49, 21, 30*, 30]
第4趟排序:[-30, -16, 9, 21*, 23, -49, 21, 30*, 30]
第5趟排序:[-49, -30, -16, 9, 21*, 23, 21, 30*, 30]
第6趟排序:[-49, -30, -16, 9, 21*, 21, 23, 30*, 30]
第7趟排序:[-49, -30, -16, 9, 21*, 21, 23, 30*, 30]
第8趟排序:[-49, -30, -16, 9, 21*, 21, 23, 30*, 30]
排序之後:[-49, -30, -16, 9, 21*, 21, 23, 30*, 30]
 
總結:直接插入排序的時間效率不高,在最壞的情況下,所有元素的比較次數總和爲(0+1+...+n-1)=O(n²);在其他情況下,也要考慮移動元素的次數,故時間複雜度爲O(n²)。直接插入排序算法的空間效率爲O(1)。從程序運行的結果來看,直接插入排序是穩定的。

4、2 折半插入排序

       折半插入排序是對直接插入排序的簡單改進。對於直接插入排序而言,當第i-1趟需要將第i個元素插入前面的0~i-1個元素序列中時,它總是從i-1個元素開始,逐個比較每個元素,直到找到它的位置。這顯然沒有利用前面0~i-1個元素已經有序的特點,而折半插入排序則改進了這一點。
       折半插入排序的做法如下所示。
  1. 計算0~i-1索引的中間點,也就是用i索引處的元素和(0+i-1)/2索引處的元素進行比較,如果i索引處的元素大,就直接在(0+i-1)/2~i-1半個範圍內搜索;反之,就在0~(0+i-1)/2半個範圍內搜索,這就是所謂的折半。
  2. 在半個範圍內搜索時,再按第1步方法進行折半搜索。總是不斷地折半,這樣就可以將搜索範圍不斷縮小,從而快速確定第i個元素的插入位置。
注意:此處介紹的折半插入,其實就是通過不斷地這般來快速確定第i個元素的插入位置,這實際上就是一種查找算法:折半查找。Java的Arrays類裏有一個binarySearch()方法,它就是折半查找的實現,用於從指定數組(或數組的一部分)中查找指定元素,前提是該數組(或數組的一部分)已經處於有序狀態。
     3.一旦確定了第i個元素的插入位置,程序將該位置以後的元素整體後移一位,然後將第i個元素放入該位置。
import java.util.Arrays;

public class BinaryInsertSort {
	public static void binaryInsertSort(DataWrap[] data) {
		System.out.println("開始排序");
		int len = data.length;
		for ( int i = 1 ; i < len ; i ++ ) {
			DataWrap tmp = data[i];
			int low = 0;
			int high = i - 1;
			while (low <= high) {
				int mid = (low + high) / 2;
				if (tmp.compareTo(data[mid]) > 0){
					low = mid + 1;
				}
				else {
					high = mid - 1;
				}
			}
			for (int j = i ; j > low ; j --) {
				data[j] = data[j - 1];
			}
			data[low] = tmp;
			System.out.println("第" + i + "次排序:" + Arrays.toString(data));
		}
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
				new DataWrap(9, ""),
				new DataWrap(-16, ""),
				new DataWrap(21, "*"),
				new DataWrap(23, ""),
				new DataWrap(-30, ""),
				new DataWrap(-49, ""),
				new DataWrap(21, ""),
				new DataWrap(30, "*"),
				new DataWrap(30, "")
			};
			System.out.println("排序之前:" + Arrays.toString(data));
			binaryInsertSort(data);
			System.out.println("排序之後:" + Arrays.toString(data));
	}
}
總結:折半插入排序與直接插入排序的效果基本相同,只是更快一些,因此折半插入排序可以更快地準確第i個元素的插入位置。

4、3 Shell排序

       Shell排序對直接插入排序進行了簡單改進:它通過加大插入排序中元素之間的間隔,並在這些有間隔的元素中進行插入排序,從而使數據項大跨度的移動。當這些數據項排過一趟序列,Shell排序算法減小數據項的間隔再進行排序,依此進行下去。這些進行排序的數據項之間的間隔被稱爲增量,習慣上用h來表示這個增量。
       假設本次Shell排序的h爲4,其插入操作如下。
-30,-16,21*,23,9,-49,21,30*,30
-30,-16,21*,23,9,-49,21,30*,30
-30,-16,21*,23,9,-49,21,30*,30
-30,-16,21*,23,9,-49,21,30*,30
-30,-16,21*,23,9,-49,21,30*,30
       注意上面紅色的數據。
       當h增量爲4時,第1趟將保證做引爲0,4,8的數據元素已經有序。第1趟完成後,算法向右移一步,對索引爲1,5的數據元素進行排序。這個排序過程持續進行,直到所有的數據項都已經完成了以4爲增量的排序。也就是說,所有間隔爲4的數據項之間都已經排列有序。當完成以4爲增量的Shell排序後,所有元素離它最終有序序列中的位置相差不到兩個單元,這就是數組“基本有序”的含義。通過創建這種交錯的內部有序的數據項集合,就可以減少直接插入排序中數據項“整體搬家”的工作量。
       上面已經演示了以4爲增量的Shell排序,接下來應該減少增量,直到完成以1位增量的Shell排序,此時數據序列將會變爲有序序列。
注意:通過上面的介紹可以發現,可以認爲直接插入排序是Shell排序的一種特例——直接使用增量爲1的Shell排序就是直接插入排序。
       最終確定Shell排序算法的關鍵在於確定h序列的值。常用的h序列有Knuth提出,該序列從1開始,通過h=3*h+1產生。前面公式用於從1開始計算這個序列,可以看到h序列爲1,4,13,40......反過來,程序中還需要反向計算h序列,那應該使用公式:h=(h-1)/3。
import java.util.Arrays;

public class ShellSort {
	public static void shellSort(DataWrap[] data) {
		System.out.println("開始排序");
		int len = data.length;
		int h = 1;  //h變量保存可變增量
		while (h <= len / 3) {
			h = h * 3 + 1;
		}
		while (h > 0) {
			System.out.println("===h的值:" + h + "===");
			for ( int i = h ; i < len ; i ++) {
				DataWrap tmp = data[i];
				if (data[i].compareTo(data[i - h]) < 0) {
					int j = i - h;
					for ( ; j >=0 && data[j].compareTo(tmp) > 0 ; j -= h ) {
						data[j + h] = data[j];
					}
					data[j + h] = tmp;
				}
			}
			h = (h - 1) / 3;
		}
	}
	public static void main(String[] args) {
		DataWrap[] data = {
				new DataWrap(9, ""),
				new DataWrap(-16, ""),
				new DataWrap(21, "*"),
				new DataWrap(23, ""),
				new DataWrap(-30, ""),
				new DataWrap(-49, ""),
				new DataWrap(21, ""),
				new DataWrap(30, "*"),
				new DataWrap(30, "")
			};
			System.out.println("排序之前:" + Arrays.toString(data));
			shellSort(data);
			System.out.println("排序之後:" + Arrays.toString(data));
	}
}
輸出結果爲:
排序之前:[9, -16, 21*, 23, -30, -49, 21, 30*, 30]
開始排序
===h的值:4===
===h的值:1===
排序之後:[-49, -30, -16, 9, 21*, 21, 23, 30*, 30]
       Shell排序比插入排序快很多,因爲當h值大的時候,數據項每一趟排序需要移動元素的個數很少,但數據項移動的距離很長,這是非常有效率的。當h減小時,每一趟排序需要移動的元素的個數增多,但是此時數據項已經接近於它們排序最終的位置,這對插入排序可以更有效率。正是這兩種情況的結合才使得Shell排序效率那麼高。但是Shell排序是不穩定的排序。

4 歸併排序法

       歸併的基本思想就是將兩個(或以上)有序的序列合併成一個新的有序序列。西華來說,歸併排序先將長度爲n的無序序列看成是n個長度爲1的有序子序列,首先做兩兩合併,得到n/2個長度爲2的有序子序列,再做兩兩合併......不斷地重複這個過程,最終可以得到一個長度爲n的有序序列。

       上圖是歸併排序的示意過程,總結來說,對於長度爲n的數據序列,只需經過log2n次合併。
       對於歸併排序而言,其算法關鍵就在於“合併”。合併算法的具體步驟如下。
  1. 定義變量i,i從0開始,依次等於A序列的每個元素的索引。
  2. 定義變量j,j從0開始,依次等於B序列的每個元素的索引。
  3. 拿A序列中i索引處的元素和B序列中j索引處的元素進行比較,將較小的複製到一個臨時數組中。
  4. 如果i索引處的元素小,則i++;如果j索引處的元素小,則j++。
       不斷地重複上面四個步驟,即可將A、B兩個序列中的數據元素複製到臨時數組中,直到其中一個數組中的所有元素都被複制到臨時數組中。最後,將另一個數組多出來的元素全部複製到臨時數組中,合併即完成,再將臨時數組中的元素複製回去即可。
import java.util.Arrays;

public class MergeSort {
	public static void mergeSort(DataWrap[] data) {
		sort(data , 0 , data.length - 1);
	}
	
	/**
	 * 將索引從left到right範圍的數組元素進行歸併排序
	 * @param data 待排序的數組
	 * @param left 待排序的數組的第一個元素的索引
	 * @param right 待排序的數組的最後一個元素的索引
	 */
	private static void sort(DataWrap[] data , int left , int right) {
		if (left < right) {
			int center = (left + right) / 2;
			sort(data , left , center);  //對左邊數組進行遞歸
			sort(data , center + 1 , right);  //對右邊數組進行遞歸
			merge(data , left , center , right);
		}
	}
	
	/**
	 * 將兩個數組進行歸併,歸併前兩個數組已經有序,歸併後依然有序
	 * @param data 數組對象
	 * @param left 左數組的第一個元素的索引
	 * @param center 左數組的最後一個元素的索引,center + 1是右數組的第一個元素的索引
	 * @param right 右數組的最後一個元素的索引
	 */
	private static void merge(DataWrap[] data , int left , int center , int right) {
		DataWrap[] tmpArr = new DataWrap[data.length];
		int mid = center + 1;
		int third = left;  //用於記錄中間數組的索引
		int tmp = left;
		while (left <= center && mid <= right) {  //從兩個數組中取出小的放入中間數組
			if (data[left].compareTo(data[mid]) <= 0) {
				tmpArr[third++] = data[left++];
			}
			else {
				tmpArr[third++] = data[mid++];
			}
		}
		while (mid <= right) {  //剩餘部分依次放入中間數組
			tmpArr[third++] = data[mid++];
		}
		while (left <= center) {
			tmpArr[third++] = data[left++];
		}
		while(tmp <= right) {  //將中間數組中的內容複製回原數組
			data[tmp] = tmpArr[tmp++];
		}
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
			new DataWrap(9, ""),
			new DataWrap(-16, ""),
			new DataWrap(21, "*"),
			new DataWrap(23, ""),
			new DataWrap(-30, ""),
			new DataWrap(-49, ""),
			new DataWrap(21, ""),
			new DataWrap(30, "*"),
			new DataWrap(30, ""),
		};
		System.out.println("排序之前:" + Arrays.toString(data));
		mergeSort(data);
		System.out.println("排序之後:" + Arrays.toString(data));
	}
}
輸出結果爲:
排序之前:[9, -16, 21*, 23, -30, -49, 21, 30*, 30]
排序之後:[-49, -30, -16, 9, 21*, 21, 23, 30*, 30]

總結:歸併算法需要遞歸地進行分解、合併,每進行一趟歸併排序需要調用一次merge()方法一次,每次執行merge()方法需要比較n次,因此歸併排序算法的時間複雜度爲O(n*log2n)。但是歸併算法的空間效率較差,它需要一個與原始序列同樣大小的輔助序列。歸併排序是穩定的。

5 桶式排序法

       桶式排序不再是一種基於比較的排序方法,它是一種非常巧妙的排序方式,但這種排序方式需要待排序序列滿足如下特徵:
  1. 待排序列的所有值處於一個可枚舉範圍內。
  2. 待排序列所在的這個可枚舉範圍不應該太大,否則排序開銷太大。
       下面介紹桶式排序的詳細過程,以如下待排序列爲例:5,4,2,4,1。這個待排序列處於0~5這個可枚舉範圍之內,而且這個範圍很小,正是桶式排序大有用處的地方。
       具體步驟如下:
       1、對這個可枚舉範圍構建一個buckets數組,用於記錄“落入”每個桶中的元素的個數。

       2、按如下公式對上圖所示的buckets數組的元素進行重新計算。
             buckets[i]=buckets[i]+buckets[i-1](其中1≤i≤buckets.length)

       重新計算後的buckets數組元素保存了“落入”當前桶和“落入”前面所有桶中元素的總數組,而且定義的桶本身就是從小到大排列的,也就是說,“落入”前面桶中的元素肯定小於“落入”當前桶中的元素。綜合上面兩點,可以得到一個結論:每個buckets數組元素的值小於、等於“落入”當前桶中元素的個數。也就是說,“落入”當前桶中的元素在有序序列中應該排在buckets數組元素值所確定的位置。
       上面的理論還有些抽象。以待排序列中的最後一個元素1爲例,找到新buckets數組中元素1對應桶的值,這表明元素1就應該排在第1位;再以待排序列中倒數第2個元素4爲例,找到新的buckets數組中元素4對應的值,這表明元素4就應該排在第4位,依次類推。
import java.util.Arrays;

public class BucketSort {
	public static void bucketSort(DataWrap[] data , int min , int max) {
		System.out.println("開始排序:");
		int arrayLength = data.length;
		DataWrap[] tmp = new DataWrap[arrayLength];
		int[] buckets = new int[max - min];  //buckets數組相當於定義了max-min個桶,並用於記錄待排序元素的信息
		for ( int i = 0 ; i < arrayLength ; i ++) {  //計算原buckets數組
			buckets[data[i].data - min]++;
		}
		System.out.println(Arrays.toString(buckets));
		for ( int i = 1 ; i < max - min ; i ++ ) {  //計算“落入”各桶內的元素在有序序列中的位置
			buckets[i] = buckets[i] + buckets[i - 1];
		}
		System.out.println(Arrays.toString(buckets));
		System.arraycopy(data, 0, tmp, 0, arrayLength);  //將data數組中數據完全複製,進行緩存
		for ( int k = arrayLength - 1 ; k >= 0 ; k -- ) {
			data[--buckets[tmp[k].data - min]] = tmp[k];
		}
	}
	
	public static void main(String[] args) {
		DataWrap[] data = {
			new DataWrap(9, ""),
			new DataWrap(5, ""),
			new DataWrap(-1, ""),
			new DataWrap(8, ""),
			new DataWrap(5, "*"),
			new DataWrap(7, ""),
			new DataWrap(3, ""),
			new DataWrap(-3, ""),
			new DataWrap(1, ""),
			new DataWrap(3, "*"),
		};
		System.out.println("排序之前:" + Arrays.toString(data));
		bucketSort(data , -3 , 10);
		System.out.println("排序之後:" + Arrays.toString(data));
	}
}
輸出結果爲:
排序之前:[9, 5, -1, 8, 5*, 7, 3, -3, 1, 3*]
開始排序:
[1, 0, 1, 0, 1, 0, 2, 0, 2, 0, 1, 1, 1]
[1, 1, 2, 2, 3, 3, 5, 5, 7, 7, 8, 9, 10]
排序之後:[-3, -1, 1, 3, 3*, 5, 5*, 7, 8, 9]


總結:桶式排序的時間效率極高,只需要經過兩輪遍歷就可以得到每個待排數據在有序序列中的位置。但是桶式排序的空間開銷比較大,它需要兩個數組:第1個buckets數組用於記錄“落入”各桶中元素的個數,進入保存各元素在有序序列中的位置;第2個數組用於緩存待排數據。桶式排序是穩定的。

6 基數排序法

       基數排序法不再是一種常規的排序方法,它必須依賴於另外的排序方法。基數排序的總體思路就是將待排序數據拆分成多個關鍵字進行排序。多關鍵字排序的思路就是將待排序數據裏的排序關鍵字拆分成多個排序關鍵字,然後,根據子關鍵字對待排數據進行排序。
       在進行多關鍵字排序時有兩種解決方案。
  1. 最高位優先法MSD(Most Significant Digit first)
  2. 最低位優先法LSD(Least Significant Digit first)
       例如,對192,221,13,23進行排序,可以觀察到它每個數據之多隻有3位,因此可以將每個數據拆分成3個關鍵字:百位(最高位)、十位、個位(最低位)。
       如果按照習慣思維,會先比較百位,百位大的數據大;百位相同再比較十位,十位大的數據大;最後再比較個位。人的習慣思維是最高位優先方式。但是這種方式計算機實現起來有一定的難度,當開始比較十位時,還需要判斷它們百位是否相同。計算機通常會選擇最低位優先法,如下所示。
       第1輪比較個位,對個位關鍵字排序後得到序列爲:221,192,13,23。
       第2輪比較十位,對十位關鍵字排序後得到序列爲:13,23,221,192。
       第3輪比較百位,對百位關鍵字排序後得到序列爲:13,23,221,192。
       從上面介紹可以看出,基數排序方法對任一個關鍵字排序時必須藉助另一種排序方法,而且這種排序方法必須是穩定的。根據桶式排序的特點,一般採用桶式排序的方式。
import java.util.Arrays;

public class MultiKeyRadixSort {
	/**
	 * @param data 待排序數組
	 * @param radix 指定關鍵字拆分的進制。如radix=10,表明按十進制拆分。
	 * @param d 指定將關鍵字拆分成幾個關鍵字
	 */
	public static void radixSort(int[] data , int radix , int d) {
		System.out.println("開始排序");
		int arrayLength = data.length;
		int[] tmp = new int[arrayLength];
		int[] buckets = new int[radix];
		for ( int i = 0 , rate = 1 ; i < d ; i ++ ) { //rate用於保存當前計算的位
			Arrays.fill(buckets, 0);  //重置count數組,開始統計第二個關鍵字
			System.arraycopy(data, 0, tmp, 0, arrayLength);
			for ( int j = 0 ; j < arrayLength ; j ++ ) {  //計算每個待排數據的子關鍵字
				int subKey = (tmp[j] / rate) % radix;
				buckets[subKey]++;
			}
			for ( int j = 1; j < radix ; j ++ ) {
				buckets[j] = buckets[j] + buckets[j - 1];
			}
			for ( int m = arrayLength - 1 ; m >=0 ; m -- ) {  //按子關鍵字對指定數據進行排序
				int subKey = (tmp[m] / rate) % radix;
				data[--buckets[subKey]] = tmp[m];
			}
			System.out.println("對" + rate + "位上子關鍵字排序:" + Arrays.toString(data));
			rate *= radix;
		}
	}
	
	public static void main(String[] args) {
		int[] data = {1100 , 192 , 211 , 12 , 14};
		System.out.println("排序之前:" + Arrays.toString(data));
		radixSort(data , 10 , 4);
		System.out.println("排序之後:" + Arrays.toString(data));
	}
}
輸出結果爲:
排序之前:[1100, 192, 211, 12, 14]
開始排序
對1位上子關鍵字排序:[1100, 211, 192, 12, 14]
對10位上子關鍵字排序:[1100, 211, 12, 14, 192]
對100位上子關鍵字排序:[12, 14, 1100, 192, 211]
對1000位上子關鍵字排序:[12, 14, 192, 211, 1100]
排序之後:[12, 14, 192, 211, 1100]

提示:對於多關鍵字來說,程序將待排數據拆分成多個子關鍵字後,既可以使用桶式排序,也可使用任何一種穩定的排序方法。
















發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章