快速排序常見三種寫法
排序的基本知識
排序是很重要的,一般排序都是針對陣列的排序,可以簡單想象一排貼好了標號的箱子放在一起,順序是打亂的因此需要排序。
排序的有快慢之分,常見的基於比較的方式進行排序的演算法一般有六種。
- 氣泡排序(bubble sort)
- 選擇排序(selection sort)
- 插入排序(insertion sort)
- 歸併排序(merge sort)
- 堆排序(heap sort)
- 快速排序(quick sort)
前三種屬於比較慢的排序的方法,時間複雜度在\(O(n^2)\)級別。後三種會快一些。但是也各有優缺點,比如歸併排序需要額外開闢一段空間用來存放陣列元素,也就是\(O(n)\)的空間複雜度。
快速排序的三種實現
這裡主要說說快速排序,通常有三種實現方法:
- 順序法
- 填充法
- 交換法
下面的程式碼用java語言實現
可以用下面的測試程式碼,也可以參考文章底部的整體的程式碼。
public class Test { public static void main(String[] args) { int[] nums = {7,8,4,9,3,2,6,5,0,1,9}; QuickSort quickSort = new QuickSort(); quickSort.quick_sort(nums, 0, nums.length-1); System.out.println(Arrays.toString(nums)); } }
遞迴基本框架
所有的快速排序幾乎都有著相同的遞迴框架,先看下程式碼
public void quick_sort(int[] array, int start, int end) {
if(start < end){
int mid = partition(array, start, end);
quick_sort(array, start, mid-1);
quick_sort(array, mid+1, end);
}
}
程式碼有如下特點
- 因為快速排序是原地排序(in-place sort),所以不需要返回值,函式結束後輸入陣列就排序完成
- 傳入quick_sort函式的引數有陣列array,起始下標start和終止下標end。這樣方便對子陣列進行操作。
- 程式碼使用了分治(divide and conquer)的思想,並用遞迴來完成
單看遞迴的框架,思路其實很簡單。
- 利用partition函式在陣列中找到一個mid下標,通過移動元素,使得mid左邊的元素都比mid小,右邊的都比mid大。
- 遞迴地,對mid左邊和右邊的元素進行快速排序。
- 不斷進行下去,區間會越來越小,函式如果start==end說明區間只有一個元素,也就不用排序,這就是終止條件。
快速排序的副產品就是快速選擇演算法,因為partition函式實際上返回的mid值就是array[mid]
在已經排序的array裡面所處的順位。
因此真正的難點就落在了怎麼樣對陣列進行劃分了,首先介紹順序法
順序法
public int partition(int[] array, int start, int end) {
int firstHigh = start; // > pivot
int pivot = array[end];
for(int j = start; j < end; j++) {
if(array[j] <= pivot) {
swap(array, firstHigh, j);
firstHigh++;
}
}
swap(array, firstHigh, end);
return firstHigh;
}
2-3行是一些指標的定義,這裡選取最後一個元素作為主元(pivot),
因此任務也就變成了:調整陣列使得pivot左側的元素都比pivot小,右側的都比pivot大。
小於或者等於pivot | pivot | 大於pivot |
---|---|---|
array[start...mid-1] |
array[mid] |
array[mid+1...end] |
4-9行是迴圈體,代表著如何將給定範圍的陣列變成表格中的樣子,不斷迴圈最終做到上面表格中的樣子,其中firstHigh變數也大於pivot。
小於或者等於pivot | firstHigh | 大於pivot | unexplored |
---|---|---|---|
array[start...firstHigh-1] |
array[firstHigh] |
array[firstHigh...j-1] |
array[j...end] |
- 初開始迴圈
j=start
,整個陣列都是unexplored的狀態,firstHigh
也等於start。 - 逐個遍歷陣列元素與pivot比較,
- 如果
array[j] > pivot
,可知array[j]
處於pivot右邊的高區,容易知道j始終大於等於firstHigh
,此時不需要做額外操作,j向右移動一位,高區多一個元素。 - 如果
array[j] <= pivot
,在左邊,array[j]
應該移動到左邊的低區,只需與firstHigh
交換即可,然後firstHigh
向右移動一位,低區多一個元素 - j==end迴圈結束,除了pivot所有的元素都被遍歷,小於等於pivot都在
firstHigh
左邊,大於都在firstHigh
右邊。 - 此時需要考慮pivot的具體位置,pivot還在最後一位沒有動,注意到
firstHigh
左側都比pivot小,因此array[end]
與array[firstHigh]
交換即可,也就是10行 - 返回的下標就是firstHigh,這就是pivot的位置
這個方法算是比較常見的吧,但也有點不好寫。
填充法
public int insertPartition(int[] array, int start, int end) {
int pivot = array[start];
int low = start;
int high = end;
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];
}
array[low] = pivot;
return low;
}
接下來一頭一尾兩個指標往中間走的做法了,2-4行做基本的設定。主元選在start位置,其實選end也行,留作練習吧。low和high是兩個指標分別指向開頭和末尾。
這裡的partition目標有一些變化,按照那個標準也能寫,無非就是小改動。
小於pivot | pivot | 大於或者等於pivot |
---|---|---|
array[start...mid-1] |
array[mid] |
array[mid+1...end] |
5-14行使用while迴圈,因為使用賦值操作,所以叫做填充法
- 首先從high指標開始,如果high指向的元素
array[high] >= pivot
,high本來就是從右邊開始的,說明此時不需要交換,high所處的位置已經滿足條件,內部是未知狀態,high左移一位。 - 但是當
array[high] < pivot
時,因此array[high]
要放到左邊去,注意到已經array[low]已經用pivot變數儲存了,這個位置可以認為是空出來了(雖然沒有真正空出來),因此第9行array[low] = array[high];
,就這樣放到了左邊。 - low指標同理,但是當執行過
array[low]=array[high]
後,array[high]
相當於也空了出來。 - 因此當
array[low] >= pivot
時,需要向右邊填充就去找array[high]
吧
注意到外層while迴圈的條件被附帶到了內層,這是很常見的,內層迴圈改變變數的值,有的時候不可避免就不滿足外層迴圈的條件了。內外層迴圈都附帶
low<high
的條件,保證退出迴圈一定low=high
low與high撞上的時候,這裡實質上是一個空位,因為當迴圈結束時,空位左邊都小於pivot,空位右邊都大於等於pivot,並且迴圈遍歷了除了pivot以外所有的元素。因此填入pivot即可。
返回low或者high都可以,這就是pivot應該在的位置。
交換法
感覺這個方法見的最多。先給一個不常見的寫法
一個不常見的寫法
public int swapPartition(int[] array, int start, int end) {
int pivot = array[end];
int low = start;
int high = end-1;
while (low < high) {
while (low < high && array[high] >= pivot) {
high--;
}
while (low < high && array[low] < pivot) {
low++;
}
if (low < high) {
swap(array, low, high);
}
}
if (pivot > array[low]) {
low++;
}
swap(array, end, low);
return low;
}
2-4行,設定pivot為最後一個元素,一般都是第一個元素,這裡最後一個也無妨。如果是第一個可以當成作業完成。既然pivot已經指定,high可以從end-1開始往回走。
小於pivot | pivot | 大於或者等於pivot |
---|---|---|
array[start...mid-1] |
array[mid] |
array[mid+1...end] |
partition的目標仍然與填充法相同。
5-15行是迴圈體,很容易看懂
- 右邊找小的,左邊找大的,兩邊都找到就交換,low往右邊走,high往左邊走。可以保證low左邊都比pivot小,high右邊都大於等於pivot
- low,high指標都要往中間走,碰上迴圈結束。
- 碰上的時候,左側一定都小於pivot,右邊都大於等於pivot,那麼中間是不是應該填pivot?於是與
array[end]
交換?
不能想的太直接,16-18行加了一個判斷,當pivot <= array[low]
時,那當然可以把array[low]
換到右邊去,因為大一些嗎。如果pivot > array[low]
呢?
...... | array[low] |
array[low+1] |
...... | array[end] |
---|---|---|---|---|
....... | low==high |
low+1 |
...... | end |
那麼array[low]應該在左邊,可以考慮變通以下,array[end]
與low+1位置的元素交換,這樣就仍然滿足條件。最後返回low+1即可,low++本身就是low自增,所以如16-20行所示。
low++
會陣列越界嗎?不會。因為判斷條件為array[high] >= pivot
,因此high指標一定向左邊移動一步
為了方便與下一節相對比,給出pivot取array[start]
的另一個版本
public int swapPartition1(int[] array, int start, int end) {
int pivot = array[start];
int low = start + 1;
int high = end;
while (low < high) {
while (low < high && array[low] <= pivot) {
low++;
}
while (low < high && array[high] > pivot) {
high--;
}
if (low < high) {
swap(array, low, high);
}
}
if (pivot < array[low]) {
low--;
}
swap(array, start, low);
return low;
}
一個常見的寫法
public int swapPartition1(int[] array, int start, int end) {
int pivot = array[start];
int low = start;
int high = end;
while (low < high) {
while (low < high && array[high] > pivot) {
high--;
}
while (low < high && array[low] <= pivot) {
low++;
}
if (low < high) {
swap(array, low, high);
}
}
swap(array, start, low);
return low;
}
這裡pivot又換成開頭了,但是不管在哪裡,都要思路清晰,知道該做什麼改動能讓程式依舊正確。
但是這個寫法很嚴格,幾乎一點不能改動的。
流程與一個不常見的寫法一節中基本類似,但是
low=start
不能動,必須是這個,不能是low=start+1
,這個舉一個只有兩個元素的陣列的例子就知道了- 6-8行的內迴圈和9-11行的內迴圈不能對調
- 兩個內迴圈中的判斷必須如上面所寫,
array[high] > pivot
,array[low] <= pivot
,>和<=不能輕易改成別的。也就是partition的目標是固定的。(一旦改動,low必須是start+1,但是這樣寫不可避免最後swap又要加判斷,反而和不常見的寫法一樣了。)
小於或者等於pivot | pivot | 大於pivot |
---|---|---|
array[start...mid-1] |
array[mid] |
array[mid+1...end] |
如此才能捨去一個不常見的寫法中16-18行的判斷條件
最關鍵的點在於迴圈位置不能對調。迴圈終止,low與high碰上,有以下幾種情況
- high先停住,low增加碰上high停止。high停下來是因為
array[high] <= pivot
當然能與array[start]
交換換到左邊去 - low先停住,high減少碰上low停止。low停下來是因為
array[low] > pivot
,但是緊跟著swap(array, low, high);
,所以low先停下來也無妨,low的位置的元素仍然滿足array[low] <= pivot
,換到左邊去沒有問題。
鑑於這個寫法太過嚴格不建議這麼寫
完整程式碼
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args) {
int[] nums = {7,8,4,9,3,2,6,5,0,1,9,4,7,3};
quickSort(nums,0, nums.length-1);
System.out.println(Arrays.toString(nums));
}
public static void quickSort(int[] array, int start, int end) {
if(start < end){
int mid = swapPartition1(array, start, end);
quickSort(array, start, mid-1);
quickSort(array, mid+1, end);
}
}
public static int partition(int[] array, int start, int end) {
int firstHigh = start;
int pivot = array[end];
for(int j=start; j < end; j++) {
if(array[j] <= pivot) {
swap(array, firstHigh, j);
firstHigh++;
}
}
swap(array, firstHigh, end);
return firstHigh;
}
public static int insertPartition(int[] array, int start, int end) {
int pivot = array[start];
int low = start;
int high = end;
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];
}
array[low] = pivot;
return low;
}
/* 為啥這是正確的 */
public static int swapPartition1(int[] array, int start, int end) {
int pivot = array[start];
int low = start;
int high = end;
while (low < high) {
while (low < high && array[high] > pivot) {
high--;
}
while (low < high && array[low] <= pivot) {
low++;
}
if (low < high) {
swap(array, low, high);
}
}
swap(array, start, low);
return low;
}
/*這個肯定是正確的*/
public static int swapPartition(int[] array, int start, int end) {
int pivot = array[end];
int low = start;
int high = end;
while (low < high) {
while (low < high && array[low] < pivot) {
low++;
}
while (low < high && array[high] >= pivot) {
high--;
}
if (low < high) {
swap(array, low, high);
}
}
if (pivot > array[low]) {
low++;
}
swap(array, end, low);
return low;
}
public static void swap(int[] array, int i, int j) {
int tmp;
tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}