快速排序(quick sort)也是一種交換排序,它在排序中採取了分治策略。
從待排序列中選取一元素作為軸值(也叫主元)。
將序列中的剩餘元素以該軸值為基準,分為左右兩部分。左部分元素不大於軸值,右部分元素不小於軸值。軸值最終位於兩部分的分割處。
對左右兩部分重複進行這樣的分割,直至無可分割。
從快速排序的演算法思想可以看出,這是一遞迴的過程。
要想徹底弄懂快速排序,得解決兩個問題:
如何選擇軸值?(軸值不同,對排序有影響嗎?)
如何分割?
問題一:軸值的選取?
軸值的重要性在於:經過分割應使序列盡量分為長度相等的兩個部分,這樣分治法才會起作用。若是軸值正好為序列的最值,分割後,元素統統跑到一邊兒去了,分治法就無效了。演算法效率無法提高。-看別人寫快排的時候,注意他軸值的選取哦。
問題二:如何分割?
這涉及到具體的技巧和策略。在稍後的**中我們一一介紹。
直接選取第乙個元素或最後乙個元素為軸值。這也是國內眾多教材中的寫法。
舉個例子:
原序列 4 8 12 1 9 6
下標 0 1 2 3 4 5
軸值 pivot=4
初始化 i j
i j i不動,移動j,while(i=pivot)j--;
移動元素
1 8 12 1 9 6
i j j不動,移動i,while(i
移動元素 1 8 12 8 9 6
i,j 再次移動j,i和j相遇,結束
最後一步 1 4 12 8 9 6 pivot歸位
軸值4的左右兩部分接著分割……
我想你一定看懂了,並且這軸值4,真的沒選好,因為分割後左部分只有乙個元素。
有人稱上面的做法是:挖坑填數。這種描述真的很形象。簡單解釋下:首先取首元素為軸值,用變數pivot儲存軸值,這就是挖了乙個坑。此時,a[0]就是乙個坑。接著移動j,把合適位置的j填入a[0],於是a[j]成了新的坑。舊的坑被填上,新的坑就出現。直到i和j相遇,這最後乙個坑,被pivot填上。至此完成了第一趟分割……
看懂了,就動手敲**吧!
void quicksort(int a, int n) //快速排序,版本一
a[i] = pivot; //把軸值放到分割處
quicksort(a, i);
quicksort(a + i + 1, n - i -1);
}}
現在想想以最後乙個元素為軸值的**了,先別急著看,先動動手哦!**如下:
void quicksort(int a, int n)
a[i] = pivot; //把軸值放到分割處
quicksort(a, i);
quicksort(a + i + 1, n - i - 1);
}}
為了讓軸值pivot不至於無效(不讓pivot出現最值的情況)。我們可以使用一些策略來改進pivot的選取。
策略一:
隨機選取序列中一元素為軸值。
int selectpivot(int a, int low, int high)
選取首尾元素不就是該策略的一種特例!
策略二:
隨機選取三數,取中位數。
int selectpivot(int a, int low, int high)
它的一種特例就是,選取原序列首、
尾、中間三數,取它們的中位數。
目前看來基本常用的就這兩種策略。
不過我得吐槽一句:如果原序列中的元素本身就是隨機存放的,也就是說,各個元素出現在各個位置的概率一樣。那麼特別地選取首尾元素和隨機選取又有什麼區別呢?不知大家怎麼看?
還得補充一句:隨機選取軸值後,記得要把它和首或尾的元素交換哦。至於為什麼?***!
這也是《演算法導論》上的版本。它的普遍做法是選取尾元素為pivot。重點是使用了乙個分割函式:partition()。
偽**與如下:
partition(a, low, high)
1. pivot <- a[high] //選取尾元素為軸值
2. i <- low-1 //把low-1賦值給i,下同
3. for j <- low to high-1 //j的變化範圍[low, high-1]
4. do if a[j] <= pivot
5. then i <- i+1
6. exchange a[i]<->a[j]
7. exchange a[i+1} <
-> a[high]
8. return i+1; //返回的是分割的位置
然後,對整個陣列進行遞迴排序:
quicksort(a, low, high)
1 if low < high
2 then q <- partition(a, low, high) //對元素進行分割就在這裡
3 quicksort(a, low, q - 1)
4 quicksort(a, q + 1, high)
如果你不習慣於看偽**,我來舉個例子:(還是上面的序列)
原序列 4 8 12 1 9 6
下標 -1 0 1 2 3 4 5 軸值pivot是6
初始化 i j a[j]=a[0]=4<6,下一步先 i++;再swap(a[i],a[j]);隨後j++;
交換 4 8 12 1 9 6
i j 接著移動j
i j a[j]=a[3]=1<6,下一步…
交換 4 1 12 8 9 6
i j
i j
交換 4 1 6 8 9 12 最後一步 swap(a[i+1], a[high]);或者是 swap(a[i+1], a[j]);
所以最後返回的是 i+1
用大白話講講上面的排序過程:用兩個指標i,j,它們初始化為i=-1;j=0,接著讓j不斷地自增,遇到a[j]>pivot就與i交換,直到j指向末尾。
更直白的話:從頭開始遍歷原序列,遇到小於軸值的就交換到序列前面。
看懂了,就寫**了…
int partition(int a, int low, int high)
swap(a[++i], a[high]); //主元歸位
return i; //上面一步已經 ++i,所以這裡不用 i+1
}void quicksort(int a, int low, int high)
}void quicksort(int a, int n)
題外話:看到有的api設計是這樣的:quicksort(int a, int low, int high)。居然讓使用者多寫乙個0!如此不為使用者考慮。應越簡潔越好。排序只給陣列名和陣列大小,即可。
對上面的流程再思考:看到初始化i=-1;你不覺得奇怪嗎?為什麼i一定要從-1開始,仔細了解了i的作用,你會發現i本可以從0開始。這種做法的partition()方法是這樣的:
int partition(int a, int low, int high)
swap(a[i], a[high]); //主元歸位
return i;
}
再思考:為什麼j不能指向high?若是更改if(a[j]
此時的partition()是這樣的:
int partition(int a, int low, int high)
return i-1; //這裡為什麼是i-1,得想明白?
}
至於有時候把quicksort()和partition()寫成乙個函式,那是再簡單不過的事情,你肯定會的。
上面用的都是遞迴的方法,把遞迴轉化非遞迴總是不簡單的,也總讓人興奮。這個版本就是快速排序的非遞迴寫法;
void quicksort(int a, int low, int high)
if (mid + 1 < high)
//只要棧不為空,說明仍有可分割的部分
while(!s.empty())
if (mid + 1 < h)
} }}
這個非遞迴的寫法是很有意思的,很需要技巧。仔細想想,你能明白的。
快速排序號稱快速搞定,時間複雜度是o(nlogn)。基本上是最優的排序方法。它的寫法不外乎以上三種,大同小異。看到這裡。你一定徹底了解了它。以上寫法,都經過了本人測試,不知道你的測試是否和我一樣?
若是有所幫助,頂乙個哦!
本專欄的目錄
所有內容的目錄
交換排序 快速排序
簡述 快速排序可以說算是針對氣泡排序的一種優化,氣泡排序是順序交換,這樣交換次數順序增長。如果我們做跳躍的交換,那麼可以使得交換次數也是跳躍性,會有所降低 演算法思想 找出乙個樞軸,用於做比較交換,並記下後用。一般用第乙個 用第 一 中間 最後三個取中後來效果會更好 定乙個高位 high和底位 lo...
交換排序之快速排序
1.基本思想 假設要排序的陣列是array 0 array n 1 首先任意選取乙個資料 通常選用第乙個資料 作為關鍵資料,然後將所有比它的數都放到它前面,所有比它大的數都放到它後面,這個過程稱為一趟快速排序。一趟快速排序的演算法是 1 設定兩個變數i j,排序開始的時候i 0,j n 1 2 以第...
交換排序 2 快速排序
快速排序 快速排序的基本演算法思想是 設待排序的元素序列個數為 n,存放在陣列 a中,令第乙個元素為樞軸元素,即將 a 0 作為參考元素,令 pivot a 0 初始時,令 i 0 j a.legth 1 然後按以下方法操作 1 從序列的 j位置往前,依次將陣列的中的元素與樞軸元素相比較,如果當前元...