Java經(jīng)典算法之快速排序詳解
一、什么是快速排序
快速排序是由冒泡排序演變而來,比冒泡排序更快的排序算法。之所以快,是因?yàn)榭焖倥判蛴昧?strong>分治法。
相同的是,與冒泡排序一樣,快速排序也屬于交換排序,通過元素之間的比較和交換來排序。
不同的是,冒泡排序每一輪只把一個(gè)元素冒泡到數(shù)列的一端,而快速排序每輪挑選一個(gè)基準(zhǔn)元素,讓比它小的元素移動到一端,讓比它大的元素移動到另一端,從而把數(shù)列拆解成兩個(gè)部分。

這種每次將數(shù)列分為兩個(gè)部分的方法就叫做分治法。
分治法的好處體現(xiàn)在相比于冒泡排序它會有更小的時(shí)間復(fù)雜度,對于n的元素的冒泡排序時(shí)間復(fù)雜度為O(n2),而快速排序總體的平均時(shí)間復(fù)雜度為O(nlogn)。
二、基準(zhǔn)元素的選擇
在使用分治法的過程中以基準(zhǔn)元素為中心把其他元素移到它的兩邊,那么如何選擇基準(zhǔn)元素呢?
1、選擇第一個(gè)元素
最簡單的方法是直接選擇數(shù)列第一個(gè)元素為基準(zhǔn)元素,但是這種方法在有些特殊情況下會出現(xiàn)問題:

對于這種原本是逆序的數(shù)列,每輪都只能確定基準(zhǔn)元素的位置,這種情況下快速排序需要進(jìn)行n輪,時(shí)間復(fù)雜度退化到了O(n2)。
2、隨機(jī)選擇
為了解決時(shí)間復(fù)雜度過大的情況,我們可以隨機(jī)選擇一個(gè)元素,并與首位元素互換位置,雖然這種情況下也有幾率選到數(shù)列的最大或最小值,但大多情況下都是可以的。
所以,雖然快速排序的平均時(shí)間復(fù)雜度為O(nlogn),但最壞情況下也可以達(dá)到O(n2)。
三、元素的交換
選好了基準(zhǔn)元素,就要將其他元素移到基準(zhǔn)元素兩邊,具體實(shí)現(xiàn)有兩種方法:
- 雙邊循環(huán)法
- 單邊循環(huán)法
1、雙邊循環(huán)法
對以下數(shù)列按從小到大進(jìn)行排序:

首先,選定基準(zhǔn)元素p,并設(shè)置左右兩個(gè)指針 l 和 r

開始循環(huán)后,從r指針開始,讓r指針元素與基準(zhǔn)元素做比較,如果大于等于p,則r指針向左移動;如果小于p,則停止移動,換到l指針。
對于當(dāng)前數(shù)列,r指針元素為1,1<4,所以r指針停止移動,換到l指針。
換到l指針后,讓l指針元素與基準(zhǔn)元素做比較,如果小于等于p,則l指針向右移動;如果大于p,則停止移動。
按照此思路,后續(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){
//控制右指針向左移動,找到小于基準(zhǔn)元素的那個(gè)數(shù)
while((l<r)&&(arr[r]>p)){
r--;
}
//控制左指針向右移動,找到大于基準(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ù)列的一邊對元素進(jìn)行遍歷交換。
開始循環(huán)選定基準(zhǔn)元素p,再設(shè)置一個(gè)mark指針指向數(shù)列起始位置,mark代表著小于基準(zhǔn)元素區(qū)域的右邊界。
從基準(zhǔn)元素的下一位開始遍歷,若元素大于基準(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)法要簡單很多。
附:算法優(yōu)化
快速排序在最壞情況下,時(shí)間復(fù)雜度竟然達(dá)到了O(n2),這哪里快速啊,所以下面就要進(jìn)行優(yōu)化了。
優(yōu)化基準(zhǔn)的選取
共有兩種方案: 1??隨機(jī)選取基準(zhǔn)法,這要是倒霉起來,依然有可能會次次都隨機(jī)選到最極端最壞的情況,所以這個(gè)不用。 2??三數(shù)取中法,這個(gè)可以保證不會讓你選到最極端最壞的情況。
三數(shù)取中法:在上面的算法中,我們的基準(zhǔn)選取的都是left下標(biāo),
而三數(shù)取中指的是在left,right,mid( (left + right)/2 )這三個(gè)下標(biāo)在中選取一個(gè)中間值作為基準(zhǔn),不是最大也不是最小,就保證了不會出現(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ù)據(jù)往下走一層都會被分成兩組,而到了最后幾層,則會因?yàn)閿?shù)據(jù)量的龐大而被分成許多組進(jìn)行遞歸,此時(shí)的遞歸開銷就會很大,很有可能導(dǎo)致~~棧溢出~~,
因此我們可以設(shè)定一個(gè)數(shù)量閘口,當(dāng)每組的數(shù)據(jù)小的到了這個(gè)閘口,就采用比較簡單的直接插入排序。
而且在快速排序的不斷遞歸下,數(shù)據(jù)一定是越來越有序的,直接插入排序的效率也會更高。
數(shù)據(jù)小時(shí)
此時(shí)即便是一開始就用直接插入排序,時(shí)間也會相差無幾。
優(yōu)化后的完整代碼
public class QuickSort {
/**
* 快速排序
* 時(shí)間復(fù)雜度:代碼未優(yōu)化時(shí):最好情況(滿二叉樹或完全二叉樹):O(n*logn), 最壞情況(順序和逆序時(shí)):O(n^2)
* 空間復(fù)雜度:代碼未優(yōu)化時(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;
}
//前后指針法
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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot數(shù)據(jù)訪問和數(shù)據(jù)視圖的使用方式詳解
這篇文章主要為大家介紹了springboot數(shù)據(jù)訪問和數(shù)據(jù)視圖的使用方式詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06
如何使用Jenkins構(gòu)建GIT+Maven項(xiàng)目
這篇文章主要介紹了如何使用Jenkins構(gòu)建GIT+Maven項(xiàng)目,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09
java List出現(xiàn)All elements are null問題及解決
這篇文章主要介紹了java List出現(xiàn)All elements are null問題及解決方案,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-11-11
Mybatis-Plus自動生成的數(shù)據(jù)庫id過長的解決
這篇文章主要介紹了Mybatis-Plus自動生成的數(shù)據(jù)庫id過長的解決,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
mybatis中映射文件(mapper)中的使用規(guī)則
這篇文章主要介紹了mybatis中映射文件(mapper)中的使用規(guī)則,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
Tomcat報(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

