《程式設計珠璣》程式碼之路16:直觀感受為何程式設計師需要學習經典演算法
作為一名老年ACM菜雞,經常見到周圍除了調庫啥也不會的程式設計師,還經常一臉正經的說:“程式設計師就是把人家寫好的東西拿出來呼叫一下,頂多改改嘛。”。emmmm,在這個貓貓狗狗都能養活自己的年代,這麼想確實沒問題 ---- 如果你確定自己不用面臨被淘汰的風險,或者在別的領域能首屈一指。
大家應該都瞭解插入排序和快速排序。
快速排序在後期其實是由一塊一塊接近有序的小塊組成的陣列,遞迴下去計算小塊的成本其實挺大,如果在某個條件下停下來,就會得到一個接近有序的陣列。此時,如果用人鬼都覺得沒用的插入排序對整個中間陣列排序,整體的效率要比快排要高。因為插入排序雖然是平方級演算法,但它在陣列接近有序時,效能線性的。
能發現遞迴效率低下時停止遞迴過程,看見人鬼都看不上的插入排序,提升整個演算法的效率,這也許就是藝術了吧。
生活不止眼前的苟且,還有詩和遠方。
遠方和詩,不是調庫就能感受到的,就像很多人識字,卻感受不到詩歌的美。
有人程式設計只用腦,懂得程式設計之美的人,還知道要用心。
旅行如此,程式設計如此,戀愛如此,生活亦如此。
好了言歸正傳:
插入排序就是每次拿到一個數,給它插到合適的位置,emmmm,沒毛病。
先看一段插入排序的程式碼:
for (int i = 0; i < n; ++i){ int t = array[i]; for (int j = i; j > 0 && array[j - 1] > t; --j){ array[j] = array[j - 1]; } array[j] = t; }
至於快速排序大家也許都比較熟悉,就每次找一個支點,把陣列調整為支點左邊的都小於支點的值,右邊的都大於等於支點的值,然後用分治的思想,把比當前支點小的部分和比當前支點大的部分分而治之。
對於如何把一個數組分成大於支點和小於支點的兩部分,其實有很多方法,其中一個是,從右半部分找一個小於支點值的,然後從左半部分找一個大於支點值的,交換他們,不斷重複這個過程。
下面實現的演算法,實際已經要比直接呼叫C函式庫快3-4倍。
void qSort(int left, int right){ if (left >= right){ return; } int t = array[left], i = left, j = right + 1; while(1){ do{ i++; }while(i <= right && array[i] < t); do{ j--; }while(array[j] > t); if (i > j) break; swap(array[i], array[j]); } qSort(left, j - 1); qSort(j + 1, right); }
然而,簡單的把快速排序和插入排序結合,還能繼續讓整個排序快近30%!!!
可以想象快速排序的遞迴樹,最下面的幾層,其實都是在用很大的開銷算幾個數字。。。
那麼在某個時間段,比如rihgt - left <= cutoff 時,停止快排過程,然後呼叫插入排序。
cutoff的值因機器不盡相同,可以用不同的值在自己的電腦上測試,50左右是個不錯的選擇。
在快排上加個返回條件,應該沒啥難度吧。。
所以這個排序函式,變成了這個樣子:
sort(){
qsort();
insertSort();
}
在呼叫qsort後,緊跟著呼叫insertSort。
這樣一來,我們就得到了一個,比書上和C函式庫都要快的排序演算法。
並不是什麼時候,都有庫可調,更不是庫永遠都是最好的,不僅僅會用庫,還要超越庫,才是一個優秀的程式設計師該做的。