排序
排序是使數據有序化的操作。這里的數據包括關鍵字和其它信息項,關鍵字用來控制排序。排序使得數據有序化,實際上是使數據按關鍵字的某個定義明確的順序規則排列。如果被排序的數據在內存中,那么這個排序方法就叫做內排序;如果數據來自磁盤則叫做外部排序。其中內部排序能很容易訪問任何數據項,而外排序必須順序地訪問數據項。本章我們主要討論內部排序。
對於內部排序,數據在內存中的存儲方式分為數組和鏈表兩種。本章我們主要討論基於數組存儲方式的算法,並簡單介紹幾種基於鏈表存儲方式的數據的算法。對算法的性能評價包括時間開銷、空間開銷、穩定性等方面。時間和空間開銷比較容易理解,所謂算法穩定性值得是:如果排序算法不改變關鍵字相同的記錄的相對順序,那它就是穩定的。通過本章的討論,讀者可以發現大部分簡單排序算法是穩定的,然而部分復雜的算法是不穩定的。
排序通常有兩種方式來訪問數據項,存取關鍵字進行比較或者訪問整個數據項進行移動。如果要排序的數據項內存空間較大,則通過間接排序來避免移動數據項。可以不對數據項本身而是對一個數據項的指針數組進行排序,其中數組的第一個元素指向最小的數據項,第二個指向次小的數據項。
.1 選擇排序
首先介紹選擇排序算法,並討論排序算法中的基本操作。算法流程如:首先找數組中的最小元素,把它與第一個位置的元素進行交換;然后,找到第二個最小的元素,並將它與數組第二個位置的元素進行交換;循環下去直到整個數組排序完成。由於每次找到的都是數組中剩余元素中的最小的元素,所以這種方法稱為選擇排序。
以字符串"selectionsort"為例,
s |
e |
l |
e |
c |
t |
i |
o |
n |
s |
o |
r |
c |
e |
l |
e |
s |
t |
i |
o |
n |
s |
o |
r |
c |
e |
l |
e |
s |
t |
i |
o |
n |
s |
o |
r |
c |
e |
e |
l |
s |
t |
i |
o |
n |
s |
o |
r |
c |
e |
e |
i |
s |
t |
l |
o |
n |
s |
o |
r |
c |
e |
e |
i |
l |
t |
s |
o |
n |
s |
o |
r |
c |
e |
e |
i |
l |
n |
s |
o |
t |
s |
o |
r |
c |
e |
e |
i |
l |
n |
o |
s |
t |
s |
o |
r |
c |
e |
e |
i |
l |
n |
o |
o |
t |
s |
s |
r |
c |
e |
e |
i |
l |
n |
o |
o |
r |
s |
s |
t |
c |
e |
e |
i |
l |
n |
o |
o |
r |
s |
s |
t |
c |
e |
e |
i |
l |
n |
o |
o |
r |
s |
s |
t |
c |
e |
e |
i |
l |
n |
o |
o |
r |
s |
s |
t |
每次循環在表中灰色底紋的元素(a[i]~a[N-1])中找出最小的字符(記下下標位置min),黑體標出的字符為找出的最小元素項;然后將a[min]與a[i]進行交換。
以下是算法的實現:
* 選擇排序算法
* @param a ITEM類型的數組
* @param l, r 待排序的始末下標
*/
public void selection(ITEM[] a, int l, int r)
throws InterruptedException{
for( int i = l; i < r; i++){
// 記錄每次掃描得到的最小元素項的位置
int min = i;
// 每次掃描尋找最小元素項的位置
for( int j = i + 1; j <= r; j++){
if(a[j].compareTo(a[min]) == -1)
min = j;
}
// 每次掃描后將第i個元素與找到的最小的元素項進行換位
exch(a, i, min);
}
}
算法內層循環選擇剩余元素中最小的元素的下標min。循環外部做元素項的移動操作,通過exch函數實現。算法中包含兩層循環,時間復雜度為O(n2)。
算法的時間消耗與數據的原始狀態無關,每次尋找剩余數據中的最小元素時之前的遍歷過程不為本次遍歷提供任何信息。對於一個順序由大到小排列的有序數組,調用選擇排序時算法的時間消耗與對隨機順序的數組的排序的時間消耗幾乎相同,這是本算法的最大缺點。
在算法中,我們可以發現,內存循環找到剩余數據中最小的元素后,在循環外對數據交換位置。與選擇排序算法相比,沒有任何其它算法能用更少的數據移動來完成排序。
在本章中,為了直觀地理解算法過程,我們利用動畫跟蹤算法過程中數據的順序變化。如圖為一組隨機數據,數據個數為200,范圍為[0, 1),在下文稱這組數據為DATASET1。利用選擇排序對數據進行排序過程中數組中數據順序變化如圖(~)所示。

