java 排序算法之快速排序
簡(jiǎn)單介紹
快速排序(Quicksort) 是對(duì) 冒泡排序的一種改進(jìn)。
基本思想
快速排序算法通過(guò)多次比較和交換來(lái)實(shí)現(xiàn)排序,其排序流程如下:
- (1)首先設(shè)定一個(gè)分界值(基準(zhǔn)值),通過(guò)該分界值將數(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ù)也可以做類(lèi)似處理。
- (4)重復(fù)上述過(guò)程,可以看出,這是一個(gè)遞歸定義。通過(guò)遞歸將左側(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),右邊組開(kāi)始查找,

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

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

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

因?yàn)?R 和 left 沒(méi)有碰頭,所以還得進(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é)束了。

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





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é)束。這一輪就稱(chēng)為快速排序
7. 繼續(xù)對(duì)分出來(lái)的小組,進(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)他們沒(méi)有碰到的時(shí)候,說(shuō)明還這一輪還可以繼續(xù)找
while (l < r) {
// 左邊組:當(dāng)找到大于基準(zhǔn)值時(shí),退出循環(huán),則表示該值需要交換到右側(cè)去: arr[l] > pivot
// 也就是說(shuō),如果 arr[l] < pivot,則表示還沒(méi)有找到比基準(zhǔn)值大的數(shù)
// 注意:不能等于 pivort,因?yàn)樽畈畹那闆r沒(méi)有找到,則最后 arr[l] 就是 pivot 這個(gè)值,也同樣退出循環(huán)
while (arr[l] < pivot) {
l++; // 所以讓左邊這一組繼續(xù)找
}
// 右邊組:當(dāng)找到小于基準(zhǔn)值時(shí),退出循環(huán),則表示該值需要交換到左側(cè)去:arr[r] < pivot
// 也就是說(shuō),如果 arr[l] > pivot,則表示還沒(méi)有找到比基準(zhǔn)值小的數(shù)
//注意:這里也同樣跟上面一樣,不能等于 pivort
while (arr[r] > pivot) {
r--;//讓右邊組繼續(xù)找
}
// 當(dāng)左側(cè)與右側(cè)相碰時(shí),說(shuō)明兩邊都沒(méi)有找到,這一輪不用進(jìn)行交換
// 等于表示,找到了中間的下標(biāo)
if (l >= r) {
break;
}
// 當(dāng)找到時(shí),則兩數(shù)進(jìn)行交換。
//注意:這里可能會(huì)出現(xiàn)有一組已經(jīng)找完了,或者沒(méi)有找到,但是另一組找到了,所以一個(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)證這里的邏輯
// 如果沒(méi)有這個(gè)判定,將會(huì)導(dǎo)致,l 永遠(yuǎn) 小于 r。循環(huán)不能退出來(lái)的情況,就出現(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)過(guò)一次循環(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)過(guò)一次循環(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的話(huà),l就會(huì)跑過(guò)分界線(xiàn)(基準(zhǔn)值),跑到另一組去,這個(gè)算法也就失敗了,
還有一個(gè)原因是,r 是剛剛交換過(guò)的,一定比 基準(zhǔn)值大,所以沒(méi)有必要再和基準(zhǔn)值比較了
*/
r -= 1;
}
// 這里和上面一致,如果說(shuō),先走了上面的 r-=1
// 這里也滿(mǎn)足(也就是說(shuō)有多個(gè)值和基準(zhǔn)值相等),那么說(shuō)明,下一次是相同的兩個(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)難以理解,但是一定要理解為什么要這樣寫(xiě)。
測(cè)試信息輸出
原始數(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--;
}
// 開(kāi)始左遞歸
// 上面算法是 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í)差不多了,為了加深記憶,自己按照基本思想和思路分析,默寫(xiě)。
/**
* 快速排序默寫(xiě)實(shí)現(xiàn)
*
* 基本思想:通過(guò)一趟將要排序的數(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 開(kāi)始到中間值 -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)行交換,這樣一輪下來(lái),就完成了一次快速排序:一部分的所有數(shù)據(jù)都比另一部分的所有數(shù)據(jù)要小。
* 4. 如果左側(cè)部分還可以分組,則進(jìn)行左側(cè)遞歸調(diào)用
* 5. 如果右側(cè)部分還可以分組,則進(jìn)行右側(cè)遞歸調(diào)用
*
* 簡(jiǎn)單說(shuō):一輪快速排序示意圖如下:
* 中間的基準(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)過(guò)程中,將會(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過(guò),一定比基準(zhǔn)值小
// 那么下一輪,右側(cè)的這個(gè)值將不再參與排序,因?yàn)閯偨粨Q過(guò),一定比基準(zhǔn)值大
r--;
l++;
}
// 當(dāng)一輪找完后,沒(méi)有找到,則是中間值時(shí),
// 需要讓他們擦肩而過(guò),也就是重新分組,中間值不再參與分組
// 否則,在某些情況下,會(huì)進(jìn)入死循環(huán)
if (l == r) {
l++;
r--;
}
// 如果左側(cè)還可以繼續(xù)分組,則繼續(xù)快排
// 由于擦肩而過(guò)了,那么左側(cè)的組值,則是最初的開(kāi)始與中間值的前一個(gè),也就是這里得到的 r
if (left < r) {
quickSort(arr, left, r);
}
if (right > l) {
quickSort(arr, l, right);
}
}
另外,在實(shí)現(xiàn)的過(guò)程中,將某些代碼為什么要那樣判斷邊界,進(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í)測(cè)試
/**
* 大量數(shù)據(jù)排序時(shí)間測(cè)試
*/
@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)過(guò)log2n趟劃分,便可得到長(zhǎng)度為1的子表。這樣,整個(gè)算法的時(shí)間復(fù)雜度為O(nlog2n)。
最壞的情況是,每次所選的中間數(shù)是當(dāng)前序列中的最大或最小元素,這使得每次劃分所得的子表中一個(gè)為空表,另一子表的長(zhǎng)度為原表的長(zhǎng)度-1。這樣,長(zhǎng)度為n的數(shù)據(jù)表的快速排序需要經(jīng)過(guò)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來(lái)實(shí)現(xiàn)遞歸。最好的情況下,即快速排序的每一趟排序都將元素序列均勻地分割成長(zhǎng)度相近的兩個(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ù)階乘算法代碼詳解-10,0000級(jí)
這篇文章主要介紹了Java版超大整數(shù)階乘算法代碼詳解-10,0000級(jí),具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01
SpringCloud-Gateway轉(zhuǎn)發(fā)WebSocket失敗問(wèn)題及解決
這篇文章主要介紹了SpringCloud-Gateway轉(zhuǎn)發(fā)WebSocket失敗問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09
Java 時(shí)間轉(zhuǎn)換的實(shí)例代碼
下面小編就為大家?guī)?lái)一篇Java 時(shí)間轉(zhuǎn)換的實(shí)例代碼。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-07-07
在SpringBoot項(xiàng)目中利用maven的generate插件
今天小編就為大家分享一篇關(guān)于在SpringBoot項(xiàng)目中利用maven的generate插件,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-01-01
Java基于堆結(jié)構(gòu)實(shí)現(xiàn)優(yōu)先隊(duì)列功能示例
這篇文章主要介紹了Java基于堆結(jié)構(gòu)實(shí)現(xiàn)優(yōu)先隊(duì)列功能,結(jié)合實(shí)例形式分析了java優(yōu)先隊(duì)列的簡(jiǎn)單定義與使用方法,需要的朋友可以參考下2017-11-11
SpringIntegration消息路由之Router的條件路由與過(guò)濾功能
本文詳細(xì)介紹了Router的基礎(chǔ)概念、條件路由實(shí)現(xiàn)、基于消息頭的路由、動(dòng)態(tài)路由與路由表、消息過(guò)濾與選擇性路由以及錯(cuò)誤處理與路由等方面的內(nèi)容,提高了系統(tǒng)的可維護(hù)性和可擴(kuò)展性,感興趣的朋友一起看看吧2025-04-04

