一次快速排序錯誤引發的思考

Chaobs發表於2015-10-30

快速排序是目前基於關鍵字的內部排序演算法中平均效能最好的,它採用了分治策略,這既是快速排序的優點也是它的缺點。從快速排序的演算法描述上我們可以發現它具有遞迴的結構:

  • (1)確定一個分界,將待排序的陣列分為左、右兩個部分;
  • (2)使所有小(大)於臨界值的資料移到左部分,大(小)於臨界值的資料移到右部分;
  • (3)這時左、右兩個部分成為了兩個獨立的陣列,分別對它們執行(1)(2)(3)的操作,直到所有資料都是有序的狀態為止。

照這樣的描述我們不難寫出快排的程式碼,我平時遇到排序的問題,只要資料量上了100,想都不想就用快排來解決,但是當我用下面這個程式測試時卻出現了問題:

我在Linux上執行這個程式出現了”Segmentation fault “錯誤,而當NUM==1000000時卻沒有這個錯誤。查閱相關資料得知這是由於程式遞迴次數太多,大量的壓棧使程式佔用的棧空間超過了作業系統所規定的大小,從而出現的記憶體錯誤。

我用ulimit -s指令的得到的結果是8192,也就是說我的系統預設給每個程式分配的大概是8M的棧空間。用指令ulimit -s unlimited使棧空間變成實際記憶體大小後,上面的程式就可以順利執行而不出錯誤了(因為Linux上不像Windows可以把棧的大小寫入可執行檔案中,所以只能用ulimit -s更改的方法了)。

難道因為棧的限制,快速排序能夠處理的資料量就有上限了嗎?那還不如用選擇排序——雖然慢,但至少不會出錯,於是我找到了這篇文章:快速排序的非遞迴實現。其實說是“非遞迴”,只不過是用自己管理的棧來消除遞迴,演算法本質上沒有區別,而且從這篇文章作者的測試來看,用棧的方法比用遞迴的方法反而更慢(作者將其解釋為:“用棧的效率比遞迴高,但是在這個程式中區域性變數也就是要每次壓棧的資料很少,棧的優勢體現不出來,反而更慢……”,我認為這種觀點是不對的,由於遞迴可以理解為有了一個“系統幫你自動管理的棧”,它的效率肯定是要比你自己管理的棧要高的,況且你在進行彈棧和壓棧操作時又呼叫了新函式,算上呼叫的開支,用棧的方法肯定比遞迴慢),不過棧在這裡的優勢是可以不用考慮作業系統的問題,而且能夠處理的資料量只和記憶體大小有關,不必受到作業系統對棧空間大小的限制(即使用棧,快排也比很多排序演算法要快得多)。

以前在學排序演算法的時候,專門有講怎樣根據實際問題來選擇合適的排序演算法,但是我圖“省事”,就只用快排和簡單選擇排序。遇到了這個問題也讓我對演算法的選擇和實現上有了更多認識,同時也瞭解到用棧消除遞迴在有些場合(比如系統棧空間受限)的重要意義。


前面我說到所謂的“非遞迴”快速排序演算法,不過是用棧來消除了遞迴,它的執行時間肯定比遞迴演算法長,我們不妨來實際實現一下。程式碼如下:

在程式碼的第110行至第122行的while迴圈中,做的正是用棧消除遞迴的工作。想想遞迴的演算法中,把劃分好的左右區間界限(即left,right)儲存到了系統管理的棧中,這裡手動把每次劃分出來的區間分界儲存至棧中,當第113和118行的兩個條件不滿足時,所在區間的元素都是有序的狀態,此時不進行壓棧操作而向前返回(即遞迴的回撥)。關於用棧消除遞迴的演算法可以參考關於資料結構的書籍,比如陳銳的《零基礎學資料結構》有關棧的那一章就有介紹。實際執行兩個程式的結果如下:

之所以只用了500個資料,是因為超過1000個資料後,非遞迴快排的速度就慢的令人難以忍受。下面是另外兩次關於遞迴演算法快排的測試:

這也印證了之前談到的系統預設限制帶來的問題。

相關文章