從圖中數據順序的變化可以看出,第i循環結束時,數組中前i個元素的將按照由小到大順序排列。執行N次循環后數組中的所有元素將排序結束。
.2 排序過程可視化
.2.1 Applet類和Runnable接口
Java程序有兩類,一類是我們通常編寫的應用程序,另一類就是小應用程序(applet)。Applet程序編譯后可以嵌入到頁面中,由支持Java的瀏覽器(IE 或 Nescape)解釋執行能夠產生特殊效果的程序。它可以大大提高Web頁面的交互能力和動態執行能力。
小應用程序除了可以由支持Java的網頁瀏覽器運行之外,也可以通過Java開發工具的appletviewer來運行。幸運的是,在我們所使用的Eclipse集成開發環境中可以直接運行applet程序。
Applet小應用程序的實現主要依靠Applet類,它繼承了java.awt.Panel。所以applet具有強大的可視化功能。
每個小應用程序都是Applet類的子類,在一般情況下有init()初始化函數,start()啟動函數,stop()停止函數等。
l init()方法
這個方法主要是為Applet的正常運行做一些初始化工作。當一個Applet被系統調用時,系統首先調用的就是該方法。通常可以在該方法中完成從網頁向Applet傳遞參數,添加用戶界面的基本組件等操作。
l start()方法
系統在調用完init()方法之后,將自動調用start()方法。而且,每當用戶離開包含該Applet的主頁后又再返回時,系統又會再執行一遍start()方法。這就意味着start()方法可以被多次執行,而不像init()方法。因此,可把只希望執行一遍的代碼放在init()方法中。可以在start()方法中開始一個線程,如繼續一個動畫、聲音等。
l stop()方法
這個方法在用戶離開Applet所在頁面時執行,因此,它也是可以被多次執行的。它使你可以在用戶並不注意Applet的時候,停止一些耗用系統資源的工作以免影響系統的運行速度,且並不需要人為地去調用該方法。如果Applet中不包含動畫、聲音等程序,通常也不必實現該方法。
這里我們借助apple類將每個數據以點的形式顯示出來。這里為了顯示方便,我們規定所有數據為double類型,范圍為[0,1)。
可以在stat()方法中開啟一個線程,並用於動態顯示數據點,Java中實現多線程有兩種途徑:繼承Thread類或者實現Runnable接口。
Runnable接口非常簡單,定義一個方法run()即可。繼承Runnable並實現這個方法就可以實現多線程了,但是這個run()方法不能自己調用,必須由系統或者客戶程序來調用,否則就和普通的方法沒有什么區別了。
.2.2 排序動畫類
基於Applet類和Runnable接口,可以實現算法動畫類Animate。其實現如下:
import java.applet.Applet;
import java.awt.*;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
/**
* 繼承Applet和Runnable的抽象類
*/
public abstract class Animate
extends Applet implements Runnable{
/* _old顏色表示的是被更換前的值,
* _new顏色表示的是被更換之后的值 */
Color _old = Color.white;
Color _new = Color.red;
Graphics g;
/* 本線程 */
Thread animatorThread;
/* N表示的是本排序對應的隨機數的個數 */
int N;
/* a數組長度為N,待排序的數組 */
ITEM[] a;
/* 為了顯示,用於暫停程序 */
private void pause() throws InterruptedException{
Thread.sleep(Parameter.sleeptime);
}
public void init(){
this.resize(320, 240);
}
/* 線程啟動 */
public void start(){
init();
g = getGraphics();
new Thread( this).start();
}
/* 線程結束 */
public void stop(){animatorThread = null;}
// 線程運行函數
public void run(){
/* 讀取配置參數 */
N = Parameter.N;
a = new ITEM[N];
String strLine = null; // 從文件讀取一行字符串
/* 產生隨機數,並首先在圖上畫出每個值對應的點 */
int i = 0;
// for(int i = 0; i < N; i++){
/* 從文件中讀取數據 */
FileReader fr = null;
BufferedReader br = null;
try
{
// 建立FileReader對象,並實例化為fr
fr = new FileReader(Parameter.filename);
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
// 建立BufferedReader對象,並實例化為br
br = new BufferedReader(fr);
while (i < N)
{
try {
// 從文件中繼續讀取一行數據
strLine = br.readLine();
} catch (IOException e2) {
e2.printStackTrace();
}
if(strLine == null)
break;
// 除基數排序之外,都是用double類型的元素項
double d = Double.parseDouble(strLine);
a[i] = new ITEM<Double>(d);
dot(X(i), Y(((Double)(a[i].elem)).doubleValue()), _new);
++i;
}
try {
fr.close();
br.close();
} catch (IOException e1) {
e1.printStackTrace();
}
// 調用排序函數,這里的sort是抽象函數
sort(a, 0, N - 1);
}
/* 將數組下標i轉為在圖像顯示的橫坐標 */
int X( int i){
return (i * getSize().width) / N;
}
/* 將數組的值轉為在圖像顯示的縱坐標 */
int Y( double v){
return ( int)((1 - v)*getSize().height);
}
/* 在位置(x, y)處顯示顏色為c的一個點 */
void dot( int x, int y, Color c){
g.setColor(c);
g.fillOval(x, y, 5, 5);
}
/* 交換第i和第j個元素,
* 原先第i個位置和第j個位置顏色為_old,
* 交換后第i位置和第j位置顏色為_new */
void exch(ITEM[] a, int i, int j) throws InterruptedException{
ITEM t = a[i];
a[i] = a[j];
a[j] = t;
dot(X(i), Y(((Double)(a[j].elem)).doubleValue()), _old);
dot(X(j), Y(((Double)(a[i].elem)).doubleValue()), _old);
dot(X(i), Y(((Double)(a[i].elem)).doubleValue()), _new);
dot(X(j), Y(((Double)(a[j].elem)).doubleValue()), _new);
pause();
}
/* 如果第i個元素大於第j個元素,則交換着兩個元素 */
void compExch(ITEM[] a, int i, int j) throws InterruptedException{
if(a[i].compareTo(a[j]) == 1){
exch(a, i, j);
}
}
/* 拷貝數據,將i位置的元素重置為val,重置前的值顏色為_old
* 重置后的顏色為_new */
void cpyVal(ITEM[] a, int i, ITEM val) throws InterruptedException{
dot(X(i), Y(((Double)(a[i].elem)).doubleValue()), _old);
a[i] = val;
dot(X(i), Y(((Double)(a[i].elem)).doubleValue()), _new);
}
/**
* 接口函數,抽象函數
* @param a 待排序的數組
* @param l 待排序的范圍的左端下標
* @param r 待排序的范圍的右端下標
*/
abstract void sort(ITEM a[], int l, int r);
}
其中init()方法將Applet窗口初始化為320×240。start()方法調用初始化方法,然后開啟一個線程顯示動畫。run()為線程運行函數,在函數中首先從指定文件中讀取指定個數的數據,並顯示在窗口中,最后調用抽象函數sort對數據進行排序。
X(int)函數將給定的數組序號轉換為顯示窗口上的橫坐標值,窗口的最左端對應數組的0下標,最右端對應數組的最后一個下標。Y(double)函數則將給定的數組中元素值轉換為顯示窗口上的縱坐標,最下端對應數組元素值為0,最上端對應的數組元素值為1,如圖。

(i, X[i])→(320*i/N, 240 * (1-x[i]))
圖 數組元素作圖
dot函數在指定的窗口位置(x, y)畫一個指定顏色的點。exch函數交換兩個數據,並將更新這兩個數據對應點的顏色。compExch函數比較數組中兩個元素a[i]和a[j],如果a[i] > a[j]則調用exch函數交換這兩個元素。cpyVal函數將元素項val賦值給數組中的第i項a[i],同時更新點(i, a[i])。
本類是一個抽象類,包含抽象函數sort。可以創建新的類繼承本類並定義sort函數,sort函數可以用不同排序算法來實現。
.3 插入排序
我們對撲克牌的通常拿法是依次取一張牌,將它插入到已經排好序的牌中的適當位置,並維持手上撲克牌全部按順序排列。在對數組中元素進行排序時可以借鑒這個過程:將數組中的元素依次作為新來的元素,插入到該元素之前的元素(子數組)中,不過這需要將較大元素依次向右移一個位置,為新元素騰出空間,最終將新元素插入到騰出的位置上。這個過程中每次將新元素插入到適當的位置,所以稱為插入排序。
插入排序過程中,當前下標以左的元素是按順序排列的,但是它們的位置並非最終的結果,這些位置在后來的元素插入時還可能會改變。當下標到達最右端,數組排序便結束。
以字符串"insertionsort"為例:
i |
n |
s |
e |
r |
t |
i |
n |
o |
s |
o |
r |
t |
i |
n |
s |
e |
r |
t |
i |
n |
o |
s |
o |
r |
t |
e |
i |
n |
s |
r |
t |
i |
n |
o |
s |
o |
r |
t |
e |
i |
n |
r |
s |
t |
i |
n |
o |
s |
o |
r |
t |
e |
i |
n |
r |
s |
t |
i |
n |
o |
s |
o |
r |
t |
e |
i |
i |
n |
r |
s |
t |
n |
o |
s |
o |
r |
t |
e |
i |
i |
n |
n |
r |
s |
t |
o |
s |
o |
r |
t |
e |
i |
i |
n |
n |
o |
r |
s |
t |
s |
o |
r |
t |
e |
i |
i |
n |
n |
o |
r |
s |
s |
t |
o |
r |
t |
e |
i |
i |
n |
n |
o |
o |
r |
s |
s |
t |
r |
t |
e |
i |
i |
n |
n |
o |
o |
r |
r |
s |
s |
t |
t |
e |
i |
i |
n |
n |
o |
o |
r |
r |
s |
s |
t |
t |
每次循環都確定了陰影部分的子數組中元素的順序,但是此時這些元素的位置並非最終的位置。黑體字符表示新元素在之前子數組中插入的位置。
算法實現如下:
* 插入排序算法
* @param a ITEM類型的數組
* @param l, r 待排序的始末下標
* @throws InterruptedException
*/
public void insertion(ITEM[] a, int l, int r)
throws InterruptedException{
// 循環數
int i;
// 先將數組的第一個位置上擺放最小的元素項
for(i = r; i > l; i--)
compExch(a, i - 1, i);
// 從第2個元素項(從第0個開始)開始迭代
for(i = l + 2; i <= r; i++){
// 從位置i-1向前,逐個向后移一位,直至a[i]擺放到正確的位置
int j = i; ITEM v = a[i];
while(v.compareTo(a[j - 1]) == -1){
// 逐個向后移一位
cpyVal(a, j, a[j-1]);
j--;
}
// 將a[i]放到正確的位置,即位置j
cpyVal(a, j, v);
Thread.sleep(Parameter.sleeptime);
}
}
算法初始時首先確定最終數組最左端元素,即第l個元素的值,這可以通過一次數組遍歷來實現。然后從第l+2個元素開始將新元素插入到之前已排序的子數組中,這里之所以從l+2個元素開始而不是第l+1個元素,因為在確保數組中第l個元素是所有元素中最小的前提下,數組中第l個和第l+1個元素肯定是按順序排列的,則可以直接從第l+2個元素開始進行插入循環。
插入排序的算法復雜度依賴於輸入文件中的關鍵字的最初順序,一般直接插入排序的時間復雜度為O(n2),但是當數列基本有序時,如果按照有數列順序排時,時間復雜度將改善到O(n)。
同樣以DATASET1為例,算法過程中數組各位置上的元素的動態變化為:

從算法過程中數據順序變化過程可以看出,第i次遍歷后前i個元素項按順序排列,其后的所有元素的順序保持不變;但前i個位置上的元素在之后的遍歷中又有所變化。
.4 冒泡排序
冒泡排序是是最常使用的一種簡單排序,不斷遍歷數據,交換倒序的相鄰元素,使得較小的數放在前面,較大的數據放在后面。
這里的數據遍歷順為從右向左,數據下標范圍為[l, r]。首先比較第r-1個和第r個數,將小數放前,大數放后;然后比較第r-2個數和第r-1個數,將小數放前,大數放后,如此繼續,直至比較最前面第0和第1個數,將小數放前,大數放后。一次遍歷后數組中第0個位置上的數據將是最小的。循環調用上述過程,每次遍歷都從第r個數開始,但是每次遍歷的數據個數依次減1,N次遍歷后數組中數據將排序完成。
以字符串"bubbleexamplesort"為例:
b |
u |
b |
b |
l |
e |
e |
x |
a |
m |
p |
l |
e |
s |
o |
r |
t |
a |
b |
u |
b |
b |
l |
e |
e |
x |
e |
m |
p |
l |
o |
s |
r |
t |
a |
b |
b |
u |
b |
e |
l |
e |
e |
x |
l |
m |
p |
o |
r |
s |
t |
a |
b |
b |
b |
u |
e |
e |
l |
e |
l |
x |
m |
o |
p |
r |
s |
t |
a |
b |
b |
b |
e |
u |
e |
e |
l |
l |
m |
x |
o |
p |
r |
s |
t |
a |
b |
b |
b |
e |
e |
u |
e |
l |
l |
m |
o |
x |
p |
r |
s |
t |
a |
b |
b |
b |
e |
e |
e |
u |
l |
l |
m |
o |
p |
x |
r |
s |
t |
a |
b |
b |
b |
e |
e |
e |
l |
u |
l |
m |
o |
p |
r |
x |
s |
t |
a |
b |
b |
b |
e |
e |
e |
l |
l |
u |
m |
o |
p |
r |
s |
x |
t |
a |
b |
b |
b |
e |
e |
e |
l |
l |
m |
u |
o |
p |
r |
s |
t |
x |
a |
b |
b |
b |
e |
e |
e |
l |
l |
m |
o |
u |
p |
r |
s |
t |
x |
a |
b |
b |
b |
e |
e |
e |
l |
l |
m |
o |
p |
u |
r |
s |
t |
x |
a |
b |
b |
b |
e |
e |
e |
l |
l |
m |
o |
p |
r |
u |
s |
t |
x |
a |
b |
b |
b |
e |
e |
e |
l |
l |
m |
o |
p |
r |
s |
u |
t |
x |
a |
b |
b |
b |
e |
e |
e |
l |
l |
m |
o |
p |
r |
s |
t |
u |
x |
a |
b |
b |
b |
e |
e |
e |
l |
l |
m |
o |
p |
r |
s |
t |
u |
x |
a |
b |
b |
b |
e |
e |
e |
l |
l |
m |
o |
p |
r |
s |
t |
u |
x |
冒泡排序算法與選擇排序算法遍歷次數相同,數值大小比較的次數也相同,計算法復雜度為O(n2);每次遍歷后冒泡排序會粗略調整剩余元素的局部順序,使得整體趨於有序,並找到剩余元素中最小的項,從這個意義上說冒泡排序是一種特殊的選擇排序。但是冒泡排序需要執行元素移位操作,這導致算法執行效率較低。算法實現如下:
* 冒泡排序算法
* @param a 一個ITEM類型的數組
* @param l, r 待排序的始末下標
* @throws InterruptedException
*/
public void bubble(ITEM[] a, int l, int r) throws InterruptedException{
// 從左向右掃描每一個元素
for( int i = l; i < r; i++)
// 每次掃描從最右端開始逐個向前冒泡,找到第i個最小的元素
for( int j = r; j > i; j--)
compExch(a, j - 1, j);
}
同樣以DATASET1為例,算法過程中數組各位置上的元素的動態變化為:

從圖可以看出,冒泡排序算法與選擇排序過程中數據順序變化很相似,只是冒泡排序除了找到剩余元素中最小元素之外還調整了剩余元素順序的整體趨勢。
.5 shell排序
在插入排序算法過程中我們提到,由於需要頻繁進行元素移位操作,導致算法效率很低。例如,a[r-1]為數組中最小的元素項,則需要進行N次移位操作才能將其移到最終的正確位置。Sell排序時插入排序的改進:允許相隔很遠的元素進行交換從而提高速度。
sell排序算法的思想是對一定間隔上的元素項進行排序,從而使得從任何一個元素起始,每間隔h個元素就產生一個已排序的文件。算法首先把數組中元素移到很遠的位置,此時h值較大,這可以使得對小一點的h值排序更容易。這樣的h一直取值到1,最終得到排好序的文件。
實現shell排序的一種方法是,對每個h,用插入排序在每個子文件上進行獨立排序。可以調用第節中的插入排序,但是元素遍歷掃描時的步進為h而不是1。
以字符串"shellexamplesort"為例,h值取4和1。h=4時,首先比較判斷a[4]和a[0];然后a[5]和a[1]……;比較a[8],a[4]和a[0]……。h=1時,遍歷過程與第節中的插入排序相同。過程如下:
h = 4
s |
h |
e |
l |
l |
e |
x |
a |
m |
p |
l |
e |
s |
o |
r |
t |
l |
h |
e |
l |
s |
e |
x |
a |
m |
p |
l |
e |
s |
o |
r |
t |
l |
e |
e |
l |
s |
h |
x |
a |
m |
p |
l |
e |
s |
o |
r |
t |
l |
e |
e |
l |
s |
h |
x |
a |
m |
p |
l |
e |
s |
o |
r |
t |
l |
e |
e |
a |
s |
h |
x |
l |
m |
p |
l |
e |
s |
o |
r |
t |
l |
e |
e |
a |
m |
h |
x |
l |
s |
p |
l |
e |
s |
o |
r |
t |
l |
e |
e |
a |
m |
h |
x |
l |
s |
p |
l |
e |
s |
o |
r |
t |
l |
e |
e |
a |
m |
h |
l |
l |
s |
p |
x |
e |
s |
o |
r |
t |
l |
e |
e |
a |
m |
h |
l |
e |
s |
p |
x |
l |
s |
o |
r |
t |
l |
e |
e |
a |
m |
h |
l |
e |
s |
p |
x |
l |
s |
o |
r |
t |
l |
e |
e |
a |
m |
h |
l |
e |
s |
o |
x |
l |
s |
p |
r |
t |
l |
e |
e |
a |
m |
h |
l |
e |
s |
o |
r |
l |
s |
p |
x |
t |
l |
e |
e |
a |
m |
h |
l |
e |
s |
o |
r |
l |
s |
p |
x |
t |
h = 1
l |
e |
e |
a |
m |
h |
l |
e |
s |
o |
r |
l |
s |
p |
x |
t |
e |
l |
e |
a |
m |
h |
l |
e |
s |
o |
r |
l |
s |
p |
x |
t |
e |
e |
l |
a |
m |
h |
l |
e |
s |
o |
r |
l |
s |
p |
x |
t |
a |
e |
e |
l |
m |
h |
l |
e |
s |
o |
r |
l |
s |
p |
x |
t |
a |
e |
e |
l |
m |
h |
l |
e |
s |
o |
r |
l |
s |
p |
x |
t |
a |
e |
e |
h |
l |
m |
l |
e |
s |
o |
r |
l |
s |
p |
x |
t |
a |
e |
e |
h |
l |
l |
m |
e |
s |
o |
r |
l |
s |
p |
x |
t |
a |
e |
e |
e |
h |
l |
l |
m |
s |
o |
r |
l |
s |
p |
x |
t |
a |
e |
e |
e |
h |
l |
l |
m |
s |
o |
r |
l |
s |
p |
x |
t |
a |
e |
e |
e |
h |
l |
l |
m |
o |
s |
r |
l |
s |
p |
x |
t |
a |
e |
e |
e |
h |
l |
l |
m |
o |
r |
s |
l |
s |
p |
x |
t |
a |
e |
e |
e |
h |
l |
l |
l |
m |
o |
r |
s |
s |
p |
x |
t |
a |
e |
e |
e |
h |
l |
l |
l |
m |
o |
r |
s |
s |
p |
x |
t |
a |
e |
e |
e |
h |
l |
l |
l |
m |
o |
p |
r |
s |
s |
x |
t |
a |
e |
e |
e |
h |
l |
l |
l |
m |
o |
p |
r |
s |
s |
x |
t |
a |
e |
e |
e |
h |
l |
l |
l |
m |
o |
p |
r |
s |
s |
t |
x |
圖 shell排序算法示例
算法實現如下:
* 下標;
*/
public void shell(ITEM[] a, int l, int r) throws InterruptedException{
// shell排序的步長
int h;
// 計算初始的步長,增量之間的比為3
for(h = l; h <= (r - l)/9; h = 3 * h + 1);
// 逐漸縮小步長,進行間隔地插入排序
for(; h > 0; h /= 3){
for( int i = l + h; i <= r; i++){
int j = i; ITEM v = a[i];
while(j >= l + h && v.compareTo(a[j - h]) == -1){
cpyVal(a, j, a[j - h]);
j -= h; /* 間隔地跳躍比較 */
}
// 賦值
cpyVal(a, j, v);
Thread.sleep(Parameter.sleeptime);
}
}
}
程序中增量h選擇為1,4,13,40,121……增量之間的比值為1/3。事實上,增量序列並不能確定,無法證明某個序列可以給算法帶來最好的性能。在實際中我們采用大致幾何遞減的增量序列,因此增量的數目與數組中元素個數成對數關系。例如,對一個有1010個元素的文件,如果每次增量大約前一次的1/2,則需要大概34個增量排序;如果此比率是1/4,則需要17個。
同樣以DATASET1為例,算法過程中數組各位置上的元素的動態變化為:

從圖中元素順序的變化過程可以發現,元素是逐漸趨於有序化的,沒變化一次h元素的有序性更高,這是一個由粗到細的過程。過程3,5,7,8分別對應的h大小為40,13,4,1。
shell排序的復雜度比前幾節中討論的算法復雜度稍低,但是依然是N的分數階O(N1.5)。在下面幾節中我們將討論一些更高效的方法。
.6 快速排序
本節將介紹快速排序算法,該算法是實際應用中使用最廣泛的算法。由於快速排序算法的實現比較容易,並且算法的資源消耗也相對較小,所以比較實用,是不少標准庫中排序算法的實現方法。
對個數為N的數組進行排序,算法的時間消耗與NlogN成正比,但是在最壞的情況下的消耗為N2。
.6.1 遞歸實現
快速算法利用一種分治法,首先算法把輸入數組A分成兩部分,划分成兩個子集As1和As2,划分點為a[p],此時滿足A = As1∪{a[p]}∪As2,其中As1中所有數據項小於a[p],As2中所有數據大於a[p],即a[p]的位置被確定了;然后對兩部分分別排序。數據的划分點取決於輸入數組中元素的初始順序。所以方法的關鍵點在於划分方法,上述過程使得數組滿足以下3個條件:
l 對p,元素a[p]在數組中的位置即為最終位置;
l a[l], a[2], ..., a[p-1]中沒有比a[p]大的元素;
l a[p+1], p[p + 1], ..., a[r]中沒有比a[p]小的元素。
划分后再分別遞歸地對分得的兩個子數組調用上述過程,這樣就可以實現快速排序的遞歸過程。
在每次划分操作過程中,可以選擇任意一個元素作為划分依據,在上面的過程中我們選擇的是a[r]。我們從最左邊掃描數組,直到找到一個比參照元素更大的元素,然后從右向左掃描直到找到第一個參照元素更小的元素。這兩個元素的位置顯然不符合上面的條件2和條件3,需要進行交換。繼續以這種方式進行下去,我們就能確保參照元素左邊沒有比參照元素更大的,而參照元素右邊沒有比參照元素更小的,如下圖:

圖中v指的是划分元素的值,i指的是左下標,j指的是右下標。如圖中,當參照元素左邊的元素大於或等於參照元素值時,左掃描(向右掃描),停止;當參照元素右邊的元素小於或等於參照元素是,就停止有掃描。當掃描交叉式,把a[r]與有子數組最左邊元素進行交換。
以字符串"quicksortstring"為例,采用迭代的思想對其進行排序。首先以元素a[14]為參考,從下標0逐漸增大,首次發現a[0]比a[14]大,從下標13逐漸減小,首次發現a[3]比a[14]小,交換a[0]和a[3];繼續從下標1逐漸增大,首次發現a[1]比a[14]大,從下標2逐漸減小,直到與下標1相遇,此時交換a[1]和a[14],可以看出,此時g位於位置1處,其左邊的元素都小於g,其右邊的元素都大於g,並且此時得到的划分點為1。
遞歸對{a[0]}和{a[2], ..., a[14]}分別調用快速排序。前者只有一個元素,無需排序;后面的子數組調用上面的過程,首先以a[14]為參照,從下標0逐漸增大,發現直到a[14]都沒有發現比a[14]大的值,則不需要交換元素,並且返回的划分點為14。則對子數組{a[2], ..., a[13]}遞歸調用快速排序。
首先以元素a[13]為參考,從下標2逐漸增大,首次發現a[3]比a[13]大,從下標13逐漸減小,首次發現a[12]比a[13]小,交換a[3]和a[12];繼續從下標4逐漸增大,首次發現a[5]比a[13]大,從下標11逐漸減小,直到與下標5相遇,此時交換a[5]和a[13],可以看出,此時n位於位置5處,其左邊的元素都小於n,其右邊的元素都大於n,並且此時得到的划分點為5。
然后分別對{a[2], ...,a[4]}{a[6], ..., a[13]}繼續遞歸調用快速排序函數。
具體步驟如下表,其中l_r欄表示數組的子數組的左右邊界,交換欄表示交換的兩個元素所處的下標,與表格中陰影部分對應。標記為○的行,表示中間交換過程,標記為●的行為最終划分點的確定。
l_r |
交換 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
○0_14 |
0↔3 |
q |
u |
i |
c |
k |
s |
o |
r |
t |
s |
t |
r |
i |
n |
g |
●0_14 |
1↔14 |
c |
u |
i |
q |
k |
s |
o |
r |
t |
s |
t |
r |
i |
n |
g |
●2_14 |
14↔14 |
c |
g |
i |
q |
k |
s |
o |
r |
t |
s |
t |
r |
i |
n |
u |
○2_13 |
3↔12 |
c |
g |
i |
q |
k |
s |
o |
r |
t |
s |
t |
r |
i |
n |
u |
●2_13 |
5↔13 |
c |
g |
i |
i |
k |
s |
o |
r |
t |
s |
t |
r |
q |
n |
u |
●2_4 |
4↔4 |
c |
g |
i |
i |
k |
n |
o |
r |
t |
s |
t |
r |
q |
s |
u |
●2_3 |
2↔3 |
c |
g |
i |
i |
k |
n |
o |
r |
t |
s |
t |
r |
q |
s |
u |
○6_13 |
8↔12 |
c |
g |
i |
i |
k |
n |
o |
r |
t |
s |
t |
r |
q |
s |
u |
○6_13 |
9↔11 |
c |
g |
i |
i |
k |
n |
o |
r |
q |
s |
t |
r |
t |
s |
u |
●6_13 |
10↔13 |
c |
g |
i |
i |
k |
n |
o |
r |
q |
r |
t |
s |
t |
s |
u |
○6_9 |
7↔8 |
c |
g |
i |
i |
k |
n |
o |
r |
q |
r |
s |
s |
t |
t |
u |
●6_9 |
8↔9 |
c |
g |
i |
i |
k |
n |
o |
q |
r |
r |
s |
s |
t |
t |
u |
●6_7 |
7↔7 |
c |
g |
i |
i |
k |
n |
o |
q |
r |
r |
s |
s |
t |
t |
u |
●11_13 |
12↔13 |
c |
g |
i |
i |
k |
n |
o |
q |
r |
r |
s |
s |
t |
t |
u |
根據上面的分析過程以及示意圖,快速排序算法中的關鍵步驟,即划分函數的實現如下:
* 具體划分過程:以數組的最后一個元素,即位於
* 位置r處的元素v作為參考,
* 最終將v放到正確的位置:其左邊的元素小於v,
* 其右邊的元素大於v
*/
public int partition(ITEM a[], int l, int r) throws InterruptedException{
int i = l - 1, j = r; ITEM v = a[r];
for(;;){
// 先從左向右逐個比較
while(a[++i].compareTo(v) < -1);
// 再從右向左逐個比較
while(v.compareTo(a[--j]) < -1)
// 從右向左逐個比較的停止條件
if(j == l)
break;
// 函數的循環停止條件
if(i >= j) break;
// 將第i個元素和第j個元素交換
exch(a, i, j);
}
// 循環停止后,將r處的元素放置正確的位置i
exch(a, i, r);
return i;
}
上面介紹的是快速排序的遞歸實現方式,具體實現如下:
* 下標;
*/
public void quicksort(ITEM[] a, int l, int r) throws InterruptedException{
if(r <= l) return;
// 划分數組
int i = partition(a, l, r);
// 遞歸調用快速排序
quicksort(a, l, i - 1);
quicksort(a, i + 1, r);
}
同樣以DATASET1為例,算法過程中數組各位置上的元素的動態變化為:

快速排序每次划分都至少確保一個位置上的元素的最終位置。從上圖過程3開始,可以看出數組中元素呈現明顯的“分簇”現象,其中分簇部分的元素就處於最終位置。從上圖中我們還可以明顯感知到快速排序算法是一種分支算法。在過程4以后的步驟中,算法分別對各個簇進行排序,即算法首先將整個數組分簇,然后再分別排序。
下面在觀察一種特殊順序的數組,並進一步認識快速排序的過程。DATASET2為整體降序排列的數組,如圖。


從DATASET2的排序過程可以看出,每次划分都有元素的位置被最終確定,並且將元素分簇,並在之后的操作中對每個分簇做進一步的排序。
.6.2 遞歸實現的復雜度
從程序實現過程可以看出,對長度為N的數組采用快速排序,在划分過程中,需要進行N+1次比較。下面討論幾種不同原始順序的數組采用快速排序算法的復雜度。
如果原始元素嚴格升序排列,選擇a[r]作為參照值時每次划分后所得的划分點都處於數組尾部,那么排序結束時總比較次數:
(N+1)+(N)+(N-1)+… +1= (N+2)(N+1)/2
如果原始元素嚴格降序排列,選擇a[r]作為參照值是每次划分后所得的划分點都處於數組首部,同樣需要約N2/2次比較。
快速排序最好的情況是每次划分都將所有元素分為兩半,這種情況下快速排序的比較時間滿足遞歸式:
CN = 2 CN/2 + N
其中2 CN表示對兩個子數組進行排序的開銷,N為划分過程中的比較次數,則可以得到遞歸解為:
CN = 2(2 CN/4 + N/2) + N = 22 CN/4 + 2N
= 22 (2 * CN/8 + N/4) + 2N = 23 CN/8 + 3N
= … = N/2 C2 + log2(N/2)N
其中C2=3,所以CN ≈Nlog2(N)
下面分析快速排序的平均比較次數。對於隨機有序的不同元素進行排序時,所使用的比較此時可以用遞歸公式表示成:
CN = N + 1 + 1/N [∑1≤k≤N(Ck-1+CN-k)], N≥2
其中N+1表示比較次數;其余項表示子數組的平均開銷,我們可以認為每個元素k可以稱為划分元素的概率為1/N,按這個元素划分后,得到的兩個隨機文件大小為k-1和N-k。
對累加式∑1≤k≤N(Ck-1+CN-k),具有對稱性,即C0+C1+…+CN-1 = CN-1+CN-2+…+C0。所以,我們得到
CN = N + 1 + 2/N [∑1≤k≤N(Ck-1)],
等式兩端同時乘以N得到
N CN = N(N+1) + 2[∑1≤k≤N(Ck-1)],帶入N-1得到:
(N - 1) CN-1 = (N - 1)N + 2[∑1≤k≤N-1(Ck-1)],
兩式相減得到
N CN – (N + 1)CN-1 = 2 N,
即
CN/(N+1) = CN-1 / N + 2/(N+1) = CN-2 / (N - 1) + 2/N + 2/(N + 1)
= CN-3 / (N - 2) + 2/(N-1) + 2/N + 2/(N + 1)
= … = C2 / 3 + 2/(N+1) + 2/N + … + 2/4
≈ 2 lnN
所以CN ≈2(N+1)lnN ≈ 1.39Nlog2N.
從近似結果可以發現,平均比較次數約比最少比較次數多出39%。
.6.3 棧的大小
對於可以使用遞歸實現的算法,一般也都可以借助堆棧結構用迭代的方法實現。對一個隨機文件來說,棧的大小與logN成正比,但在退化的情況下,棧的大小與N成正比。
利用堆棧數據結構,首先將數組的左右邊界壓棧,在每次划分后,將划分后得到的兩個子數組的邊界分別壓棧。這里首先將划分點p右邊子數組的邊界壓棧,然后將划分點左邊子數組的邊界壓棧。實現如下:
// 創建棧數據結構
LinkStack S = new LinkStack();
// 將數組最左端下標壓棧
S.push( new ElemItem<Integer>(l));
// 將數組最右端下標壓棧
S.push( new ElemItem<Integer>(r));
// 打印棧中元素個數
System.out.print(S.getSize() + " ");
// 迭代過程
while(S.getSize() > 0){
// 彈出棧頂元素(右端下標)
r = ((Integer)(S.pop().elem)).intValue();
// 彈出棧頂元素(左端下標)
l = ((Integer)(S.pop().elem)).intValue();
System.out.print(S.getSize() + " ");
// 如果彈出的右端下標小於左端下標,跳過此次循環
if(r <= l) continue;
// 將左端下標到右端下標之間的元素進行划分
int i = partition(a, l, r);
// 將i右邊兩端位置壓棧
S.push( new ElemItem<Integer>(i+1));
S.push( new ElemItem<Integer>(r));
// 將左邊兩端位置壓棧
S.push( new ElemItem<Integer>(l));
S.push( new ElemItem<Integer>(i-1));
// 打印出棧中元素個數
System.out.print(S.getSize() + " ");
}
System.out.println();
}
首先考慮隨機順序的DATASET1,記錄得到的堆棧大小的變化如下圖。注意,這里棧的大小為遞歸深度的兩倍,因為每次壓棧是將子文件的首位分別壓棧,即棧的大小會增加2:

