Quick Sort 快速排序算法
Table of Contents
前言
快速排序算法應該是常見的排序算法中使用的最多的一個,很多語言內置的排序算法都間接或直接的使用了這一算法。
因此,我們是很有必要學習快速排序算法的。
算法步驟
在了解詳細的算法步驟之前可以先來看一下快速排序算法的算法復雜度:
| $O(nlog_2n)$ | $O(n^2)$ | $O(1)$ |
通過快速排序算法的算法復雜度我們可以猜測它可能是一種 分治 算法,而事實也是如此。
當通過快速排序算法對數組 \(A\) 進行排序時,我們需要遞歸的將其分為不同的部分進行處理,其基本步驟為:
可以看到,快速排序算法的基本步驟并不難,實現這個算法主要需要考慮的問題便是 樞紐元 的選取和 怎樣分割 數組。
選取樞紐元
常見的樞紐元選擇方式大概就是選擇數組 中間 的那個元素,簡單直接有效。
在選取樞紐元后,通常需要對兩端和樞紐元處的值進行排序,這樣可以稍稍提高算法的效率并避免一些意外情況1:
public static int selectPivot(int[] arr, int left, int right) { // 包含右邊界int mid = (left + right) / 2;if (arr[left] > arr[mid]) {swap(arr, left, mid);}if (arr[left] > arr[right]) {swap(arr, left, right);}if (arr[mid] > arr[right]) {swap(arr, mid, right);}/* arr[left] <= arr[mid] <= arr[right] */swap(arr, mid, right - 1);return arr[right - 1]; }在將兩端和樞紐元處的值排序后,最左端的值必然是小于等于樞紐元處的值的,最右端的值必然是大于等于樞紐元處的值的,這時,我們需要分割的數組便變成了 [left + 1, right - 1].
然后,交換樞紐元和 right - 1 處的元素,使得樞紐元離開要被分割的數組2,這時,我們需要分割的數組便變成了 [left + 1, right - 2].
介紹完了常用的做法,這里在介紹一種不常用的做法和一種常見的錯誤做法:
- 隨機選取樞紐元是一種不常用的做法,因為隨機數的生成成本相較于中值的計算成本要昂貴的多
- 直接選取第一個元素作為樞紐元是一種常見的錯誤做法,當輸入的數組是預排序的時,會讓所有元素都分配到單個組中,并不斷遞歸,使得時間復雜度變成 \(O(n^2)\)
分割數組
分割數組時,需要首先將選取的樞紐元和末端的元素交換位置,然后從左向右遍歷所有小于樞紐元的元素,從右向左遍歷所有大于樞紐元的元素,當兩者遇到大于或小于樞紐元的元素停下時,便交換遇到的元素,然后繼續遍歷,直到交錯:
初始狀態:8 1 4 9 0 3 5 2 7 6i j p交換前:8 1 4 9 0 3 5 2 7 6i j p交換后:2 1 4 9 0 3 5 8 7 6i j p交錯后便將 i 處的元素和末端的樞紐元相交換,此時,樞紐元左邊的元素都小于等于它,右邊的元素都大于等于它:
交錯后:2 1 4 5 0 3 9 8 7 6j i p交換后:2 1 4 5 0 3 6 8 7 9i p遍歷過程中需要考慮的一個問題是:遇到和樞紐元相等的元素怎么處理?是都停止還是都不停止?
假設輸入的元素都相等,我們來看一下兩種策略最后可能的情況:
都停止:8 8 8 8 8 8 8 8 8j i p都不停止:8 8 8 8 8 8 8 8 8i j p可以看到,采用都停止的策略時,雖然會產生一些不必要的交換,但是都不停止的話,數組的分割便會極為的不均衡,這會使得時間復雜度很高。
因此,更好的選擇是在遇到和樞紐元相等的元素后都停下來。
算法實現
在確定了樞紐元的選取方法和數組的分割策略后,就可以嘗試實現快速排序算法了:
public static void quickSort(int[] arr) {quickSort(arr, 0, arr.length - 1); }public static void quickSort(int[] arr, int left, int right) { // 包含右邊界if (left >= right) { // 元素小于等于 1 個return;}int i = left, j = right - 1, pivot = selectPivot(arr, left, right);while (i < j) {while (arr[++i] < pivot) {}while (arr[--j] > pivot) {}if (i < j) {swap(arr, i, j) ;}}swap(arr, i, right - 1);quickSort(arr, left, i - 1);quickSort(arr, i + 1, right); }public static int selectPivot(int[] arr, int left, int right) {int mid = (left + right) / 2;if (arr[left] > arr[mid]) {swap(arr, left, mid);}if (arr[left] > arr[right]) {swap(arr, left, right);}if (arr[mid] > arr[right]) {swap(arr, mid, right);}/* arr[left] <= arr[mid] <= arr[right] */swap(arr, mid, right - 1);return arr[right - 1]; }public static void swap(int[] arr, int i, int j) {int tmp = arr[i];arr[i] = arr[j];arr[j] = tmp; }算法的實現并不是很難,但是需要注意的一段代碼是:
while (i < j) {while (arr[++i] < pivot) {}while (arr[--j] > pivot) {}if (i < j) {swap(arr, i, j) ;} }假如將這段代碼修改為如下形式,會使得在遇到 arr[i] = arr[j] = pivot 的情況后陷入死循環:
while (i < j) {while (arr[i] < pivot) {i++;}while (arr[j] > pivot) {j--;}if (i < j) {swap(arr, i, j) ;} }小數組和插入排序
快速排序在小數組上的表現還不如插入排序好,因此,在實現快速排序時,常常還會通過插入排序來排序較小的數組,比如說 OpenJDK 中的實現便是這樣的。
改進后的實現如下:
public static void insertSort(int[] arr, int left, int right) { // 包含右邊界for (int p = left + 1; p <= right; p++) {int tmp = arr[p];for (int j = p; j > left && arr[j - 1] > tmp; j--) {arr[j] = arr[j - 1];}arr[j] = tmp;} }public static void quickSort(int[] arr) {quickSort(arr, 0, arr.length - 1); }public static void quickSort(int[] arr, int left, int right) {if (left + 20 >= right) { // 小數組insertSort(arr, left, right);return;}int i = left, j = right - 1, pivot = selectPivot(arr, left, right);while (i < j) {while (arr[++i] < pivot) {}while (arr[--j] > pivot) {}if (i < j) {swap(arr, i, j) ;}}swap(arr, i, right - 1);quickSort(arr, left, i - 1);quickSort(arr, i + 1, right); }結語
這篇博客的大部分內容都來源于《數據結構和算法分析 —— C 語言描述》一書的 7.7 節,有興趣的可以看一看 @_@
Footnotes
1 意外情況可以參考《數據結構和算法分析 —— C 語言描述》一書中的習題 7.38
2 我并沒有得到這種做法的原因和解釋,只知道這種做法可以讓數組的分割更加安全(避免出錯或低效)
轉載于:https://www.cnblogs.com/rgbit/p/11069585.html
總結
以上是生活随笔為你收集整理的Quick Sort 快速排序算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 重庆满华农业公司是否具备农业科技种植的条
- 下一篇: 中国农业银行(火村支行)的行号是多少号