快速排序的過程十分簡潔明瞭,就是將待排序元素序列進行切分,然後分支遞歸。以數組從小到大排序爲例,現將快速排序算法中切分數組獨立出來作爲一個方法partition來說明。
獨立partition方法出來之後的quick_sort的代碼如下:
void quick_sort(int array[], int low, int high)
{
if(low>= high)
return;
int index = partition(array, low, high);
quick_sort(array, low, index-1);
quick_sort(array, index+1, high);
}
常見的切分思想則有有兩種,下面就兩種不同的思想來討論之:
第一種思想是自頭向尾遍歷數組,將小於樞紐值的元素插入到數組前段,其代碼爲:
int partion(int array[], int low, int high)
{
int index = low;
int pivot = array[high];
for(int i = low; i < high; i++)
{
if(array[i] <= pivot)
{
swap(&array[i], &array[index++]);
}
}
swap(&array[high], &array[index]);
return _low;
}
這個切分數組的思想很簡單,就是掃描數組,將比pivot小的元素依次置入到相應的位置。個人感覺這個方法雖然容易理解和實現,但交換的次數似乎多了那麼一點....
第二種思想則是頭尾同時掃描數組,當然這種思想倒也有兩種不同的實現方式,我們也會在下面看到這兩種略有差別的實現方式。
方式一,代碼如下:
int partion(int array[], int low, int high)
{
int pivot = array[low];
int _low = low;
int _high = high;
while(_low < _high)
{
while(_low<_high && array[_low] <= pivot)
{
_low++;
}
while(_low<_high && array[_high] >= pivot)
{
_high--;
}
swap(&array[_low], &array[_high]);
}
swap(&array[_low],&array[low]);
return _low;
}
如果十分熟悉快速排序的人恐怕會一眼就可以看出來上述代碼是錯誤的(那麼這些朋友們應該可以跳過這麼一大段了)。這裏我們也要討論的就是這個問題,pivot的取值與先_high--還是先_low++的順序有聯繫嗎?起初,我也認爲這個決策的先後順序對排序結果應該是沒有什麼影響的,草草瞭解了下其思想就匆匆帶着代碼上陣了,代碼實現倒是很快,但糾結了很長時間愣是找不出來錯在哪兒。調試了半天,弄得焦頭爛額,最終決定坐下來好好分析一下,這才發現原來這個順序還真是起着決定性的作用的。那麼究竟問題出在哪裏呢?問題就出來快速排序的思想並沒掌握好吧(如果你也說不出個爲什麼順序存在的意義的話)
以上面代碼爲例,當給定一數組[4,3,2,1,5]。開始時,_low指向的是元素4,_high指向的元素是5;在循環結束還未執行最後的swap時,_low指向的元素是5,_high指向的元素也是5,此時數組的狀態是[4,3,2,1,5],而在swap之後,則是[5,3,2,1,4]。啊,竟然切片不成功?!
相反,我再把_high--與_low++的順序交換一下,得到如下代碼:
int partion(int array[], int low, int high)
{
int pivot = array[low];
int _low = low;
int _high = high;
while(_low < _high)
{
while(_low<_high && array[_high] >= pivot)
{
_high--;
}
while(_low<_high && array[_low] <= pivot)
{
_low++;
}
swap(&array[_low], &array[_high]);
}
swap(&array[_low],&array[low]);
return _low;
}
再給定同樣的輸入,卻得到的是正確的結果。開始時,_low指向的是元素4,_high指向的元素是5;在循環結束還未進行最後的swap時,_low指向的元素是1,_high指向的元素也是1,此時數組的狀態是[4,3,2,1,5],而在swap之後,則是[1,3,2,4,5]。現在可算是切片成功了!
恩,至此也基本說明了pivot值的設定與_high--和_low++的先後順序存在密切聯繫。(可悲的是,我找了好些本書,還真沒有哪一本書上明確的說明了這個問題,都是直接給出僞碼或源碼了事,或許這個簡單的問題還真算不上什麼問題吧)
不過,針對前一個代碼片段你可能會說執行 swap(&array[_low-1],&array[low]);不就行了?OK,那你可以先試試[4,3,2,1]再說吧。
好了,現在來分析一下爲何pivot取值會導致其後代碼的順序不同呢。讓我們還是以pivot=array[low]爲例來說明吧。
首先我們要確定的是_low與_high最終會相等(這可是循環結束的條件啊),_low++的目的是尋找第一個比pivot大的元素,_high--則是尋找第一個比pivot小的元素。在輸出的正確結果中,_low/_high左邊的元素會小於等於_low/_high所指向的元素,而其右邊的元素則大於等於它。往回退一步到執行最後一個swap之前,_low/_high左邊的元素除了pivot(即array[low])外均小於等於pivot,右邊則是全都大於等於pivot。但array[_low]與pivot相比呢?因爲它馬上就要和array[low]交換了,所以其必須小於等於pivot。但在先執行_low++再執行_high--的情況下,上述給出的兩個數組[4,3,2,1,5]和[4,3,2,1]中的情況則截然相反,第一個數組中是array[_low]大,第二個數組中是array[_high]小。因爲這裏我們總是先讓_low++盡力去尋找第一個比pivot大的元素,然後纔會輪到_high--去尋找第一個比pivot小的元素,但我們這裏需要保證的是在執行最後一個swap時,array[_low/_high]必須得比pivot小。因此在這種情況下,當_low與_high相遇時,_high就被動了,_high不能保證此時array[_low/_high]一定不比pivot大,要不swap就沒有任何意義了。只有讓_high佔到主動才能達到我們的目的,此時就得先執行_high--了。當然,若設置pivot爲array[high],則顯然得先進行_low++。
在方式二中,問題就顯得沒那麼複雜了:
int partion(int array[], int low, int high)
{
int pivot = array[low];
int _low = low;
int _high = high;
while(_low < _high)
{
while(_low<_high && array[_high] >= pivot)
{
_high--;
}
array[_low] = array[_high];
while(_low<_high && array[_low] <= pivot)
{
_low++;
}
array[_high] = array[_low];
}
swap(&array[_low],&array[low]);
return _low;
}
這個顯然順尋一眼就可以判斷出來,壓根就沒有混淆性。而且其執行起來效率更高(如果完全不考慮編譯器優化的話)。
最後以簡單的表格來總結一下吧
pivot = array[low] |
先_low++後_high-- |
ⅹ |
pivot = array[low] |
先_high--後_low++ |
√ |
pivot = array[high] |
先_low++後_high-- |
√ |
pivot = array[high] |
先_high--後_low++ |
ⅹ |
PS:在隨機化版本的快速排序中,也只是將隨機選取的樞紐值與array[low]或array[high]交換一下,最後所要面對的問題也還是一樣的。
當然,這兩種思想下數組切分的還是很有區別的,雖然感覺上第一種要好實現和好理解一點,但按我的理解,還是第二種思想要高效一點。其原因有二:
一. 第一種數組切分要進行的交換實在是太多了(相對來說),大約爲第二種數組切分方式的兩倍。(但時間複雜度都是O(n))
二. 另外感覺第一種數組切分情況下,在最終結果中與pivot相等的數要麼全都集中到左子數組中(如果採用<=來比較)要麼全都集中到右子數組中(如果採用<來比較);而在第二種切分數組方式下,則可以混合搭配<=,<與>=,>,能將與之相等的元素更好的“均勻”分配到兩個子數組中,可以更好的貫徹分治的思想。
不得不感嘆算法的奇妙之處,有一個簡單的快速排序也還是有那麼些細節是極其需要注意的。同時也不得不意識到理解算法的原理與能夠轉換成代碼來實現還是很有區別的,只有自己獨立的做了纔會真正意識到自己掌握還是沒掌握….