對於元素順序整體趨於降序的DATASET2,堆棧大小變化如圖。

對於順序嚴格為降序的數組,則每次划分點依次為1,2,3,…其遞歸深度將逐漸加深,如圖。

從上面三個例子可以發現,對不同順序的輸入文件排序將會導致不同的迭代深度,在退化的情況下,棧增長到的大小與N成正比。對於較大的輸入文件,這樣的開銷上界可能會導致遞歸深度過深,甚至程序崩潰。
可以采用一種策略來減小堆棧大小的上界。在每次划分后檢查兩個子文件的大小,並把較大的子文件先壓到棧中。這樣處理時排序過程中元素處理的順序與之前算法有所不同,但不會影響算法的時間開銷。這種策略下,最壞情況下的棧的大小必須小於TN,滿足遞歸式TN=T[N/2]+2(T1=T0=0).可以計算得棧中元素個數不會超過2log2N。
根據這一策略,代碼實現如下:
public void quicksort2_2(ITEM[] a, int l, int r) throws InterruptedException{
// 創建棧數據結構
LinkStack S = new LinkStack();
// 將數組最左端下標壓棧
S.push( new ElemItem<Integer>(l));
// 將數組最右端下標壓棧
S.push( new ElemItem<Integer>(r));
// 打印棧中元素個數
System.out.print(S.getSize() + " ");
// 迭代過程
while(S.getSize() > 0){
// 彈出棧頂元素(右端下標)
r = ((Integer)(S.pop().elem)).intValue();
// 彈出棧頂元素(左端下標)
l = ((Integer)(S.pop().elem)).intValue();
System.out.print(S.getSize() + " ");
// 如果彈出的右端下標小於左端下標,跳過此次循環
if(r <= l) continue;
// 將左端下標到右端下標之間的元素進行划分
int i = partition(a, l, r);
// 如果i右邊的元素個數更少,先將其左邊兩端位置壓棧
if(i - l > r - i){
S.push( new ElemItem<Integer>(l));
S.push( new ElemItem<Integer>(i - 1));
}
// 將i右邊兩端位置壓棧
S.push( new ElemItem<Integer>(i+1));
S.push( new ElemItem<Integer>(r));
// 如果i右邊的元素個數更多,后將左邊兩端位置壓棧
if(r - i >= i - l){
S.push( new ElemItem<Integer>(l));
S.push( new ElemItem<Integer>(i-1));
}
// 打印出棧中元素個數
System.out.print(S.getSize() + " ");
}
System.out.println();
// for(int i = l; i <= r; i++) System.out.print(a[i] + " ");
}
對於大小為200的待排序文件,棧的最大深度不大於2log2200=15.2877,取值為16。對幾個數據集進行測試,並驗證這個結果。如下圖,與上面的圖比較可以發現,遞歸深度降低一半,並且都小於16。對於退化的情形,遞歸深度呈現周期性,范圍在0~4之間。

