java 排序算法之快速排序
簡單介紹
快速排序(Quicksort) 是對 冒泡排序的一種改進。
基本思想
快速排序算法通過多次比較和交換來實現(xiàn)排序,其排序流程如下:
- (1)首先設(shè)定一個分界值(基準值),通過該分界值將數(shù)組分成左右兩部分。
- (2)將大于或等于分界值的數(shù)據(jù)集中到數(shù)組右邊,小于分界值的數(shù)據(jù)集中到數(shù)組的左邊。此時,左邊部分中各元素都小于或等于分界值,而右邊部分中各元素都大于或等于分界值。
- (3)然后,左邊和右邊的數(shù)據(jù)可以獨立排序。對于左側(cè)的數(shù)組數(shù)據(jù),又可以取一個分界值,將該部分數(shù)據(jù)分成左右兩部分,同樣在左邊放置較小值,右邊放置較大值。右側(cè)的數(shù)組數(shù)據(jù)也可以做類似處理。
- (4)重復上述過程,可以看出,這是一個遞歸定義。通過遞歸將左側(cè)部分排好序后,再遞歸排好右側(cè)部分的順序。當左、右兩個部分各數(shù)據(jù)排序完成后,整個數(shù)組的排序也就完成了。
該思想可以概括為:挖坑填數(shù) + 分治法。
比如如下的示意圖:
- 上圖以 最后一個元素的值 作為基準
- 比基準值小的,排在左側(cè),比基準值大的排在右側(cè)
- 然后再以分好的部分重復以上操作,直到每個部分中只有一個數(shù)據(jù)時,就排好序了
思路分析
基本思想如上,但是實現(xiàn)思路有多種,一般取基準值都是以首尾或者中間,這里使用 數(shù)組中間 的作為基準值,進行講解。
- 原始數(shù)組:
arr = [-9,78,0,23,-567-70]
- 設(shè)置兩個變量,左下標
L = 0
,右下標R = 數(shù)組大小 - 1
,選數(shù)組中間的值為基準值,pivot = arr[(L + R )/ 2]
,基準值為 0。再用兩個變量保存 左下標 和 右下標,left = L
和right = R
,用于后面的排序做準備。
可以看到,pivot把數(shù)組分成了兩組
- 左邊一組,從左到右,挨個與 基準值 比較,找出比基準值大的值,跳出查找循環(huán)
- 右邊一組,從右到左,挨個與 基準值 比較,找出比基準值小的值,跳出查找循環(huán)
- 可以看到左右兩組各找到一個對應的值,那么就讓他們進行交換
- 然后繼續(xù)找,直到左右兩邊碰到,
可以看到左邊的組已經(jīng)找完了,L
指向了基準值,那就跳出查找循環(huán),右邊組開始查找,
但是我們可以看到右邊的數(shù)也都符合規(guī)則,所以R
也循環(huán)遍歷到了基準值的位置,此時L
和R
已經(jīng)碰到一起了,這一輪就結(jié)束。這一輪就稱為快速排序。
繼續(xù)對分出來的小組,進行上述的快速排序操作,直到組內(nèi)只剩下一個數(shù)時,則排序完成。
- 將
L
和R
分別前進一步和后退一步,
如圖,就可以再次利用這兩個變量,對兩組進行快速排序了。
- 先對左邊的組進行快速排序,同樣進行上面的操作,
這里就用到了上一次快速循環(huán)所保存的left
和 right(上圖沒有畫出來)
了。
- 因為這里
left
直接就指向了pivot
,所以不用進行移動查找了。R
跟pivot
進行比較 ,-567 < -9 ,R
和left
進行交換,得到如下圖,注:pivot
是一個值,不是引用類型
因為 R
和 left
沒有碰頭,所以還得進行一次循環(huán)比較。因為 R
就在基準點這,所以不移動,R
和pivot
比較,-9 > -567 , 所以left
前進一步,
此時R
和left
已經(jīng)碰到一起了,這一輪就結(jié)束了。
- 右邊的組也是同樣的道理,這里就不作過多的解析了
l ------------ pivot --------------- r 一組從左往右找 一組從右往左找
可以看到,分組后,可以使用遞歸,對這一組繼續(xù)分組,然后對他們進行快速排序。
代碼實現(xiàn)
推導實現(xiàn)
推導法先實現(xiàn)第一輪
@Test public void processDemo() { int arr[] = {-9, 78, 0, 23, -567, 70}; System.out.println("原始數(shù)組:" + Arrays.toString(arr)); processQuickSort(arr, 0, arr.length - 1); } /** * @param arr * @param left 左邊這一組的下標起始點,到中間值,則為一組 * @param right 右邊這一組的下標結(jié)束點,到中間值,則為一組 */ public void processQuickSort(int[] arr, int left, int right) { /* 基本思想:選擇一個基準值,將基準值小分成一組,比基準值大的分成一組。 這里的實現(xiàn)思路: 1. 挑選的基準值為數(shù)組中間的值 2. 中間值就把數(shù)組分成了兩組 3. 左邊一組,從左到右,挨個與 基準值 比較,找出比基準值大的值 4. 右邊一組,從右到左,挨個與 基準值 比較,找出比基準值小的值 5. 左右兩邊各找到一個值,那么就讓這兩個值進行交換 6. 然后繼續(xù)找,直到左右兩邊碰到,這一輪就結(jié)束。這一輪就稱為快速排序 7. 繼續(xù)對分出來的小組,進行上述的快速排序操作,直到組內(nèi)只剩下一個數(shù)時,則排序完成 l ------------ pivot --------------- r 一組從左往右找 一組從右往左找 */ int l = left; int r = right; // 中心點,讓這個點作為基準值 int pivot = arr[(left + right) / 2]; // 當他們沒有碰到的時候,說明還這一輪還可以繼續(xù)找 while (l < r) { // 左邊組:當找到大于基準值時,退出循環(huán),則表示該值需要交換到右側(cè)去: arr[l] > pivot // 也就是說,如果 arr[l] < pivot,則表示還沒有找到比基準值大的數(shù) // 注意:不能等于 pivort,因為最差的情況沒有找到,則最后 arr[l] 就是 pivot 這個值,也同樣退出循環(huán) while (arr[l] < pivot) { l++; // 所以讓左邊這一組繼續(xù)找 } // 右邊組:當找到小于基準值時,退出循環(huán),則表示該值需要交換到左側(cè)去:arr[r] < pivot // 也就是說,如果 arr[l] > pivot,則表示還沒有找到比基準值小的數(shù) //注意:這里也同樣跟上面一樣,不能等于 pivort while (arr[r] > pivot) { r--;//讓右邊組繼續(xù)找 } // 當左側(cè)與右側(cè)相碰時,說明兩邊都沒有找到,這一輪不用進行交換 // 等于表示,找到了中間的下標 if (l >= r) { break; } // 當找到時,則兩數(shù)進行交換。 //注意:這里可能會出現(xiàn)有一組已經(jīng)找完了,或者沒有找到,但是另一組找到了,所以一個是指向 pivot 的, //另一個則是指向要交換的數(shù),交換后 pivot的值在數(shù)組中的位置會發(fā)生改變,下次的交換方式就會發(fā)生變化。這個地方要動腦筋想想。 int temp = arr[l]; arr[l] = arr[r]; arr[r] = temp; // 當交換后, // 當數(shù)組是: {-9, 78, 0, -23, 0, 70} 時(pivot的值在數(shù)組中有多個),就可以驗證這里的邏輯 // 如果沒有這個判定,將會導致,l 永遠 小于 r。循環(huán)不能退出來的情況,就出現(xiàn)死循環(huán)了 if (arr[l] == pivot) { /* l 自己不能往前移動 一步,因為當交換完成后為:{-9, 0, 0, -23, 78, 70} l = 1,arr[l] = 0 r = 4,arr[r] = 78 再經(jīng)過一次循環(huán)后 l = 1,arr[l] = 0 r = 3,arr[r] = -23 交換后數(shù)組為:{-9,-23,0,0,78,70} 此時 l = 1,arr[l] = -23;r = 3,arr[r] = 0 又經(jīng)過一次循環(huán)后 l = 2,arr[l] = 0 r = 3,arr[r] = 0 數(shù)組為:{-9,-23,0,0,78,70} 進入了死循環(huán) 這里好好動腦子想想 這里為什么是用r-=1呢?是因為if里面的條件是arr[l] == pivot,如果要排序的數(shù)組中不存在多個和基準值相等的值, 那么用l+=1的話,l就會跑過分界線(基準值),跑到另一組去,這個算法也就失敗了, 還有一個原因是,r 是剛剛交換過的,一定比 基準值大,所以沒有必要再和基準值比較了 */ r -= 1; } // 這里和上面一致,如果說,先走了上面的 r-=1 // 這里也滿足(也就是說有多個值和基準值相等),那么說明,下一次是相同的兩個值,一個是 r == 一個是基準值 // 但是他們是相同的值,r后退一步 l前進一步,不影響。但是再走這完這里邏輯時,就會導致 l > r,退出整個循環(huán) if (arr[r] == pivot) { l += 1; } } System.out.println("第 1 輪排序后:" + Arrays.toString(arr)); }
注意:上述的算法特別是邊界判定,就是上面「當交換后」對 r-=1
的這個邊界判定時,有點難以理解,但是一定要理解為什么要這樣寫。
測試信息輸出
原始數(shù)組:[-9, 78, 0, 23, -567, 70]
第 1 輪排序后:[-9, -567, 0, 23, 78, 70]
那么如何向左遞歸和右遞歸呢?在上面的代碼后面接著實現(xiàn)如下
System.out.println("第 1 輪排序后:" + Arrays.toString(arr)); /* if (l >= r) { break; } 循環(huán)從這條代碼中跳出就會l = r */ // 如果 l = r,會出現(xiàn)死循環(huán),出現(xiàn)棧溢出 //這里也要動腦子想想 if (l == r) { l++; r--; } // 開始左遞歸 // 上面算法是 r--,l++ ,往兩組的中間走,當 left < r 時,表示還可以繼續(xù)分組 if (left < r) { processQuickSort(arr, left, r); } if (right > l) { processQuickSort(arr, l, right); }
完整實現(xiàn)
完整實現(xiàn)和推導實現(xiàn)其實差不多了,為了加深記憶,自己按照基本思想和思路分析,默寫。
/** * 快速排序默寫實現(xiàn) * * 基本思想:通過一趟將要排序的數(shù)據(jù),分隔成獨立的兩個部分,一部分的所有數(shù)據(jù)都比另一部分的所有數(shù)據(jù)要小。 * 思路分析: * {-9, 78, 0, 23, -567, 70}; length=6 * 1. 挑選中間的值作為 基準值:(0 + (6 -1))/2= [2] = 0 * 2. 左側(cè) left 部分,從 0 開始到中間值 -1: 0,1: -9, 78,找出一個比基準值大的數(shù) * 3. 右側(cè) right 部分,從中間值 + 1 到數(shù)組大小-1:3,5:23,-567, 70,找出一個比基準值小的數(shù) * 4. 如果找到,則將他們進行交換,這樣一輪下來,就完成了一次快速排序:一部分的所有數(shù)據(jù)都比另一部分的所有數(shù)據(jù)要小。 * 4. 如果左側(cè)部分還可以分組,則進行左側(cè)遞歸調(diào)用 * 5. 如果右側(cè)部分還可以分組,則進行右側(cè)遞歸調(diào)用 * * 簡單說:一輪快速排序示意圖如下: * 中間的基準值 * l ------------ pivot --------------- r * 一組從左往右找 一組從右往左找 * 找到比基準值大的數(shù) 找出一個比基準值小的數(shù) * 然后進行交換 * */ @Test public void quickSortTest() { int arr[] = {-9, 78, 0, 23, -567, 70}; // int arr[] = {-9, 78, 0, -23, 0, 70}; // 在推導過程中,將會導致交換異常的數(shù)組,在這里不會出現(xiàn)那種情況 int left = 0; int right = arr.length - 1; System.out.println("原始數(shù)組:" + Arrays.toString(arr)); quickSort(arr, left, right); System.out.println("排序后:" + Arrays.toString(arr)); } public void quickSort(int[] arr, int left, int right) { // 找到中間值 int pivotIndex = (left + right) / 2; int pivot = arr[pivotIndex]; int l = left; int r = right; while (l < r) { // 從左往右找,直到找到一個數(shù),比基準值大的數(shù) while (arr[l] < pivot) { l++; } // 從右往左找,知道找到一個數(shù),比基準值小的數(shù) while (arr[r] > pivot) { r--; } // 表示未找到 if (l >= r) { break; } // 進行交換 int temp = arr[l]; arr[l] = arr[r]; arr[r] = temp; // 那么下一輪,左側(cè)的這個值將不再參與排序,因為剛交換過,一定比基準值小 // 那么下一輪,右側(cè)的這個值將不再參與排序,因為剛交換過,一定比基準值大 r--; l++; } // 當一輪找完后,沒有找到,則是中間值時, // 需要讓他們擦肩而過,也就是重新分組,中間值不再參與分組 // 否則,在某些情況下,會進入死循環(huán) if (l == r) { l++; r--; } // 如果左側(cè)還可以繼續(xù)分組,則繼續(xù)快排 // 由于擦肩而過了,那么左側(cè)的組值,則是最初的開始與中間值的前一個,也就是這里得到的 r if (left < r) { quickSort(arr, left, r); } if (right > l) { quickSort(arr, l, right); } }
另外,在實現(xiàn)的過程中,將某些代碼為什么要那樣判斷邊界,進行了梳理。你會發(fā)現(xiàn)上述代碼和推導的代碼有一個地方不一樣。這個是我自己按邏輯進行的改進,更容易看明白一些。目前未發(fā)現(xiàn) bug,如果有錯請評論指出,畢竟這個算法還是有點難的。
大數(shù)據(jù)量耗時測試
/** * 大量數(shù)據(jù)排序時間測試 */ @Test public void bulkDataSort() { int max = 80000; // int max = 8; int[] arr = new int[max]; for (int i = 0; i < max; i++) { arr[i] = (int) (Math.random() * 80000); } if (arr.length < 10) { System.out.println("原始數(shù)組:" + Arrays.toString(arr)); } Instant startTime = Instant.now(); // processQuickSort(arr, 0, arr.length - 1); // 和老師的原版代碼對比,結(jié)果是一樣的 quickSort(arr, 0, arr.length - 1); if (arr.length < 10) { System.out.println("排序后:" + Arrays.toString(arr)); } Instant endTime = Instant.now(); System.out.println("共耗時:" + Duration.between(startTime, endTime).toMillis() + " 毫秒"); }
多次運行輸出
共耗時:40 毫秒
共耗時:52 毫秒
共耗時:36 毫秒
共耗時:31 毫秒
性能分析
快速排序的一次劃分算法從兩頭交替搜索,直到low和hight重合,因此其時間復雜度是O(n);而整個快速排序算法的時間復雜度與劃分的趟數(shù)有關(guān)。
理想的情況是,每次劃分所選擇的中間數(shù)恰好將當前序列幾乎等分,經(jīng)過log2n趟劃分,便可得到長度為1的子表。這樣,整個算法的時間復雜度為O(nlog2n)。
最壞的情況是,每次所選的中間數(shù)是當前序列中的最大或最小元素,這使得每次劃分所得的子表中一個為空表,另一子表的長度為原表的長度-1。這樣,長度為n的數(shù)據(jù)表的快速排序需要經(jīng)過n趟劃分,使得整個排序算法的時間復雜度為O(n2)。
為改善最壞情況下的時間性能,可采用其他方法選取中間數(shù)。通常采用“三者值取中”方法,即比較H->r[low].key、H->r[high].key與H->r[(low+high)/2].key,取三者中關(guān)鍵字為中值的元素為中間數(shù)。
可以證明,快速排序的平均時間復雜度也是O(nlog2n)。因此,該排序方法被認為是目前最好的一種內(nèi)部排序方法。
從空間性能上看,盡管快速排序只需要一個元素的輔助空間,但快速排序需要一個??臻g來實現(xiàn)遞歸。最好的情況下,即快速排序的每一趟排序都將元素序列均勻地分割成長度相近的兩個子表,所需棧的最大深度為log2(n+1);但最壞的情況下,棧的最大深度為n。這樣,快速排序的空間復雜度為O(log2n)。
以上就是java 排序算法之快速排序的詳細內(nèi)容,更多關(guān)于java 快速排序的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java設(shè)計模式單例模式(Singleton)用法解析
這篇文章主要介紹了Java設(shè)計模式單例模式(Singleton)用法解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2019-11-11在Spring中利用@Order注解對bean和依賴進行排序
在Spring框架中,@Order是一個經(jīng)常被忽視但非常重要的注解,在項目開發(fā)中,當我們需要維護bean的特定順序或者存在許多相同類型的bean時,這個注解就發(fā)揮了作用,這篇文章講的就是如何利用@Order注解對bean和依賴進行排序,需要的朋友可以參考下2023-11-11解決MyEclipse出現(xiàn)the user operation is waiting的問題
今天做項目的時候每次修改代碼保存后都會跳出一個框框,然后就有兩個進度條,上面寫the user operation is wating...小編去網(wǎng)上查了查解決了這個問題,下面跟大家分享一下。2018-04-04Struts2中Action三種接收參數(shù)形式與簡單的表單驗證功能
本文以登錄驗證為例,進行代碼展示,下面給大家詳細介紹Struts2中Action三種接收參數(shù)形式與簡單的表單驗證功能,需要的朋友參考下2017-03-03Java?Servlet響應httpServletResponse過程詳解
HttpServletResponse是處理http響應的對象,調(diào)用該對象的方法,設(shè)置到對象屬性的內(nèi)容,tomcat最終會組織為http響應報文2022-02-02