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