.6.4 快速排序改進
對快速排序的改進的思路總是圍繞如何使每次划分的位置位於所有數據的中間位置。有幾種方法可以得到這樣的效果。避免出現最壞情況的一種有效的算法是從數組中選擇一個隨機元素作為划分操作的參照元素。這樣出現最壞情況的可能性將非常小。這是一種概率算法。不過這種方法需要設計簡單又有效的隨機數產生方法。
另一種找到一個更好的划分的方法是從文件中選出3個元素,然后選出3個元素的中值作為划分參考值。通過從數組的左邊、中間和右邊選出3個元素,對這3個元素進行排序,然后把中間元素與a[r-1]進行交換,然后隨a[l+1],…,a[r-2]運行划分算法。這種改進方法稱為三者取中法。
三者取中法在以下幾個方面有助於快速排序的效率提高:最壞情況在實際排序中更不可能出現。對於要花N2時間的排序,對於所檢查的3個元素,必須有2個是文件中最小元素或最大元素,並且在大多數划分中都必須如此。其次,三者取中方法與小文件截斷法結合可以使快速排序算法的運行時間比單純地遞歸實現改進20%左右。
首先三者取中法的實現如下:
* 小文件的閾值為M,小於M的小文件被快速排序忽略;
* 主要改進思想,忽略了過小的文件,這樣可以減小棧的深度;
* 基於三者取中法的划分過程能使划分的位置更接近數組的中央,
* 這樣使得兩邊的元素個數比較平衡。
*/
private final static int M = 10;
public void quicksort3(ITEM[] a, int l, int r) throws InterruptedException{
// 創建棧
LinkStack S = new LinkStack();
// 先后將數組左右端的位置壓棧
S.push( new ElemItem<Integer>(l));
S.push( new ElemItem<Integer>(r));
// 打印棧中元素的個數
System.out.print(S.getSize() + " ");
// 循環迭代
while(S.getSize() > 0){
// 彈出棧頂元素(數組右端下標)
r = ((Integer)(S.pop().elem)).intValue();
// 彈出棧頂元素(數組左端下標)
l = ((Integer)(S.pop().elem)).intValue();
// 打印棧中元素個數
System.out.print(S.getSize() + " ");
// 如果右端下標小於左端下標,跳出此次循環
if(r - l <= 0) continue;
/* 下面4行代碼的作用是最左端、中央、最右端三個
* 元素進行取中數的過程,中央位置上的元素最后
* 存放至位置r-1
*/
exch(a, (l + r) / 2, r - 1);
compExch(a, l, r - 1);
compExch(a, l, r);
compExch(a, r - 1, r);
// 如果此時小文件的大小小於M,忽略對其的排序
if(r - l <= M) continue;
/* 對l+1到r-1之間的元素進行划分,
* 由於進行了取中數操作,r-1位置上的元素一定是大於l位置上
* 的元素,同時小於r位置上的元素,所以划分的是l+1~r-1之間
* 的元素。
*/
int i = partition(a, l + 1, r - 1);
// 如果i右邊的元素個數更少,先將其左邊兩端位置壓棧
if(i - l > r - i){
S.push( new ElemItem<Integer>(l));
S.push( new ElemItem<Integer>(i-1));
}
// 將i右邊兩端位置壓棧
S.push( new ElemItem<Integer>(i+1));
S.push( new ElemItem<Integer>(r));
// 如果i右邊的元素個數更多,后將左邊兩端位置壓棧
if(r - i >= i - l){
S.push( new ElemItem<Integer>(l));
S.push( new ElemItem<Integer>(i-1));
}
System.out.print(S.getSize() + " ");
}
}
本算法是三者取中法的迭代實現,並且其中設定了小文件截斷。所謂小文件截斷指的是在子文件大小小於設定值M時,則停止對小文件的繼續迭代。這里通過語句
if(r - l <= M) continue;
來截斷過深迭代。這里的M是一個參數,它的確切值取決於具體實現,可以通過嘗試性試驗來確定該值的大小,一般的范圍為5~25之間。過大的M值會影響快速排序的優勢,過小的M值會導致過深的迭代,從而也會影響算法的效率,如圖。本程序中選擇的M值為10。
三者去中過程由以下四行代碼完成:
compExch(a, l, r - 1);
compExch(a, l, r);
compExch(a, r - 1, r);
首先將數組中間位置上的元素交換至r-1位置,然后連續進行三次判斷交換,最后滿足一下條件a[l]≥a[r-1],a[l]≥a[r],a[r-1]≥a[r],最終滿足關系:
a[l] ≥a[r-1] ≥a[r],
則此時a[r-1]為原數組中最左邊、中間和最右邊3個元素的中值。以a[r-1]作為划分參照值,調用函數partition(a, l + 1, r - 1)。
到這里位置算法還沒有完成,因為子文件迭代截止的緣故,排序結果只是“整體有序”,如圖。

