A. 常用的數據排序演算法有哪些,各有什麼特點舉例結合一種排序演算法並應用數組進行數據排序。
排序簡介
排序是數據處理中經常使用的一種重要運算,在計算機及其應用系統中,花費在排序上的時間在系統運行時間中佔有很大比重;並且排序本身對推動演算法分析的發展也起很大作用。目前已有上百種排序方法,但尚未有一個最理想的盡如人意的方法,本章介紹常用的如下排序方法,並對它們進行分析和比較。
1、插入排序(直接插入排序、折半插入排序、希爾排序);
2、交換排序(起泡排序、快速排序);
3、選擇排序(直接選擇排序、堆排序);
4、歸並排序;
5、基數排序;
學習重點
1、掌握排序的基本概念和各種排序方法的特點,並能加以靈活應用;
2、掌握插入排序(直接插入排序、折半插入排序、希爾排序)、交換排序(起泡排序、快速排序)、選擇排序(直接選擇排序、堆排序)、二路歸並排序的方法及其性能分析方法;
3、了解基數排序方法及其性能分析方法。
排序(sort)或分類
所謂排序,就是要整理文件中的記錄,使之按關鍵字遞增(或遞減)次序排列起來。其確切定義如下:
輸入:n個記錄R1,R2,…,Rn,其相應的關鍵字分別為K1,K2,…,Kn。
輸出:Ril,Ri2,…,Rin,使得Ki1≤Ki2≤…≤Kin。(或Ki1≥Ki2≥…≥Kin)。
1.被排序對象--文件
被排序的對象--文件由一組記錄組成。
記錄則由若干個數據項(或域)組成。其中有一項可用來標識一個記錄,稱為關鍵字項。該數據項的值稱為關鍵字(Key)。
注意:
在不易產生混淆時,將關鍵字項簡稱為關鍵字。
2.排序運算的依據--關鍵字
用來作排序運算依據的關鍵字,可以是數字類型,也可以是字元類型。
關鍵字的選取應根據問題的要求而定。
【例】在高考成績統計中將每個考生作為一個記錄。每條記錄包含准考證號、姓名、各科的分數和總分數等項內容。若要惟一地標識一個考生的記錄,則必須用"准考證號"作為關鍵字。若要按照考生的總分數排名次,則需用"總分數"作為關鍵字。
排序的穩定性
當待排序記錄的關鍵字均不相同時,排序結果是惟一的,否則排序結果不唯一。
在待排序的文件中,若存在多個關鍵字相同的記錄,經過排序後這些具有相同關鍵字的記錄之間的相對次序保持不變,該排序方法是穩定的;若具有相同關鍵字的記錄之間的相對次序發生變化,則稱這種排序方法是不穩定的。
注意:
排序演算法的穩定性是針對所有輸入實例而言的。即在所有可能的輸入實例中,只要有一個實例使得演算法不滿足穩定性要求,則該排序演算法就是不穩定的。
排序方法的分類
1.按是否涉及數據的內、外存交換分
在排序過程中,若整個文件都是放在內存中處理,排序時不涉及數據的內、外存交換,則稱之為內部排序(簡稱內排序);反之,若排序過程中要進行數據的內、外存交換,則稱之為外部排序。
注意:
① 內排序適用於記錄個數不很多的小文件
② 外排序則適用於記錄個數太多,不能一次將其全部記錄放人內存的大文件。
2.按策略劃分內部排序方法
可以分為五類:插入排序、選擇排序、交換排序、歸並排序和分配排序。
排序演算法分析
1.排序演算法的基本操作
大多數排序演算法都有兩個基本的操作:
(1) 比較兩個關鍵字的大小;
(2) 改變指向記錄的指針或移動記錄本身。
注意:
第(2)種基本操作的實現依賴於待排序記錄的存儲方式。
2.待排文件的常用存儲方式
(1) 以順序表(或直接用向量)作為存儲結構
排序過程:對記錄本身進行物理重排(即通過關鍵字之間的比較判定,將記錄移到合適的位置)
(2) 以鏈表作為存儲結構
排序過程:無須移動記錄,僅需修改指針。通常將這類排序稱為鏈表(或鏈式)排序;
(3) 用順序的方式存儲待排序的記錄,但同時建立一個輔助表(如包括關鍵字和指向記錄位置的指針組成的索引表)
排序過程:只需對輔助表的表目進行物理重排(即只移動輔助表的表目,而不移動記錄本身)。適用於難於在鏈表上實現,仍需避免排序過程中移動記錄的排序方法。
3.排序演算法性能評價
(1) 評價排序演算法好壞的標准
評價排序演算法好壞的標准主要有兩條:
① 執行時間和所需的輔助空間
② 演算法本身的復雜程度
(2) 排序演算法的空間復雜度
若排序演算法所需的輔助空間並不依賴於問題的規模n,即輔助空間是O(1),則稱之為就地排序(In-PlaceSou)。
非就地排序一般要求的輔助空間為O(n)。
(3) 排序演算法的時間開銷
大多數排序演算法的時間開銷主要是關鍵字之間的比較和記錄的移動。有的排序演算法其執行時間不僅依賴於問題的規模,還取決於輸入實例中數據的狀態。
文件的順序存儲結構表示
#define n l00 //假設的文件長度,即待排序的記錄數目
typedef int KeyType; //假設的關鍵字類型
typedef struct{ //記錄類型
KeyType key; //關鍵字項
InfoType otherinfo;//其它數據項,類型InfoType依賴於具體應用而定義
}RecType;
typedef RecType SeqList[n+1];//SeqList為順序表類型,表中第0個單元一般用作哨兵
注意:
若關鍵字類型沒有比較算符,則可事先定義宏或函數來表示比較運算。
【例】關鍵字為字元串時,可定義宏"#define LT(a,b)(Stromp((a),(b))<0)"。那麼演算法中"a<b"可用"LT(a,b)"取代。若使用C++,則定義重載的算符"<"更為方便。
按平均時間將排序分為四類:
(1)平方階(O(n2))排序
一般稱為簡單排序,例如直接插入、直接選擇和冒泡排序;
(2)線性對數階(O(nlgn))排序
如快速、堆和歸並排序;
(3)O(n1+£)階排序
£是介於0和1之間的常數,即0<£<1,如希爾排序;
(4)線性階(O(n))排序
如桶、箱和基數排序。
各種排序方法比較
簡單排序中直接插入最好,快速排序最快,當文件為正序時,直接插入和冒泡均最佳。
影響排序效果的因素
因為不同的排序方法適應不同的應用環境和要求,所以選擇合適的排序方法應綜合考慮下列因素:
①待排序的記錄數目n;
②記錄的大小(規模);
③關鍵字的結構及其初始狀態;
④對穩定性的要求;
⑤語言工具的條件;
⑥存儲結構;
⑦時間和輔助空間復雜度等。
不同條件下,排序方法的選擇
(1)若n較小(如n≤50),可採用直接插入或直接選擇排序。
當記錄規模較小時,直接插入排序較好;否則因為直接選擇移動的記錄數少於直接插人,應選直接選擇排序為宜。
(2)若文件初始狀態基本有序(指正序),則應選用直接插人、冒泡或隨機的快速排序為宜;
(3)若n較大,則應採用時間復雜度為O(nlgn)的排序方法:快速排序、堆排序或歸並排序。
快速排序是目前基於比較的內部排序中被認為是最好的方法,當待排序的關鍵字是隨機分布時,快速排序的平均時間最短;
堆排序所需的輔助空間少於快速排序,並且不會出現快速排序可能出現的最壞情況。這兩種排序都是不穩定的。
若要求排序穩定,則可選用歸並排序。但本章介紹的從單個記錄起進行兩兩歸並的 排序演算法並不值得提倡,通常可以將它和直接插入排序結合在一起使用。先利用直接插入排序求得較長的有序子文件,然後再兩兩歸並之。因為直接插入排序是穩定的,所以改進後的歸並排序仍是穩定的。
4)在基於比較的排序方法中,每次比較兩個關鍵字的大小之後,僅僅出現兩種可能的轉移,因此可以用一棵二叉樹來描述比較判定過程。
當文件的n個關鍵字隨機分布時,任何藉助於"比較"的排序演算法,至少需要O(nlgn)的時間。
箱排序和基數排序只需一步就會引起m種可能的轉移,即把一個記錄裝入m個箱子之一,因此在一般情況下,箱排序和基數排序可能在O(n)時間內完成對n個記錄的排序。但是,箱排序和基數排序只適用於像字元串和整數這類有明顯結構特徵的關鍵字,而當關鍵字的取值范圍屬於某個無窮集合(例如實數型關鍵字)時,無法使用箱排序和基數排序,這時只有藉助於"比較"的方法來排序。
若n很大,記錄的關鍵字位數較少且可以分解時,採用基數排序較好。雖然桶排序對關鍵字的結構無要求,但它也只有在關鍵字是隨機分布時才能使平均時間達到線性階,否則為平方階。同時要注意,箱、桶、基數這三種分配排序均假定了關鍵字若為數字時,則其值均是非負的,否則將其映射到箱(桶)號時,又要增加相應的時間。
(5)有的語言(如Fortran,Cobol或Basic等)沒有提供指針及遞歸,導致實現歸並、快速(它們用遞歸實現較簡單)和基數(使用了指針)等排序演算法變得復雜。此時可考慮用其它排序。
(6)本章給出的排序演算法,輸人數據均是存儲在一個向量中。當記錄的規模較大時,為避免耗費大量的時間去移動記錄,可以用鏈表作為存儲結構。譬如插入排序、歸並排序、基數排序都易於在鏈表上實現,使之減少記錄的移動次數。但有的排序方法,如快速排序和堆排序,在鏈表上卻難於實現,在這種情況下,可以提取關鍵字建立索引表,然後對索引表進行排序。然而更為簡單的方法是:引人一個整型向量t作為輔助表,排序前令t[i]=i(0≤i<n),若排序演算法中要求交換R[i]和R[j],則只需交換t[i]和t[j]即可;排序結束後,向量t就指示了記錄之間的順序關系:
R[t[0]].key≤R[t[1]].key≤…≤R[t[n-1]].key
若要求最終結果是:
R[0].key≤R[1].key≤…≤R[n-1].key
則可以在排序結束後,再按輔助表所規定的次序重排各記錄,完成這種重排的時間是O(n)。
B. 冒泡排序法和快速排序比較的演算法
打你屁股,這么簡單的問題都不認真研究一下。
冒泡排序是最慢的排序,時間復雜度是 O(n^2)。
快速排序是最快的排序。關於快速排序,我推薦你看看《代碼之美》第二章:我編寫過的最漂亮的代碼。作者所說的最漂亮,就是指效率最高的。
--------------------------------摘自《代碼之美》---------------
當我撰寫關於分治(divide-and-conquer)演算法的論文時,我發現C.A.R. Hoare的Quicksort演算法(「Quicksort」,Computer Journal 5)無疑是各種Quicksort演算法的鼻祖。這是一種解決基本問題的漂亮演算法,可以用優雅的代碼實現。我很喜歡這個演算法,但我總是無法弄明白演算法中最內層的循環。我曾經花兩天的時間來調試一個使用了這個循環的復雜程序,並且幾年以來,當我需要完成類似的任務時,我會很小心地復制這段代碼。雖然這段代碼能夠解決我所遇到的問題,但我卻並沒有真正地理解它。
我後來從Nico Lomuto那裡學到了一種優雅的劃分(partitioning)模式,並且最終編寫出了我能夠理解,甚至能夠證明的Quicksort演算法。William Strunk Jr.針對英語所提出的「良好的寫作風格即為簡練」這條經驗同樣適用於代碼的編寫,因此我遵循了他的建議,「省略不必要的字詞」(來自《The Elements of Style》一書)。我最終將大約40行左右的代碼縮減為十幾行的代碼。因此,如果要回答「你曾編寫過的最漂亮代碼是什麼?」這個問題,那麼我的答案就是:在我編寫的《Programming Pearls, Second Edition》(Addison-Wesley)一書中給出的Quichsort演算法。在示例2-1中給出了用C語言編寫的Quicksort函數。我們在接下來的章節中將進一步地研究和改善這個函數。
【示例】 2-1 Quicksort函數
void quicksort(int l, int u)
{ int i, m;
if (l >= u) return; 10
swap(l, randint(l, u));
m = l;
for (i = l+1; i <= u; i++)
if (x[i] < x[l])
swap(++m, i);
swap(l, m);
quicksort(l, m-1);
quicksort(m+1, u);
}
如果函數的調用形式是quicksort(0, n-1),那麼這段代碼將對一個全局數組x[n]進行排序。函數的兩個參數分別是將要進行排序的子數組的下標:l是較低的下標,而u是較高的下標。函數調用swap(i,j)將會交換x[i]與x[j]這兩個元素。第一次交換操作將會按照均勻分布的方式在l和u之間隨機地選擇一個劃分元素。
在《Programming Pearls》一書中包含了對Quicksort演算法的詳細推導以及正確性證明。在本章的剩餘內容中,我將假設讀者熟悉在《Programming Pearls》中所給出的Quicksort演算法以及在大多數初級演算法教科書中所給出的Quicksort演算法。
如果你把問題改為「在你編寫那些廣為應用的代碼中,哪一段代碼是最漂亮的?」我的答案還是Quicksort演算法。在我和M. D. McIlroy一起編寫的一篇文章("Engineering a sort function," Software-Practice and Experience, Vol. 23, No. 11)中指出了在原來Unix qsort函數中的一個嚴重的性能問題。隨後,我們開始用C語言編寫一個新排序函數庫,並且考慮了許多不同的演算法,包括合並排序(Merge Sort)和堆排序(Heap Sort)等演算法。在比較了Quicksort的幾種實現方案後,我們著手創建自己的Quicksort演算法。在這篇文章中描述了我們如何設計出一個比這個演算法的其他實現要更為清晰,速度更快以及更為健壯的新函數——部分原因是由於這個函數的代碼更為短小。Gordon Bell的名言被證明是正確的:「在計算機系統中,那些最廉價,速度最快以及最為可靠的組件是不存在的。」現在,這個函數已經被使用了10多年的時間,並且沒有出現任何故障。
考慮到通過縮減代碼量所得到的好處,我最後以第三種方式來問自己在本章之初提出的問題。「你沒有編寫過的最漂亮代碼是什麼?」。我如何使用非常少的代碼來實現大量的功能?答案還是和Quicksort有關,特別是對這個演算法的性能分析。我將在下一節給出詳細介紹。
2.2 事倍功半
Quicksort是一種優雅的演算法,這一點有助於對這個演算法進行細致的分析。大約在1980年左右,我與Tony Hoare曾經討論過Quicksort演算法的歷史。他告訴我,當他最初開發出Quicksort時,他認為這種演算法太簡單了,不值得發表,而且直到能夠分析出這種演算法的預期運行時間之後,他才寫出了經典的「Quicksoft」論文。
我們很容易看出,在最壞的情況下,Quicksort可能需要n2的時間來對數組元素進行排序。而在最優的情況下,它將選擇中值作為劃分元素,因此只需nlgn次的比較就可以完成對數組的排序。那麼,對於n個不同值的隨機數組來說,這個演算法平均將進行多少次比較?
Hoare對於這個問題的分析非常漂亮,但不幸的是,其中所使用的數學知識超出了大多數程序員的理解范圍。當我為本科生講授Quicksort演算法時,許多學生即使在費了很大的努力之後,還是無法理解其中的證明過程,這令我非常沮喪。下面,我們將從Hoare的程序開
11
始討論,並且最後將給出一個與他的證明很接近的分析。
我們的任務是對示例2-1中的Quicksort代碼進行修改,以分析在對元素值均不相同的數組進行排序時平均需要進行多少次比較。我們還將努力通過最短的代碼、最短運行時間以及最小存儲空間來得到最深的理解。
為了確定平均比較的次數,我們首先對程序進行修改以統計次數。因此,在內部循環進行比較之前,我們將增加變數comps的值(參見示例2-2)。
【示例2-2】 修改Quicksort的內部循環以統計比較次數。
for (i = l+1; i <= u; i++) {
comps++;
if (x[i] < x[l])
swap(++m, i);
}
如果用一個值n來運行程序,我們將會看到在程序的運行過程中總共進行了多少次比較。如果重復用n來運行程序,並且用統計的方法來分析結果,我們將得到Quicksort在對n個元素進行排序時平均使用了1.4 nlgn次的比較。
在理解程序的行為上,這是一種不錯的方法。通過十三行的代碼和一些實驗可以反應出許多問題。這里,我們引用作家Blaise Pascal和T. S. Eliot的話,「如果我有更多的時間,那麼我給你寫的信就會更短。」現在,我們有充足的時間,因此就讓我們來對代碼進行修改,並且努力編寫出更短(同時更好)的程序。
我們要做的事情就是提高這個演算法的速度,並且盡量增加統計的精確度以及對程序的理解。由於內部循環總是會執行u-l次比較,因此我們可以通過在循環外部增加一個簡單的操作來統計比較次數,這就可以使程序運行得更快一些。在示例2-3的Quicksort演算法中給出了這個修改。
【示例2-3】 Quicksort的內部循環,將遞增操作移到循環的外部
comps += u-l;
for (i = l+1; i <= u; i++)
if (x[i] < x[l])
swap(++m, i);
這個程序會對一個數組進行排序,同時統計比較的次數。不過,如果我們的目標只是統計比較的次數,那麼就不需要對數組進行實際地排序。在示例2-4中去掉了對元素進行排序的「實際操作」,而只是保留了程序中各種函數調用的「框架」。
【示例2-4】將Quicksort演算法的框架縮減為只進行統計
void quickcount(int l, int u)
{ int m;
if (l >= u) return;
m = randint(l, u);
comps += u-l;
quickcount(l, m-1);
quickcount(m+1, u);
}
12
這個程序能夠實現我們的需求,因為Quichsort在選擇劃分元素時採用的是「隨機」方式,並且我們假設所有的元素都是不相等的。現在,這個新程序的運行時間與n成正比,並且相對於示例2-3需要的存儲空間與n成正比來說,現在所需的存儲空間縮減為遞歸堆棧的大小,即存儲空間的平均大小與lgn成正比。
雖然在實際的程序中,數組的下標(l和u)是非常重要的,但在這個框架版本中並不重要。因此,我們可以用一個表示子數組大小的整數(n)來替代這兩個下標(參見示例2-5)
【示例2-5】 在Quicksort代碼框架中使用一個表示子數組大小的參數
void qc(int n)
{ int m;
if (n <= 1) return;
m = randint(1, n);
comps += n-1;
qc(m-1);
qc(n-m);
}
現在,我們可以很自然地把這個過程整理為一個統計比較次數的函數,這個函數將返回在隨機Quicksort演算法中的比較次數。在示例2-6中給出了這個函數。
【示例2-6】 將Quicksort框架實現為一個函數
int cc(int n)
{ int m;
if (n <= 1) return 0;
m = randint(1, n);
return n-1 + cc(m-1) + cc(n-m);
}
在示例2-4、示例2-5和示例2-6中解決的都是相同的基本問題,並且所需的都是相同的運行時間和存儲空間。在後面的每個示例都對這些函數的形式進行了改進,從而比這些函數更為清晰和簡潔。
在定義發明家的矛盾(inventor's paradox)(How To Solve It, Princeton University Press)時,George Póllya指出「計劃越宏大,成功的可能性就越大。」現在,我們就來研究在分析Quicksort時的矛盾。到目前為止,我們遇到的問題是,「當Quicksort對大小為n的數組進行一次排序時,需要進行多少次比較?」我們現在將對這個問題進行擴展,「對於大小為n的隨機數組來說,Quichsort演算法平均需要進行多少次的比較?」我們通過對示例2-6進行擴展以引出示例2-7。
【示例2-7】 偽碼:Quicksort的平均比較次數
float c(int n)
if (n <= 1) return 0
sum = 0
for (m = 1; m <= n; m++)
sum += n-1 + c(m-1) + c(n-m)
return sum/n
如果在輸入的數組中最多隻有一個元素,那麼Quichsort將不會進行比較,如示例2-6
13
中所示。對於更大的n,這段代碼將考慮每個劃分值m(從第一個元素到最後一個,每個都是等可能的)並且確定在這個元素的位置上進行劃分的運行開銷。然後,這段代碼將統計這些開銷的總和(這樣就遞歸地解決了一個大小為m-1的問題和一個大小為n-m的問題),然後將總和除以n得到平均值並返回這個結果。
如果我們能夠計算這個數值,那麼將使我們實驗的功能更加強大。我們現在無需對一個n值運行多次來估計平均值,而只需一個簡單的實驗便可以得到真實的平均值。不幸的是,實現這個功能是要付出代價的:這個程序的運行時間正比於3n(如果是自行參考(self-referential)的,那麼用本章中給出的技術來分析運行時間將是一個很有趣的練習)。
示例2-7中的代碼需要一定的時間開銷,因為它重復計算了中間結果。當在程序中出現這種情況時,我們通常會使用動態編程來存儲中間結果,從而避免重復計算。因此,我們將定義一個表t[N+1],其中在t[n]中存儲c[n],並且按照升序來計算它的值。我們將用N來表示n的最大值,也就是進行排序的數組的大小。在示例2-8中給出了修改後的代碼。
【示例2-8】 在Quicksort中使用動態編程來計算
t[0] = 0
for (n = 1; n <= N; n++)
sum = 0
for (i = 1; i <= n; i++)
sum += n-1 + t[i-1] + t[n-i]
t[n] = sum/n
這個程序只對示例2-7進行了細微的修改,即用t[n]來替換c(n)。它的運行時間將正比於N2,並且所需的存儲空間正比於N。這個程序的優點之一就是:在程序執行結束時,數組t中將包含數組中從元素0到元素N的真實平均值(而不是樣本均值的估計)。我們可以對這些值進行分析,從而生成在Quichsort演算法中統計比較次數的計算公式。
我們現在來對程序做進一步的簡化。第一步就是把n-1移到循環的外面,如示例2-9所示。
【示例2-9】 在Quicksort中把代碼移到循環外面來計算
t[0] = 0
for (n = 1; n <= N; n++)
sum = 0
for (i = 1; i <= n; i++)
sum += t[i-1] + t[n-i]
t[n] = n-1 + sum/n
現在將利用對稱性來對循環做進一步的調整。例如,當n為4時,內部循環計算總和為:
t[0]+t[3] + t[1]+t[2] + t[2]+t[1] + t[3]+t[0]
在上面這些組對中,第一個元素增加而第二個元素減少。因此,我們可以把總和改寫為:
2 * (t[0] + t[1] + t[2] + t[3])
我們可以利用這種對稱性來得到示例2-10中的Quicksort。
【示例2-10】 在Quichsort中利用了對稱性來計算
t[0] = 0
14
for (n = 1; n <= N; n++)
sum = 0
for (i = 0; i < n; i++)
sum += 2 * t[i]
t[n] = n-1 + sum/n
然而,在這段代碼的運行時間中同樣存在著浪費,因為它重復地計算了相同的總和。此時,我們不是把前面所有的元素加在一起,而是在循環外部初始化總和並且加上下一個元素,如示例2-11所示。
【示例2-11】 在Quicksort中刪除了內部循環來計算
sum = 0; t[0] = 0
for (n = 1; n <= N; n++)
sum += 2*t[n-1]
t[n] = n-1 + sum/n
這個小程序確實很有用。程序的運行時間與N成正比,對於每個從1到N的整數,程序將生成一張Quicksort的估計運行時間表。
我們可以很容易地把示例2-11用表格來實現,其中的值可以立即用於進一步的分析。在2-1給出了最初的結果行。
表2-1 示例2-11中實現的表格輸出
N Sum t[n]
0 0 0
1 0 0
2 0 1
3 2 2.667
4 7.333 4.833
5 17 7.4
6 31.8 10.3
7 52.4 13.486
8 79.371 16.921
這張表中的第一行數字是用代碼中的三個常量來進行初始化的。下一行(輸出的第三行)的數值是通過以下公式來計算的:
A3 = A2+1 B3 = B2 + 2*C2 C3 = A2-1 + B3/A3
把這些(相應的)公式記錄下來就使得這張表格變得完整了。這張表格是「我曾經編寫的最漂亮代碼」的很好的證據,即使用少量的代碼完成大量的工作。
但是,如果我們不需要所有的值,那麼情況將會是什麼樣?如果我們更希望通過這種來方式分析一部分數值(例如,在20到232之間所有2的指數值)呢?雖然在示例2-11中構建了完整的表格t,但它只需要使用表格中的最新值。因此,我們可以用變數t的定長空間來替代table t[]的線性空間,如示例2-12所示。
【示例2-12】 Quicksoft 計算——最終版本
sum = 0; t = 0
15
for (n = 1; n <= N; n++)
sum += 2*t
t = n-1 + sum/n
然後,我們可以插入一行代碼來測試n的適應性,並且在必要時輸出這些結果。
這個程序是我們漫長學習旅途的終點。通過本章所採用的方式,我們可以證明Alan Perlis的經驗是正確的:「簡單性並不是在復雜性之前,而是在復雜性之後」 ("Epigrams on Programming," Sigplan Notices, Vol. 17, Issue 9)。