Java經(jīng)典算法之快速排序詳解
一、什么是快速排序
快速排序是由冒泡排序演變而來(lái),比冒泡排序更快的排序算法。之所以快,是因?yàn)榭焖倥判蛴昧?strong>分治法。
相同的是,與冒泡排序一樣,快速排序也屬于交換排序,通過(guò)元素之間的比較和交換來(lái)排序。
不同的是,冒泡排序每一輪只把一個(gè)元素冒泡到數(shù)列的一端,而快速排序每輪挑選一個(gè)基準(zhǔn)元素,讓比它小的元素移動(dòng)到一端,讓比它大的元素移動(dòng)到另一端,從而把數(shù)列拆解成兩個(gè)部分。
這種每次將數(shù)列分為兩個(gè)部分的方法就叫做分治法。
分治法的好處體現(xiàn)在相比于冒泡排序它會(huì)有更小的時(shí)間復(fù)雜度,對(duì)于n的元素的冒泡排序時(shí)間復(fù)雜度為O(n2),而快速排序總體的平均時(shí)間復(fù)雜度為O(nlogn)。
二、基準(zhǔn)元素的選擇
在使用分治法的過(guò)程中以基準(zhǔn)元素為中心把其他元素移到它的兩邊,那么如何選擇基準(zhǔn)元素呢?
1、選擇第一個(gè)元素
最簡(jiǎn)單的方法是直接選擇數(shù)列第一個(gè)元素為基準(zhǔn)元素,但是這種方法在有些特殊情況下會(huì)出現(xiàn)問(wèn)題:
對(duì)于這種原本是逆序的數(shù)列,每輪都只能確定基準(zhǔn)元素的位置,這種情況下快速排序需要進(jìn)行n輪,時(shí)間復(fù)雜度退化到了O(n2)。
2、隨機(jī)選擇
為了解決時(shí)間復(fù)雜度過(guò)大的情況,我們可以隨機(jī)選擇一個(gè)元素,并與首位元素互換位置,雖然這種情況下也有幾率選到數(shù)列的最大或最小值,但大多情況下都是可以的。
所以,雖然快速排序的平均時(shí)間復(fù)雜度為O(nlogn),但最壞情況下也可以達(dá)到O(n2)。
三、元素的交換
選好了基準(zhǔn)元素,就要將其他元素移到基準(zhǔn)元素兩邊,具體實(shí)現(xiàn)有兩種方法:
- 雙邊循環(huán)法
- 單邊循環(huán)法
1、雙邊循環(huán)法
對(duì)以下數(shù)列按從小到大進(jìn)行排序:
首先,選定基準(zhǔn)元素p,并設(shè)置左右兩個(gè)指針 l 和 r
開(kāi)始循環(huán)后,從r指針開(kāi)始,讓r指針元素與基準(zhǔn)元素做比較,如果大于等于p,則r指針向左移動(dòng);如果小于p,則停止移動(dòng),換到l指針。
對(duì)于當(dāng)前數(shù)列,r指針元素為1,1<4,所以r指針停止移動(dòng),換到l指針。
換到l指針后,讓l指針元素與基準(zhǔn)元素做比較,如果小于等于p,則l指針向右移動(dòng);如果大于p,則停止移動(dòng)。
按照此思路,后續(xù)步驟如下:
實(shí)現(xiàn)代碼如下:
import java.util.Arrays; public class quickSort { public static void quickSort(int arr[],int startIndex,int endIndex){ //遞歸結(jié)束條件為startIndex大于或等于endIndex if(startIndex>=endIndex){ return; } //得到基準(zhǔn)元素位置 int pIndex=partition(arr,startIndex,endIndex); //根據(jù)基準(zhǔn)元素分兩部分進(jìn)行遞歸排序 quickSort(arr,startIndex,pIndex-1); quickSort(arr,pIndex+1,endIndex); } /* * 分治法(雙邊循環(huán)法) * arr 待排序數(shù)組 * startIndex 起始下標(biāo) * endIndex 結(jié)束下標(biāo) * */ public static int partition(int arr[],int startIndex,int endIndex) { int p=arr[startIndex];//基準(zhǔn)元素(可取隨機(jī)位置) int l=startIndex;//左指針 int r=endIndex;//右指針 while(l!=r){ //控制右指針向左移動(dòng),找到小于基準(zhǔn)元素的那個(gè)數(shù) while((l<r)&&(arr[r]>p)){ r--; } //控制左指針向右移動(dòng),找到大于基準(zhǔn)元素的那個(gè)數(shù) while((l<r)&&(arr[l]<=p)){ l++; } //交換l指針和r指針?biāo)傅脑? if(l<r){ int tmp=arr[l]; arr[l]=arr[r]; arr[r]=tmp; } } //交換基準(zhǔn)元素和重合點(diǎn)的元素 arr[startIndex]=arr[l]; arr[l]=p; return l; } public static void main(String[] args) { int arr[]={4,7,6,5,3,2,8,1}; quickSort(arr,0,7); System.out.println(Arrays.toString(arr)); } }
2、單邊循環(huán)法
雙邊循環(huán)更加直觀,但代碼比較麻煩,而單邊循環(huán)法從數(shù)列的一邊對(duì)元素進(jìn)行遍歷交換。
開(kāi)始循環(huán)選定基準(zhǔn)元素p,再設(shè)置一個(gè)mark指針指向數(shù)列起始位置,mark代表著小于基準(zhǔn)元素區(qū)域的右邊界。
從基準(zhǔn)元素的下一位開(kāi)始遍歷,若元素大于基準(zhǔn)元素,則繼續(xù)往后遍歷。如果小于基準(zhǔn)元素,先將mark指針右移一位,然后將最新遍歷的元素與基準(zhǔn)元素交換。
單邊循環(huán)法與雙邊循環(huán)法主要是partition函數(shù)的實(shí)現(xiàn)不一樣
/* * 分治法(單邊循環(huán)法) * arr 待排序數(shù)組 * startIndex 起始下標(biāo) * endIndex 結(jié)束下標(biāo) * */ public static int partition(int arr[],int startIndex,int endIndex) { int p=arr[startIndex];//基準(zhǔn)元素(可取隨機(jī)位置) int mark=startIndex; for(int i=startIndex+1;i<=endIndex;i++){ if(arr[i]<arr[mark]){ mark++; int tmp=arr[mark]; arr[mark]=arr[i]; arr[i]=tmp; } } //交換基準(zhǔn)元素和mark指針的元素 arr[startIndex]=arr[mark]; arr[mark]=p; return mark; }
可以看出,單邊循環(huán)法只需要一個(gè)循環(huán)即可,比雙邊循環(huán)法要簡(jiǎn)單很多。
附:算法優(yōu)化
快速排序在最壞情況下,時(shí)間復(fù)雜度竟然達(dá)到了O(n2),這哪里快速啊,所以下面就要進(jìn)行優(yōu)化了。
優(yōu)化基準(zhǔn)的選取
共有兩種方案: 1??隨機(jī)選取基準(zhǔn)法,這要是倒霉起來(lái),依然有可能會(huì)次次都隨機(jī)選到最極端最壞的情況,所以這個(gè)不用。 2??三數(shù)取中法,這個(gè)可以保證不會(huì)讓你選到最極端最壞的情況。
三數(shù)取中法:在上面的算法中,我們的基準(zhǔn)選取的都是left下標(biāo),
而三數(shù)取中指的是在left,right,mid( (left + right)/2 )這三個(gè)下標(biāo)在中選取一個(gè)中間值作為基準(zhǔn),不是最大也不是最小,就保證了不會(huì)出現(xiàn)極端情況。
出現(xiàn)了以上的最壞情況,也就是讓快速排序變成了二分查找。
private static int minThree(int[]array,int left,int right) { //三數(shù)取中法,優(yōu)化遞歸實(shí)現(xiàn)的快速排序 //使得最壞情況時(shí),快速排序變?yōu)槎植檎? int mid = (left+right)/2; if(array[right] > array[left]) { int tmp = array[left]; array[left] = array[right]; array[right] = tmp; } if(array[mid] > array[left]) { return left; } if(array[mid] > array[right]) { return mid; } return right; }
優(yōu)化少量數(shù)據(jù)時(shí)的排序方案
數(shù)據(jù)量大時(shí)就像二叉樹(shù)一樣,每一組數(shù)據(jù)往下走一層都會(huì)被分成兩組,而到了最后幾層,則會(huì)因?yàn)閿?shù)據(jù)量的龐大而被分成許多組進(jìn)行遞歸,此時(shí)的遞歸開(kāi)銷(xiāo)就會(huì)很大,很有可能導(dǎo)致~~棧溢出~~,
因此我們可以設(shè)定一個(gè)數(shù)量閘口,當(dāng)每組的數(shù)據(jù)小的到了這個(gè)閘口,就采用比較簡(jiǎn)單的直接插入排序。
而且在快速排序的不斷遞歸下,數(shù)據(jù)一定是越來(lái)越有序的,直接插入排序的效率也會(huì)更高。
數(shù)據(jù)小時(shí)
此時(shí)即便是一開(kāi)始就用直接插入排序,時(shí)間也會(huì)相差無(wú)幾。
優(yōu)化后的完整代碼
public class QuickSort { /** * 快速排序 * 時(shí)間復(fù)雜度:代碼未優(yōu)化時(shí):最好情況(滿二叉樹(shù)或完全二叉樹(shù)):O(n*logn), 最壞情況(順序和逆序時(shí)):O(n^2) * 空間復(fù)雜度:代碼未優(yōu)化時(shí):最好情況(滿二叉樹(shù)或完全二叉樹(shù)):O(logn), 最壞情況(順序和逆序時(shí)):O(n) * 穩(wěn)定性:不穩(wěn)定 * @param array */ public static void quickSort(int[] array) { quick(array,0, array.length-1); } private static void quick(int[] array,int left,int right) { if(left >= right) { return; } // 設(shè)置數(shù)量閘口, // 數(shù)量小,使用直接插入排序 if(right - left + 1 < 14) { InsertSort(array); return; } // 將三數(shù)取中法取得的中間值換到left處 swap(array,minThree(array,left,right),left); int piovt = partition(array,left,right); quick(array, left, piovt-1); quick(array,piovt+1,right); } //挖坑法 private static int partition(int[] array,int left,int right) { // 在left下標(biāo)挖一個(gè)坑 int tmp = array[left]; while (left < right) { // 讓right下標(biāo)去找比tmp小的數(shù) while (right > left && array[right] >= tmp) { right--; } // 填left下標(biāo)的坑,此時(shí)right下標(biāo)變成一個(gè)坑了 array[left] = array[right]; // 讓left下標(biāo)去找比tmp大的數(shù) while (left < right && array[left] <= tmp) { left++; } // 填right下標(biāo)的坑,此時(shí)left下標(biāo)變成一個(gè)坑了 array[right] = array[left]; } // 將基準(zhǔn)值放到合適的位置 array[left] = tmp; // 返回基準(zhǔn)下標(biāo) return left; } //Hoare法 private static int partition3(int[] array,int left,int right) { // 基準(zhǔn)值 int tmp = array[left]; // 基準(zhǔn)下標(biāo) int index = left; while (left < right) { // 讓right找比tmp小的數(shù) while (right > left && array[right] >= tmp) { right--; } // 讓left找比tmp大的數(shù) while (left < right && array[left] <= tmp) { left++; } // 讓left與right這兩個(gè)數(shù)進(jìn)行交換 swap(array,left,right); } // 將基準(zhǔn)值放到合適的位置 swap(array,index,right); // 返回基準(zhǔn)下標(biāo) return right; } //前后指針?lè)? private static int partition2(int[] array, int left, int right) { int prev = left ; int cur = left+1; while (cur <= right) { if(array[cur] < array[left] && array[++prev] != array[cur]) { swap(array,cur,prev); } cur++; } swap(array,prev,left); return prev; } private static int minThree(int[]array,int left,int right) { //三數(shù)取中法,優(yōu)化遞歸實(shí)現(xiàn)的快速排序 //使得最壞情況時(shí),快速排序變?yōu)槎植檎? int mid = (left+right)/2; if(array[right] > array[left]) { int tmp = array[left]; array[left] = array[right]; array[right] = tmp; } if(array[mid] > array[left]) { return left; } if(array[mid] > array[right]) { return mid; } return right; } private static void swap(int[] array,int a,int b) { int tmp = array[a]; array[a] = array[b]; array[b] = tmp; } }
總結(jié)
到此這篇關(guān)于Java經(jīng)典算法之快速排序詳解的文章就介紹到這了,更多相關(guān)Java快速排序內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot數(shù)據(jù)訪問(wèn)和數(shù)據(jù)視圖的使用方式詳解
這篇文章主要為大家介紹了springboot數(shù)據(jù)訪問(wèn)和數(shù)據(jù)視圖的使用方式詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06如何使用Jenkins構(gòu)建GIT+Maven項(xiàng)目
這篇文章主要介紹了如何使用Jenkins構(gòu)建GIT+Maven項(xiàng)目,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09java List出現(xiàn)All elements are null問(wèn)題及解決
這篇文章主要介紹了java List出現(xiàn)All elements are null問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-11-11Mybatis-Plus自動(dòng)生成的數(shù)據(jù)庫(kù)id過(guò)長(zhǎng)的解決
這篇文章主要介紹了Mybatis-Plus自動(dòng)生成的數(shù)據(jù)庫(kù)id過(guò)長(zhǎng)的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12mybatis中映射文件(mapper)中的使用規(guī)則
這篇文章主要介紹了mybatis中映射文件(mapper)中的使用規(guī)則,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Tomcat報(bào)錯(cuò):HTTP Status 500 (Wrapper cannot find servlet class)
這篇文章主要介紹了Tomcat報(bào)錯(cuò):HTTP Status 500 (Wrapper cannot find servlet class)解決辦法的相關(guān)資料,需要的朋友可以參考下2016-11-11