對於整體有序的數組,只需要再做局部調整便可以完成最終排序,可以通過插入排序來完成。所以結合三者取中快速排序、子文件截取和插入排序從而形成混合排序法,其實現如下:
* 如果M > 1,基於取中划分法的快速排序后得到的數組不是完全有序的,
* 只是“基本”有序的;最后還需要借助於插入排序完成最終的排序。
* */
public void hybridsort(ITEM[] a, int l, int r) throws InterruptedException{
// 基於取中法划分的快速排序
quicksort3(a, l, r);
// 插入排序
insertion(a, l, r);
}
分別對DATASET1, DATASET2和退化的序列分別調用混合排序算法,其迭代深度分別為如圖。在.6.2中我們采用了一定的策略使得遞歸堆棧大小控制在log2N量級,從DATASET1和DATASET2的排序結果可以看出迭代深度均值約為5,這里的混合算法的迭代深度約為4,可以發現迭代深度有一定的減小。

.7 歸並排序
在前一節中我們研究了快速排序以及相關的改進方法。本節將介紹另一種基於歸並(split-merge)過程的算法。歸並算法運用了分治算法和自底向上方法的思想。
在快速排序法文件划分為兩個子文件,其中划分點的位置被最終確定;而歸並排序的過程與之相反,它是將兩個分別排好序的算法合並成一個有序的文件。如果都用遞歸方式來描述這兩個方法,它們的差異將很明顯:
Partition into subfile1 and subfile2 Quicksort subfile1 Quicksort subfile2 |
Mergesort subfile1 Mergesort subfile2 Merge subfile1 and subfile2 |
歸並排序有一個很大特性,其算法的時間始終與Nlog2N成正比,並且與輸入的原始文件的順序無關。另外,歸並排序時穩定的排序,而快速排序和下一節將介紹的堆排序時間復雜度也與Nlog2N成正比,但是這兩個算法都不是穩定的。
.7.1 兩路歸並
給定兩個已經排好序的文件subfile1和subfile2,可以把他們合並成一個有序的輸出文件file。掃描兩個輸入文件的首位元素,取出較小的元素輸出到file,如圖;這樣不斷循環直到其中至少一個文件中的元素都被取到file中為止,最后將剩余的元素一起放置到file的,如圖。

