摘要:快速排序的核心是以基數(shù)為中心,將數(shù)組分為兩個(gè)區(qū)間,小于基數(shù)的放到基數(shù)的左邊,大于基數(shù)的放到基數(shù)的右邊??焖倥判蛟诿看瓮诳拥倪^程中,需要個(gè)空間存儲基數(shù)。而快速排序的大概需要次的處理,所以占用空間也是個(gè)。
快速排序 原理
快速排序是C.R.A.Hoare提出的一種交換排序。它采用分治的策略,所以也稱其為分治排序。
實(shí)現(xiàn)快速排序算法的關(guān)鍵在于,先在數(shù)組中選一個(gè)數(shù)作為基數(shù),接著以基數(shù)為中心將數(shù)組中的數(shù)字分為兩部分,比基數(shù)小的放在數(shù)組的左邊,比基數(shù)大的放到數(shù)組的右邊。接下來我們可以用遞歸的思想分別對基數(shù)的左右兩邊進(jìn)行排序。
整個(gè)快速排序可以總結(jié)為以下三步:
從數(shù)組中取出一個(gè)數(shù)作為基數(shù)
分區(qū),將數(shù)組分為兩部分,比基數(shù)小的放在數(shù)組的左邊,比基數(shù)大的放到數(shù)組的右邊。
遞歸使左右區(qū)間重復(fù)第二步,直至各個(gè)區(qū)間只有一個(gè)數(shù)。
樣例分析下面我用博客專家MoreWindows的挖坑填數(shù)+分治思想來分析一個(gè)實(shí)例(使用快速排序?qū)⒁韵聰?shù)組排序)
首先,取數(shù)組中的第一個(gè)數(shù)字52為第一次分區(qū)的基數(shù)
初始時(shí),start=0、end=8、base=52(start區(qū)間的頭、end區(qū)間的尾、base基數(shù))
由于將array[0]中的數(shù)據(jù)賦給了基數(shù)base,我們可以理解為在array[0]處挖了一個(gè)坑,當(dāng)然這個(gè)坑是可以用其它數(shù)據(jù)來填充的。
接著我們從end開始 向前找比base小或者等于base的數(shù)據(jù),很快我們就找了19,我們就19這個(gè)數(shù)據(jù)填充到array[0]位置,這樣array[8]位置就又形成了一個(gè)新的坑。不怕,我們接著找符合條件的數(shù)據(jù)來填充這個(gè)新坑。
start=0、end=8、base=52
然后我們再從start開始向后找大于或等于base的數(shù)據(jù),找到了88,我們用88來填充array[1],這樣array[1]位置也形成了一個(gè)新坑。 Don"t Worry 我們接著從end開始 向前找符合條件的數(shù)據(jù)來填充這個(gè)坑。
start=1、end=8、base=52
{% note success %} 需要注意的是start和end隨著查找的過程是不斷變化的。 {% endnote %}
之后一直重復(fù)上面的步驟,先從前向后找,再從后向前找。直至所有小于基數(shù)的數(shù)據(jù)都出現(xiàn)在左邊,大于基數(shù)的數(shù)據(jù)都出現(xiàn)在右邊。
最后將base中的數(shù)據(jù)填充到數(shù)組中即可完成這次分區(qū)的過程。
然后遞歸使左右區(qū)間重復(fù)分區(qū)過程,直至各個(gè)區(qū)間只有一個(gè)數(shù),即可完成整個(gè)排序過程。
{% note primary %}
快速排序的核心是以基數(shù)為中心,將數(shù)組分為兩個(gè)區(qū)間,小于基數(shù)的放到基數(shù)的左邊,大于基數(shù)的放到基數(shù)的右邊。
在第一次分區(qū)的時(shí)候我們以數(shù)組中的第一個(gè)數(shù)據(jù)為基數(shù),在array[0]處挖了一個(gè)坑,這時(shí)我們只能從區(qū)間的后面往前找小于基數(shù)的數(shù)據(jù)來填充array[0]這個(gè)坑,如果直接從前往后找符合條件的數(shù)據(jù),就會造成大于基數(shù)的數(shù)據(jù)留在了數(shù)組的左邊。
所以我們只能通過從后往前找小于基數(shù)的數(shù)據(jù)來填充前面的坑,從前往后找大于基數(shù)的數(shù)據(jù)來填充后面的坑。這樣可以保證在只遍歷一遍數(shù)組就能將數(shù)組分為兩個(gè)區(qū)間
最后一步我們用基數(shù)本身來填充數(shù)組的最后一個(gè)坑
{% endnote %}
代碼實(shí)現(xiàn):
// 分區(qū) public static int partition(int[] array, int start, int end) { int base = array[start]; while (start < end) { while (start < end && array[end] >= base) end --; array[start] = array[end]; while (start < end && array[start] <= base) start ++; array[end] = array[start]; } array[end] = base; return start; } // 快速排序 public static void quickSort(int[] array,int start, int end) { if(start < end) { int index = partition(array, start, end); quickSort(array, start, index-1); quickSort(array, index+1, end); } }
class QuickSort { public: //快速排序 int* quickSort(int* A, int n) { QSort(A,0,n-1); return A; } //對數(shù)組A[low,...,high] void QSort(int *A,int low,int high) { if(low=key) high--; A[low]=A[high]; while(low 算法分析: 當(dāng)數(shù)據(jù)有序時(shí),以第一個(gè)關(guān)鍵字為基準(zhǔn)分為兩個(gè)子序列,前一個(gè)子序列為空,此時(shí)執(zhí)行效率最差。時(shí)間復(fù)雜度為O(n^2)
而當(dāng)數(shù)據(jù)隨機(jī)分布時(shí),以第一個(gè)關(guān)鍵字為基準(zhǔn)分為兩個(gè)子序列,兩個(gè)子序列的元素個(gè)數(shù)接近相等,此時(shí)執(zhí)行效率最好。時(shí)間復(fù)雜度為O(nlogn)
所以,數(shù)據(jù)越隨機(jī)分布時(shí),快速排序性能越好;數(shù)據(jù)越接近有序,快速排序性能越差。快速排序在每次“挖坑”的過程中,需要 1 個(gè)空間存儲基數(shù)。而快速排序的大概需要 NlogN次的處理,所以占用空間也是 NlogN 個(gè)。
下面我們在來看幾種改進(jìn)的快排算法
快速排序中元素切分的方式快速排序中最重要的步驟就是將小于等于中軸元素的整數(shù)放到中軸元素的左邊,將大于中軸元素的數(shù)據(jù)放到中軸元素的右邊,這里我們把該步驟定義為"切分"。以首元素作為中軸元素,下面介紹幾種常見的"切分方式"。
兩端掃描,一端挖坑,另一端填補(bǔ)這種方法就是我們上面用到的"挖坑填數(shù)",在這再做個(gè)簡單的總結(jié)。
這種方法的基本思想是:使用兩個(gè)變量i和j,i指向最左邊的元素,j指向最右邊的元素,我們將首元素作為中軸,將首元素復(fù)制到變量pivot中,這時(shí)我們可以將首元素i所在的位置看成一個(gè)坑,我們從j的位置從右向左掃描,找一個(gè)小于等于中軸的元素A[j],來填補(bǔ)A[i]這個(gè)坑,填補(bǔ)完成后,拿去填坑的元素所在的位置j又可以看做一個(gè)坑,這時(shí)我們在以i的位置從前往后找一個(gè)大于中軸的元素來填補(bǔ)A[j]這個(gè)新的坑,如此往復(fù),直到i和j相遇(i == j,此時(shí)i和j指向同一個(gè)坑)。最后我們將中軸元素放到這個(gè)坑中。最后對左半數(shù)組和右半數(shù)組重復(fù)上述操作。
這種方法的代碼實(shí)現(xiàn)請參考上面的完整代碼。
從兩端掃描交換這種方法的基本思想是:使用兩個(gè)變量i和j,i指向首元素的元素下一個(gè)元素(最左邊的首元素為中軸元素),j指向最后一個(gè)元素,我們從前往后找,直到找到一個(gè)比中軸元素大的,然后從后往前找,直到找到一個(gè)比中軸元素小的,然后交換這兩個(gè)元素,直到這兩個(gè)變量交錯(cuò)(i > j)(注意不是相遇 i == j,因?yàn)橄嘤龅脑剡€未和中軸元素比較)。最后對左半數(shù)組和右半數(shù)組重復(fù)上述操作。
public static void QuickSort1(int[] A, int L, int R){ if(L < R){//遞歸的邊界條件,當(dāng) L == R時(shí)數(shù)組的元素個(gè)數(shù)為1個(gè) int pivot = A[L];//最左邊的元素作為中軸,L表示left, R表示right int i = L+1, j = R; //當(dāng)i == j時(shí),i和j同時(shí)指向的元素還沒有與中軸元素判斷, //小于等于中軸元素,i++,大于中軸元素j--, //當(dāng)循環(huán)結(jié)束時(shí),一定有i = j+1, 且i指向的元素大于中軸,j指向的元素小于等于中軸 while(i <= j){ while(i <= j && A[i] <= pivot){ i++; } while(i <= j && A[j] > pivot){ j--; } //當(dāng) i > j 時(shí)整個(gè)切分過程就應(yīng)該停止了,不能進(jìn)行交換操作 //這個(gè)可以改成 i < j, 這里 i 永遠(yuǎn)不會等于j, 因?yàn)橛猩鲜鰞蓚€(gè)循環(huán)的作用 if(i <= j){ Swap(A, i, j); i++; j--; } } //當(dāng)循環(huán)結(jié)束時(shí),j指向的元素是最后一個(gè)(從左邊算起)小于等于中軸的元素 Swap(A, L, j);//將中軸元素和j所指的元素互換 QuickSort1(A, L, j-1);//遞歸左半部分 QuickSort1(A, j+1, R);//遞歸右半部分 } }從一端掃描和前面兩種方法一樣,我們選取最左邊的元素作為中軸元素,A[1,i]表示小于等于pivot的部分,i指向中軸元素(i < 1),表示小于等于pivot的元素個(gè)數(shù)為0,j以后的都是未知元素(即不知道比pivot大,還是比中軸元素?。琷初始化指向第一個(gè)未知元素。
當(dāng)A[j]大于pivot時(shí),j繼續(xù)向前,此時(shí)大于pivot的部分就增加一個(gè)元素
當(dāng)A[j]小于等于pivot時(shí),我們注意i的位置,i的下一個(gè)就是大于pivot的元素,我們將i增加1然后交換A[i]和A[j],交換后小于等于pivot的部分增加1,j增加1,繼續(xù)掃描下一個(gè)。而i的下一個(gè)元素仍然大于pivot,又回到了先前的狀態(tài)。
public static void QuickSort3(int[] A, int L, int R){ if(L < R){ int pivot = A[L];//最左邊的元素作為中軸元素 //初始化時(shí)小于等于pivot的部分,元素個(gè)數(shù)為0 //大于pivot的部分,元素個(gè)數(shù)也為0 int i = L, j = L+1; while(j <= R){ if(A[j] <= pivot){ i++; Swap(A, i, j); j++;//j繼續(xù)向前,掃描下一個(gè) }else{ j++;//大于pivot的元素增加一個(gè) } } //A[i]及A[i]以前的都小于等于pivot //循環(huán)結(jié)束后A[i+1]及它以后的都大于pivot //所以交換A[L]和A[i],這樣我們就將中軸元素放到了適當(dāng)?shù)奈恢? Swap(A, L, i); QuickSort3(A, L, i-1); QuickSort3(A, i+1, R); } }快速排序的改進(jìn)方法 三向切分的快速排序三向切分快速排序的基本思想,用i,j,k三個(gè)將數(shù)組切分成四部分,a[0, i-1]表示小于pivot的部分,a[i, k-1]表示等于pivot的部分,a[j+1]之后的表示大于pivot的部分,而a[k, j]表示未進(jìn)行比較的元素(即不知道比pivot大,還是比pivot元素?。?。我們要注意a[i]始終位于等于pivot部分的第一個(gè)元素,a[i]的左邊是小于pivot的部分。
我們依舊選取最左邊的元素作為中軸元素,初始化時(shí),i = L,k = L+1,j=R(L表示最左邊元素的索引,R表示最右邊元素的索引)
通過上一段的表述可知,初始化時(shí)
j)。 這里我們掃描的目的就是為了逐個(gè)減少未知元素,并將每個(gè)元素按照和pivot的大小關(guān)系放到不同的區(qū)間上去。在k的掃描過程中我們可以對a[k]分為三種情況討論
a[k] < pivot 交換a[i]和a[k],然后i和k都自增1,k繼續(xù)掃描
a[k] = pivot k自增1,k接著繼續(xù)掃描
a[k] > pivot 這個(gè)時(shí)候顯然a[k]應(yīng)該放到最右端,大于pivot的部分。但是我們不能直接將a[k]與a[j]交換,因?yàn)槟壳癮[j]和pivot的關(guān)系未知,所以我們這個(gè)時(shí)候應(yīng)該從j的位置自右向左掃描。而a[j]與pivot的關(guān)系可以繼續(xù)分為三種情況討論
a[j] > pivot --- j自減1,j接著繼續(xù)掃描
a[j] = pivot --- 交換a[k]和a[j],k自增1,j自減1,k繼續(xù)掃描(注意此時(shí)j的掃描就結(jié)束了)
a[j] < pivot --- 此時(shí)我們注意到a[j] < pivot, a[k] > pivot, a[i] == pivot,那么我們只需要將a[j]放到a[i]上,a[k]放到a[j]上,而a[i]放到a[k]上。然后i和k自增1,j自減1,k繼續(xù)掃描(注意此時(shí)j的掃描就結(jié)束了)
注意,當(dāng)掃描結(jié)束時(shí),i和j的表示了=等于pivot部分的起始位置和結(jié)束位置。我們只需要對小于pivot的部分以及大于pivot的部分重復(fù)上述操作即可。public static void QuickSort3Way(int[] A, int L, int R){ if(L >= R){//遞歸終止條件,少于等于一個(gè)元素的數(shù)組已有序 return; } int i,j,k,pivot; pivot = A[L]; //首元素作為中軸 i = L; k = L+1; j = R; OUT_LOOP: while(k <= j){ if(A[k] < pivot){ Swap(A, i, k); i++; k++; }else if(A[k] == pivot){ k++; }else{// 遇到A[k]>pivot的情況,j從右向左掃描 while(A[j] > pivot){//A[j]>pivot的情況,j繼續(xù)向左掃描 j--; if(j < k){ break OUT_LOOP; } } if(A[j] == pivot){//A[j]==pivot的情況 Swap(A, k, j); k++; j--; }else{//A[j]雙軸快速排序 雙軸快速排序算法的思路和上面的三向切分快速排序算法的思路基本一致,雙軸快速排序算法使用兩個(gè)軸元素,通常選取最左邊的元素作為pivot1和最右邊的元素作pivot2。首先要比較這兩個(gè)軸的大小,如果pivot1 > pivot2,則交換最左邊的元素和最右邊的元素,已保證pivot1 <= pivot2。雙軸快速排序同樣使用i,j,k三個(gè)變量將數(shù)組分成四部分
A[L+1, i]是小于pivot1的部分,A[i+1, k-1]是大于等于pivot1且小于等于pivot2的部分,A[j, R]是大于pivot2的部分,而A[k, j-1]是未知部分。和三向切分的快速排序算法一樣,初始化i = L,k = L+1,j=R,k自左向右掃描直到k與j相交為止(k == j)。我們掃描的目的就是逐個(gè)減少未知元素,并將每個(gè)元素按照和pivot1和pivot2的大小關(guān)系放到不同的區(qū)間上去。
在k的掃描過程中我們可以對a[k]分為三種情況討論(注意我們始終保持最左邊和最右邊的元素,即雙軸,不發(fā)生交換)
a[k] < pivot1 i先自增,交換a[i]和a[k],k自增1,k接著繼續(xù)掃描
a[k] >= pivot1 && a[k] <= pivot2 k自增1,k接著繼續(xù)掃描
a[k] > pivot2: 這個(gè)時(shí)候顯然a[k]應(yīng)該放到最右端大于pivot2的部分。但此時(shí),我們不能直接將a[k]與j的下一個(gè)位置a[--j]交換(可以認(rèn)為A[j]與pivot1和pivot2的大小關(guān)系在上一次j自右向左的掃描過程中就已經(jīng)確定了,這樣做主要是j首次掃描時(shí)避免pivot2參與其中),因?yàn)槟壳癮[--j]和pivot1以及pivot2的關(guān)系未知,所以我們這個(gè)時(shí)候應(yīng)該從j的下一個(gè)位置(--j)自右向左掃描。而a[--j]與pivot1和pivot2的關(guān)系可以繼續(xù)分為三種情況討論
a[--j] > pivot2 j接著繼續(xù)掃描
a[--j] >= pivot1且a[j] <= pivot2 交換a[k]和a[j],k自增1,k繼續(xù)掃描(注意此時(shí)j的掃描就結(jié)束了)
a[--j] < pivot1 先將i自增1,此時(shí)我們注意到a[j] < pivot1, a[k] > pivot2, pivot1 <= a[i] <=pivot2,那么我們只需要將a[j]放到a[i]上,a[k]放到a[j]上,而a[i]放到a[k]上。k自增1,然后k繼續(xù)掃描(此時(shí)j的掃描就結(jié)束了)
注意pivot1和pivot2在始終不參與k,j掃描過程。
掃描結(jié)束時(shí),A[i]表示了小于pivot1部分的最后一個(gè)元素,A[j]表示了大于pivot2的第一個(gè)元素,這時(shí)我們只需要交換pivot1(即A[L])和A[i],交換pivot2(即A[R])與A[j],同時(shí)我們可以確定A[i]和A[j]所在的位置在后續(xù)的排序過程中不會發(fā)生變化(這一步非常重要,否則可能引起無限遞歸導(dǎo)致的棧溢出),最后我們只需要對A[L, i-1],A[i+1, j-1],A[j+1, R]這三個(gè)部分繼續(xù)遞歸上述操作即可。
public static void QuickSortDualPivot(int[] A, int L, int R){ if(L >= R){ return; } if(A[L] > A[R]){ Swap(A, L, R); //保證pivot1 <= pivot2 } int pivot1 = A[L]; int pivot2 = A[R]; //如果這樣初始化 i = L+1, k = L+1, j = R-1,也可以 //但代碼中邊界條件, i,j先增減,循環(huán)截止條件,遞歸區(qū)間的邊界都要發(fā)生相應(yīng)的改變 int i = L; int k = L+1; int j = R; OUT_LOOP: while(k < j){ if(A[k] < pivot1){ i++;//i先增加,首次運(yùn)行pivot1就不會發(fā)生改變 Swap(A, i, k); k++; }else if(A[k] >= pivot1 && A[k] <= pivot2){ k++; }else{ while(A[--j] > pivot2){//j先增減,首次運(yùn)行pivot2就不會發(fā)生改變 if(j <= k){//當(dāng)k和j相遇 break OUT_LOOP; } } if(A[j] >= pivot1 && A[j] <= pivot2){ Swap(A, k, j); k++; }else{ i++; Swap(A, j, k); Swap(A, i, k); k++; } } } Swap(A, L, i);//將pivot1交換到適當(dāng)位置 Swap(A, R, j);//將pivot2交換到適當(dāng)位置 //一次雙軸切分至少確定兩個(gè)元素的位置,這兩個(gè)元素將整個(gè)數(shù)組區(qū)間分成三份 QuickSortDualPivot(A, L, i-1); QuickSortDualPivot(A, i+1, j-1); QuickSortDualPivot(A, j+1, R); }下面代碼初始化方式使得邊界條件更加容易確定,在下面代碼的方式中A[L+1]~A[i-1]表示小于pivot1的部分,A[i]~A[j]表示兩軸之間的部分,A[j]~A[R-1]表示大于pivot2的部分。
當(dāng)排序數(shù)組中不存在pivot1~pivot2中的部分時(shí),一趟交換完成后i恰好比j大1;當(dāng)排序數(shù)組中僅僅存在一個(gè)元素x使得pivot1 <=x <=pivot2時(shí),一趟交換完成,i和j恰好相等。
public static void QuickSortDualPivot(int[] A, int L, int R){ if(L >= R){ return; } if(A[L] > A[R]){ Swap(A, L, R); //保證pivot1 <= pivot2 } int pivot1 = A[L]; int pivot2 = A[R]; int i = L+1; int k = L+1; int j = R-1; OUT_LOOP: while(k <= j){ if(A[k] < pivot1){ Swap(A, i, k); k++; i++; }else if(A[k] >= pivot1 && A[k] <= pivot2){ k++; }else{ while(A[j] > pivot2){ j--; if(j < k){//當(dāng)k和j錯(cuò)過 break OUT_LOOP; } } if(A[j] >= pivot1 && A[j] <= pivot2){ Swap(A, k, j); k++; j--; }else{//A[j] < pivot1 Swap(A, j, k);//注意k不動(dòng) j--; } } } i--; j++; Swap(A, L, i);//將pivot1交換到適當(dāng)位置 Swap(A, R, j);//將pivot2交換到適當(dāng)位置 //一次雙軸切分至少確定兩個(gè)元素的位置,這兩個(gè)元素將整個(gè)數(shù)組區(qū)間分成三份 QuickSortDualPivot(A, L, i-1); QuickSortDualPivot(A, i+1, j-1); QuickSortDualPivot(A, j+1, R); }
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/69484.html
摘要:優(yōu)化當(dāng)待排序序列長度時(shí),使用插入排序,可以加速排序。插入排序原理通過構(gòu)建有序序列,對于未排序元素,在已排序序列中從后向前掃描,找到相應(yīng)位置并插入。堆排序可通過樹形結(jié)構(gòu)保存部分比較結(jié)果,可減少比較次數(shù)。 前端學(xué)習(xí):教程&開發(fā)模塊化/規(guī)范化/工程化/優(yōu)化&工具/調(diào)試&值得關(guān)注的博客/Git&面試-前端資源匯總 歡迎提issues斧正:排序算法 JavaScript-排序算法及簡易優(yōu)化 快速...
摘要:如果語句中使用了子查詢集合操作臨時(shí)表等情況,會給列帶來很大的復(fù)雜性。會遞歸執(zhí)行這些子查詢,把結(jié)果放在臨時(shí)表里。查詢優(yōu)化器從中所選擇使用的索引。該字段顯示了查詢優(yōu)化器通過系統(tǒng)收集的統(tǒng)計(jì)信息估算出來的結(jié)果集記錄條數(shù)。 引言 優(yōu)化SQL,是DBA常見的工作之一。如何高效、快速地優(yōu)化一條語句,是每個(gè)DBA經(jīng)常要面對的一個(gè)問題。在日常的優(yōu)化工作中,我發(fā)現(xiàn)有很多操作是在優(yōu)化過程中必不可少的步驟。然...
閱讀 2665·2021-11-22 15:24
閱讀 1402·2021-11-17 09:38
閱讀 2790·2021-10-09 09:57
閱讀 1234·2019-08-30 15:44
閱讀 2474·2019-08-30 14:00
閱讀 3565·2019-08-30 11:26
閱讀 2957·2019-08-29 16:28
閱讀 773·2019-08-29 13:56