java 排序算法之快速排序
簡單介紹
快速排序(Quicksort) 是對(duì) 冒泡排序的一種改進(jìn)。
基本思想
快速排序算法通過多次比較和交換來實(shí)現(xiàn)排序,其排序流程如下:
- (1)首先設(shè)定一個(gè)分界值(基準(zhǔn)值),通過該分界值將數(shù)組分成左右兩部分。
- (2)將大于或等于分界值的數(shù)據(jù)集中到數(shù)組右邊,小于分界值的數(shù)據(jù)集中到數(shù)組的左邊。此時(shí),左邊部分中各元素都小于或等于分界值,而右邊部分中各元素都大于或等于分界值。
- (3)然后,左邊和右邊的數(shù)據(jù)可以獨(dú)立排序。對(duì)于左側(cè)的數(shù)組數(shù)據(jù),又可以取一個(gè)分界值,將該部分?jǐn)?shù)據(jù)分成左右兩部分,同樣在左邊放置較小值,右邊放置較大值。右側(cè)的數(shù)組數(shù)據(jù)也可以做類似處理。
- (4)重復(fù)上述過程,可以看出,這是一個(gè)遞歸定義。通過遞歸將左側(cè)部分排好序后,再遞歸排好右側(cè)部分的順序。當(dāng)左、右兩個(gè)部分各數(shù)據(jù)排序完成后,整個(gè)數(shù)組的排序也就完成了。
該思想可以概括為:挖坑填數(shù) + 分治法。
比如如下的示意圖:

- 上圖以 最后一個(gè)元素的值 作為基準(zhǔn)
- 比基準(zhǔn)值小的,排在左側(cè),比基準(zhǔn)值大的排在右側(cè)
- 然后再以分好的部分重復(fù)以上操作,直到每個(gè)部分中只有一個(gè)數(shù)據(jù)時(shí),就排好序了
思路分析
基本思想如上,但是實(shí)現(xiàn)思路有多種,一般取基準(zhǔn)值都是以首尾或者中間,這里使用 數(shù)組中間 的作為基準(zhǔn)值,進(jìn)行講解。
- 原始數(shù)組:
arr = [-9,78,0,23,-567-70]

- 設(shè)置兩個(gè)變量,左下標(biāo)
L = 0,右下標(biāo)R = 數(shù)組大小 - 1,選數(shù)組中間的值為基準(zhǔn)值,pivot = arr[(L + R )/ 2],基準(zhǔn)值為 0。再用兩個(gè)變量保存 左下標(biāo) 和 右下標(biāo),left = L和right = R,用于后面的排序做準(zhǔn)備。

可以看到,pivot把數(shù)組分成了兩組
- 左邊一組,從左到右,挨個(gè)與 基準(zhǔn)值 比較,找出比基準(zhǔn)值大的值,跳出查找循環(huán)

- 右邊一組,從右到左,挨個(gè)與 基準(zhǔn)值 比較,找出比基準(zhǔn)值小的值,跳出查找循環(huán)

- 可以看到左右兩組各找到一個(gè)對(duì)應(yīng)的值,那么就讓他們進(jìn)行交換

- 然后繼續(xù)找,直到左右兩邊碰到,

可以看到左邊的組已經(jīng)找完了,L指向了基準(zhǔn)值,那就跳出查找循環(huán),右邊組開始查找,

但是我們可以看到右邊的數(shù)也都符合規(guī)則,所以R也循環(huán)遍歷到了基準(zhǔn)值的位置,此時(shí)L和R 已經(jīng)碰到一起了,這一輪就結(jié)束。這一輪就稱為快速排序。
繼續(xù)對(duì)分出來的小組,進(jìn)行上述的快速排序操作,直到組內(nèi)只剩下一個(gè)數(shù)時(shí),則排序完成。
- 將
L和R分別前進(jìn)一步和后退一步,

如圖,就可以再次利用這兩個(gè)變量,對(duì)兩組進(jìn)行快速排序了。
- 先對(duì)左邊的組進(jìn)行快速排序,同樣進(jìn)行上面的操作,

這里就用到了上一次快速循環(huán)所保存的left 和 right(上圖沒有畫出來)了。
- 因?yàn)檫@里
left直接就指向了pivot,所以不用進(jìn)行移動(dòng)查找了。R跟pivot進(jìn)行比較 ,-567 < -9 ,R和left進(jìn)行交換,得到如下圖,注:pivot是一個(gè)值,不是引用類型

因?yàn)?R 和 left 沒有碰頭,所以還得進(jìn)行一次循環(huán)比較。因?yàn)?R 就在基準(zhǔn)點(diǎn)這,所以不移動(dòng),R 和pivot 比較,-9 > -567 , 所以left 前進(jìn)一步,

此時(shí)R和left 已經(jīng)碰到一起了,這一輪就結(jié)束了。

- 右邊的組也是同樣的道理,這里就不作過多的解析了





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