以下為兩個輸入子文件,記為文件a和b,將這兩個子文件進行歸並,其結果保存到數組c中。
文件a: |
a |
c |
e |
g |
i |
k |
m |
o |
q |
s |
u |
w |
y |
文件b: |
b |
d |
f |
h |
j |
l |
m |
p |
r |
按照上面介紹的過程,對這兩個文件進行歸並,以下是歸並過程中選擇的輸出元素的來源以及所在位置。可以看到,文件b先結束輸出,然后將a中剩余的元素s, u, w, y直接添加到數組c中。
a: c, e, g, i, k, m, o, q, s, u, w, y,
b: b, d, f, h, j, l, m, p, r,
choose b @0
b: d, f, h, j, l, m, p, r,
a: c, e, g, i, k, m, o, q, s, u, w, y,
choose a @1
a: e, g, i, k, m, o, q, s, u, w, y,
b: d, f, h, j, l, m, p, r,
choose b @1
b: f, h, j, l, m, p, r,
a: e, g, i, k, m, o, q, s, u, w, y,
choose a @2
a: g, i, k, m, o, q, s, u, w, y,
b: f, h, j, l, m, p, r,
choose b @2
b: h, j, l, m, p, r,
a: g, i, k, m, o, q, s, u, w, y,
choose a @3
a: i, k, m, o, q, s, u, w, y,
b: h, j, l, m, p, r,
choose b @3
b: j, l, m, p, r,
a: i, k, m, o, q, s, u, w, y,
choose a @4
a: k, m, o, q, s, u, w, y,
b: j, l, m, p, r,
choose b @4
b: l, m, p, r,
a: k, m, o, q, s, u, w, y,
choose a @5
a: m, o, q, s, u, w, y,
b: l, m, p, r,
choose b @5
b: m, p, r,
a: m, o, q, s, u, w, y,
choose b @6
b: p, r,
a: m, o, q, s, u, w, y,
choose a @6
a: o, q, s, u, w, y,
b: p, r,
choose a @7
a: q, s, u, w, y,
b: p, r,
choose b @7
b: r,
a: q, s, u, w, y,
choose a @8
a: s, u, w, y,
b: r,
choose b @8
b:
a: s, u, w, y,
choose a @9
a: u, w, y,
choose a @10
a: w, y,
choose a @11
a: y,
choose a @12
a:
歸並過程的代碼實現如下:
public void merge2ways(ITEM[] c, int cl,
ITEM[] a, int al, int ar,
ITEM[] b, int bl, int br,
int flag){
int i = al, j = bl, cr = cl + ar - al + br - bl + 1;
try{
for( int k = cl; k <= cr; k++){
// 如果a中元素全被取走,將b中元素直接添加到c中
if(i > ar){
if(flag % 2 == 1) c[k] = b[j++];
else cpyVal(c, k, b[j++]);
continue;
}
// 如果b中原元素全被取走,將a中元素直接添加到c中
if(j > br){
if(flag % 2 == 1 ) c[k] = a[i++];
else cpyVal(c, k, a[i++]);
continue;
}
// 直接比較,a[i]和b[j]並將較小的元素存放入c[k]中
// c[k] = (a[i].compareTo(b[j]) == -1)?a[i++]:b[j++];
try{
if(a[i].compareTo(b[j]) < 0){
if(flag % 2 == 1) c[k] = a[i++];
else cpyVal(c, k, a[i++]);
}
else {
if(flag % 2 == 1) c[k] = b[j++];
else cpyVal(c, k, b[j++]);
}
} catch(Exception e){
e.printStackTrace();
}
}
} catch(Exception e){e.printStackTrace();}
}
代碼中有一個輔助變量flag,主要用於決定向輸出文件c中添加元素的方法:
l 如果flag為1,則直接通過賦值操作完成;
l 如果flag為0,則通過cpyVal函數完成,這樣可以在圖形界面上顯示元素位置的動態變化情況。
另外,程序代碼中沒有對數組c的大小做判斷,如果c的大小不夠大,無法將輸入的兩個文件中的所有元素都存放進去,則程序會出錯。所以在調用本函數時,用戶需要保證數組c的容量足夠大。cr表示的是c數組中的最右端位置。
這里函數的入參中輸入文件為a和b,事實上,a和b可以為同一個數組A,al和ar表示數組A的前半部分,bl和br表示數組A的后半部分,在數組前半部分和后半部分的分別有序的情況,可以調用本函數
merge2ways(c, 0, A, al, ar, A, bl, br, flag),
從而將數組A的排序結果存放至數組c。
細心的的讀者可能已經發現,程序中組要數組c來存放輸出,這通常是比較大的空間開銷。最好的方法是原地排序防范,在不適用大量額外空間、只通過元素之間的移位來完成排序。