快速排序逐行代碼精析
1.定義
快速排序有三要素:
- 分區算法【如何將一個無序的數組分區,纔是快速排序的關鍵】
- 遞歸【分治法解決問題】
- 合併問題【其實不用合併數組,因爲數組在遞歸的時候就已經完全有序了】
2.算法思想
- 利用分治的方法【表現形式是遞歸】將一個數組由一個樞軸元素分成兩部分。
樞軸的左部分比樞軸元素小,樞軸元素的右部分比樞軸元素大。【正確的描述應該是:樞軸左邊的元素不比樞軸大,樞軸右邊的元素不比樞軸小。】 - 取某無序數組區間的第一個元素【下標爲
0(不一定是0,樞軸元素下標是low,只有第一次遞歸時,纔是0)開始計算】爲樞軸元素,記爲pivotKey
。 - 初始時:
low = 0,high = array.length-1
【是否減一,則要看數組的存儲是從0開始,還是1開始。】; - 從
high
開始依次向左依次遍歷數組,如果low<high && array[high] > pivotKey
,則執行high--
;反之,退出while循環; - 從
low
開始依次向右遍歷數組,如果low<high && array[low] < pivotKey
,則執行low++
;反之,退出while循環; - 判斷low是否小於等於high,若是,則將
array[low]
與array[high]
交換;否則,不交換。 - 當
low==high
時,一輪排序結束,返回當前樞軸元素所在的下標。 - 樞軸元素下標左部分和右部分的無序數組再次進行排序。直至全部完成。
3.代碼
3.1 僞代碼
因爲僞代碼好理解,方便記憶,所以先給出文字版的僞代碼,然後再給出具體代碼。
- 僞代碼
#include<iostream>
using namespace std;
int arr[maxN];//初始數組
//0.待排序的區間是 [low,high]
void quickSort(int low,int high){
/*step 1.臨界返回條件
01.如果只有一個元素(low==high),就不用排序了,直接返回就可以了。
02.如果區間low甚至比high還要大,肯定也是要返回的。出現這種情況的原因是:
每次排序只將一個元素正確的放到位置上。如果對於原本就有序的序列,在將high變成i-1時,就可能比low小了
(同樣的道理,i+1可能比high大)
*/
if(low >= high)
return ;
/*
step 2.移動元素,做區間的劃分
01.選樞軸元素,做左右區間的劃分
02.【重要】不能污染low,high,因爲後面還要遞歸調用。所以賦值爲i,j即可。
03.【重要】使用三個while循環,每次循環成立的條件都必須有 i<=j。
同時需要注意,左側不小於時停止while循環,右側不大於時停止while循環。
在代碼中的體現就是,while循環中的條件沒有等於號。
04.只有在i <= j時,才交換兩個數 (等於號可以不要)
*/
/*
step 3.遞歸對左右子區間排序
*/
quickSort(low,i-1);
quickSort(i+1,high);
}
- 具體代碼模板【記下來!】
#include<iostream>
using namespace std;
const int maxN = 100;
int arr[maxN];//初始數組
void quickSort(int low,int high){
if(low >= high)
return ;
int pivotKey = arr[low];
int i = low,j = high;
while( i < j) {
while(arr[i] < pivotKey && i<=j ) {
i++;
}
while(arr[j] > pivotKey && i<=j ){
j--;
}
if(i<j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
quickSort(low,i-1);
quickSort(i+1,high);
}
int main(){
int n;
cin >> n;
for(int i = 0;i< n;i++)
cin >> arr[i];
quickSort(0,n-1);
for(int i = 0;i<n;i++)
cout << arr[i]<<" ";
cout <<"\n";
}
沉痛的面試教訓
- 【20200621】 我大概在20200518號面試了一家公司,當時面試官讓我手寫快排,那真是一個字的尬!我寫成出了shit一樣的代碼。這樣的事情發生不止一次了,在我記憶裏至少有兩個面試官問過我快排的問題了,而我答得都不是很好。希望自己基礎知識能夠牢牢掌握。
#include <iostream>
using namespace std;
const int N = 5;
int arr[N] ={3,4,1,2,5};
void quickSort(int left, int right){
if(left > right)
return ;
int mid = (left+right)/2;
int tl = left,tr = right;//temp
while(tl <= tr){
//1!!!!
while(arr[tl] < arr[mid] )
tl++;//不一定執行
while(arr[tr] > arr[mid] )
tr--;
if(tr >= tl){//swap
int temp;
temp = arr[tl];
arr[tl] = arr[tr];
arr[tr]=temp;
}
}
quickSort(left,mid);
quickSort(mid+1,right);
}
int main()
{
quickSort(0,4);
for(int i = 0;i< 5;i++)
cout << arr[i] << " ";
return 0;
}
上面這段程序至少存在如下幾個問題:
bug1
快排是根據樞軸值將區間分成兩個子區間。
int mid = (left+right)/2;
……
while(arr[tl] < arr[mid])
tl++;
if(tr >= tl){
//交換操作...
}
這段代碼裏會不會出現:本身的arr[mid]
被換到了另外一個位置? 那麼問題就顯現出來:我們根據某個值劃分區間,結果這個值是個變量!!。這樣就無法把樞軸元素放到正確的位置上了。
bug2
程序會陷入死循環。
while(tl <= tr){
while(arr[tl] < arr[mid] )
tl++;//不一定執行
while(arr[tr] > arr[mid] )
tr--;
if(tr >= tl){//swap
int temp;
temp = arr[tl];
arr[tl] = arr[tr];
arr[tr]=temp;
}
}
這段while 代碼會讓程序陷入死循環。假設現在有如下區間
1 4 3,樞軸元素取1,tl = 0, tr = 2。 按照上面這個代碼,會得到tl=tr=1處的死循環,原因就在於 while(tl<=tr)
這個條件
bug3
對與分好的區間,我們接着分其子區間即可。
quickSort(left,mid-1);
quickSort(mid+1,right);
但是上面的代碼則暴露了一個問題:我並不是真正瞭解快速排序。我竟然把左區間的右端點寫成了mid-1
,天理難容!! 經過上面這個while循環,我們已經將區間根據樞軸值 pivot
分成了左右兩個部分,這個while終止的條件也一定是 tl=tr,即arr[tl]的值就是樞軸變量的值。 那麼我們的區間應該分成:
quickSort(left,tl-1);
quickSort(tl+1,right);
至此,運行一下修改後的快排,貌似可以得到正確答案了。
#include <iostream>
using namespace std;
const int N = 5;
int arr[N] ={3,4,1,2,5};
//int arr[N] ={3,4,1,2,1};
void quickSort(int left, int right){
if(left > right)
return ;
int mid = (left+right)/2;
int tl = left,tr = right;//temp
int pivot = arr[mid];//隨機一下=>取中間的值
while(tl < tr){
while(arr[tl] < pivot )
tl++;//不一定執行
while(arr[tr] > pivot )
tr--;
if(tr >= tl){//swap
int temp;
temp = arr[tl];
arr[tl] = arr[tr];
arr[tr]=temp;
}
}
quickSort(left,tl-1);
quickSort(tl+1,right);
}
int main()
{
quickSort(0,4);
for(int i = 0;i< 5;i++)
cout << arr[i] << " ";
return 0;
}
但是如果將arr[]
數組替換成 int arr[N] ={5,4,1,2,1};
,則發現程序又進入了死循環!!又是爲什麼呢?
看看下面這組測試數據,其就會得到一個死循環的結果:
這裏死循環的原因在於有相同的數字1,爲什麼有相同的數字就導致死循環呢? 因爲我們的第二重while循環存在問題。
while(tl<tr)
{
...
if(){
... // 程序就是在這裏出現了問題!!
}
}
上面程序的問題就是:在相等的情況下,如果只做交換,那麼“指針”位置相當於沒有移動,所以就導致指針不再往希望方向移動,基於此,可以把代碼修改爲如下
while(tl<tr)
{
...
if(){
... // 程序就是在這裏出現了問題!!
tr--;
tl++; //往後移動一下
}
}
最後再給出一份沒有bug的代碼:
==========
// Created by lawson on 20-6-21.
#include<iostream>
using namespace std;
int arr[5] = {3,4,1,2,1};
//int arr[5] = {4,3,1,2,5};
//int arr[3] = {1,2,1};
int partition(int left,int right){
int pivot = arr[left];//01.使用區間左端點作爲樞軸
//02.遞歸邊界是left < right => 說明最後在退出while循環時會得到:left=right
while(left < right){
/*03.思考爲什麼去掉等於號就不行了?
原因是:會導致死循環。
如果不在while()循環中越過相同值,那麼就應該在交換值的時候越過它;[上面那份代碼]
如果在while()循環中越過相同值,那麼後面在交換指的時候就可以不用執行right-- 和 left++了【本代碼】
04.必須保證先從右往左,因爲arr[left]已經保存放到pivot中了,可以覆蓋;
但是arr[right]卻是不能覆蓋的
* */
while(left < right && arr[right]>=pivot) //從右往左找第一個比它小的數
right--;
arr[left] = arr[right];
/*
01.程序運行到這裏的時候,說明 right 這個位置的數已經可以被覆蓋了,因爲它已經把數給arr[left]了,
所以後面可以開始對 left 進行一個 while 操作。直到找到一個 “合適的位置” 將left 賦值到right 中。 此時left 要麼小於right ,要麼等於right。無論在哪種條件下都是滿足交換條件的。
*/
while(left < right && arr[left]<=pivot)//從左往右找第一個比它大的數
left++;
arr[right] = arr[left]; // => 會默認保證right > left
}
arr[left] = pivot; //將pivot 的值放在一個正確的位置上
return left;//這個位置就是一個後面區間的分界點
}
void quickSort(int left,int right){
if(right <= left)
return ;
int mid = partition(left,right);
quickSort(left,mid-1);
quickSort(mid+1,right);
}
int main(){
int n = 5;
quickSort(0,n-1);
for(int i = 0;i< n;i++){
cout << arr[i]<<" ";
}
}