【快速排序】★★★★★
一、快速排序的簡介
快速排序是一種總體上來講時間複雜度較低的排序,其主要利用了分冶的思想;在排序一大段資料時,每次通過選取key值,然後利用不同的方法將該段資料分為兩段(小於等於key的一段在一邊,大於key的一段在一邊,key的資料在這兩段的中間);然後通過遞迴的方法分別對上述的左右兩段資料採用同樣的思想分段;快速排序每一趟下來,位於兩段中間的key值就會被置於最終排序時該資料的正確位置,就是說快速排序每經過一趟排序,排好一個元素;快速排序遞迴的返回條件是,當某段資料只剩下一個數據或者沒有資料時,那麼遞迴返回;
快速排序排的是一個左右閉合區間;
快排每一趟只將一個數排好,放在正確的位置;
key能選最左邊也能選最右邊
當一組數為有序時使用快排對這組數排序,此時為快排的最壞情況;時間複雜度最高,為O(N^2);
二、學習快速排序的要點
(1)快速排序的實現方法
①遞迴的方法
- 左右指標法
- 挖坑法
- 前後指標法
②非遞迴的方法
利用容器介面卡實現快速排序的非遞迴方法
(2)快速排序遞迴方法的優化
三數取中法
為了避免快速排序的最壞情況的出現,就是快速排序是取key值時取到最大值或者最小值;
利用三數取中法,可以保證key不會取到最大值和最小值;這樣就不會出現快排的最壞情況,時間複雜度為O(N^2)的情況;小區間優化法
快排的遞迴實現,在遞迴時會進行函式的壓棧開銷很大,如果對於一堆很多的資料,完全採用遞迴的快排進行排序,那麼遞迴的越深,出現的小區間越多,函式的壓棧開銷越大,從而讓快排的時間複雜度更高,而且,對於小區間的較少資料的排序,利用快排實際和利用插入排序的時間效率差不多,可能插入排序更高效,所以可以對快排的演算法遞迴到一定的程度的小區間時,不在利用快排的原理進行排序,而是利用插入排序進行排序,這樣就會沒有大的函式壓棧的時間空間開銷,讓快排整體來看效率更高;(3)STLc++標準模板庫中sort()演算法的底層實現就是利用使用了三數取中和小區間優化後的快速排序實現的;
三、快速排序的詳細講解
(1)左右指標法
演算法步驟
1>選取基準值(key)—我選資料集合的最右邊的數
2>定義要排序的區間的兩端的位置,分別用left和right表示;
3>left從左邊第一個數先開始向右尋找大於key的數,直到找到後停止
4>right從右邊第一個數開始向左尋找小於key的數,直到找到後停止
5>將left位置處的資料和right中的資料交換;
6>left繼續向右找,right繼續向左找,直到left和right相遇跳出迴圈
7>將相遇位置的資料和Key值位置的資料進項交換,一趟快速排序完成,現在key值所呆的位置就是全部排序玩key所呆的位置;
此時key值所在位置的左邊全是小於key得資料;key值所在位置的右邊全是大於key的資料
8>遞迴地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。圖說
程式實現
#include<iostream>
#include<assert.h>
using namespace std;
void QuickSort1(int* arr,int left,int right)
{
int key=right;
int begin=left;
int end=right;
while (left>=right)//遞迴返回條件
{
return ;
}
while(left<right)
{
while (left<right&&arr[left]<=arr[key])//從左邊開始找到比基準值大的數停下來
{
++left;
}
while (right>left&&arr[right]>=arr[key])//從右邊開始找到比基準值小的數停下來
{
--right;
}
if (left<right&&arr[left]!=arr[right])//如果左右指標都停下來了,並且不是同一個值,自身和自身交換,沒意義,降低效率
{
swap(arr[left],arr[right]);
}
}
//一趟排序完成,此時left=right,交換相遇點位置的資料和key位置的資料
swap(arr[left],arr[key]);//不論最後一次是誰主動遇見在等的誰,每次最後一次用相遇點和key作交換,都是用大值與key交換;
//遞迴地把小於基準值元素的子數列和大於基準值元素的子數列排序
QuickSort1(arr,begin,left-1);
QuickSort1(arr,left+1,end);
}
void Printf(int* a,int sz)
{
assert(sz>0);
for (int i=0;i<sz;++i)
{
cout<<a[i]<<" ";
}
cout<<endl;
}
int main()
{
int a[]={2,0,4,9,3,6,8,7,1,5};
int sz=sizeof(a)/sizeof(a[0]);
QuickSort1(a,0,sz-1);
Printf(a,sz);
return 0;
}
4.測試結果
(2)挖坑法
1.演算法實現步驟
1>用兩個位置left和right用來標識區間範圍,初始坑設定到key值的地方。由於我將key值定義為區間最右邊的值,所以要左指標開始走。hollow記錄坑的位置;
2>兩個位置left和right;首先是left從左邊向右邊開始找比key坑位置資料大的資料;找到後,將left所在位置的資料填入key坑中;此時left變為坑;
3>然後right從右向左開始尋找比k值小的值,找到以後,直接將此值填入left坑中,這時right有變為坑;
4>重複3,4部操作,直到left和right相遇,一趟快排完成;
此時比key值小的資料全部在key的左邊,比key大的值全在key的右邊。
5>利用遞迴把小於基準值key元素的子數列和大於基準值元素的子數列排序。
2.圖說
3.程式碼實現
#include<iostream>
#include<assert.h>
using namespace std;
void QuickSort2(int* arr,int left,int right)
{
assert(arr);
if (left>=right)
{
return ;
}
int key=arr[right];//健值
int hollow=right;//記錄坑的位置
int begin=left;
int end=right;
while (left<right)
{
while (left<right&&arr[left]<=key)//尋找大於健值的資料
{
++left;
}
//出來以後left找到了大於健值的資料,將其填入當前坑中;之後left的當前位置變為坑
arr[hollow]=arr[left];//填坑
hollow=left;//更新坑的位置
while(right>left&&arr[right]>=key)//尋找小於健值的資料
{
--right;
}
arr[hollow]=arr[right];//填坑
hollow=right;//更新坑的位置
//出來以後right找到了小於健值的資料,將其填入當前left坑中;之後right的當前位置變為坑
}
//迴圈結束以後,left和right相遇;此時將key值填入當前的坑中,一趟快排完成
if (left==right)
{
arr[left]=key;
}
QuickSort2(arr,begin,left-1);
QuickSort2(arr,left+1,end);
}
void Printf(int* a,int sz)
{
assert(sz>0);
for (int i=0;i<sz;++i)
{
cout<<a[i]<<" ";
}
cout<<endl;
}
int main()
{
int a[]={1,7,8,4,2,3,6,5};
int sz=sizeof(a)/sizeof(a[0]);
QuickSort2(a,0,sz-1);
Printf(a,sz);
return 0;
}
4.測試結果
5.挖坑法的程式碼優化
用swap代替賦值;少去最後一步給相遇點賦值key值,提高程式效能
//挖坑法優化--用swap代替賦值;少去最後一步給相遇點賦值key值,提高程式效能
void QuickSort22(int* arr,int left,int right)
{
assert(arr);
if (left>=right)
{
return ;
}
int key=arr[right];//健值
int hollow=right;//記錄坑的位置
int begin=left;
int end=right;
while (left<right)
{
while (left<right&&arr[left]<=key)//尋找大於健值的資料
{
++left;
}
//出來以後left找到了大於健值的資料,將其填入當前坑中;之後left的當前位置變為坑
swap(arr[hollow],arr[left]);//填坑
hollow=left;//更新坑的位置
while(right>left&&arr[right]>=key)//尋找小於健值的資料
{
--right;
}
swap(arr[hollow],arr[right]);//填坑
hollow=right;//更新坑的位置
//出來以後right找到了小於健值的資料,將其填入當前left坑中;之後right的當前位置變為坑
}
QuickSort2(arr,begin,left-1);
QuickSort2(arr,left+1,end);
}
6.分析賦值挖坑法和交換挖坑法:
其實賦值挖坑法只是每次將找到的大於或者小於的數賦值給了當前的坑,這就勢必導致在陣列中,第一個被賦值的數,也就是key的值,被前一個數覆蓋,在陣列中找到不key;也就是說每次賦值完以後,其實就是當前被賦值的坑儲存了下一個坑的值,那麼最後一個坑的值會被他的前一個坑儲存,所以,在left和right相遇的坑即最後一個坑的值會在陣列中出現兩個,所以要把這個數賦值為key;
那麼交換賦值法,是每次把找到的數和坑值交換,這就保證陣列 中不會多數也不會少數,每次更新的坑都儲存key,最後不用那key賦值給最後的坑;
(3)前後指標法
1.演算法步驟:
1>在left到right的全閉合區間裡,取cur為left;prev為cur的前一個位置;取right位置的值為key值;
2>cur從左向右尋找比key小的值,找到以後停下來,然後將prev++,此後,將prev處的值和cur處的值交換;
3>重複2步驟;直到cur走到最後一個位置;此時將prev++,然後將prev位置處的值和cur位置處的值交換;至此一趟快排完成;
4>利用遞迴,將key只左右的大資料區間和小資料區間進行排序;
2.圖說:
3.程式碼實現
#include<iostream>
#include<assert.h>
using namespace std;
//3.前後指標法
void QuickSort3(int* arr,int left,int right)
{
assert(arr);
if (left>=right)
{
return ;
}
int begin=left;
int end=right;
int key=arr[right];//選取基準值key
int cur=left;
int prev=left-1;
while (cur!=right)
{
if (arr[cur]<key&&++prev!=cur)
{
swap(arr[prev],arr[cur]);
}
++cur;
}
//迴圈出來以後,此時,cur到了最後的位置
swap(arr[++prev],arr[right]);
QuickSort3(arr,begin,prev-1);
QuickSort3(arr,prev+1,end);
}
void Printf(int* a,int sz)
{
assert(sz>0);
for (int i=0;i<sz;++i)
{
cout<<a[i]<<" ";
}
cout<<endl;
}
int main()
{
int a[]={6,0,5,1,3,4};
int sz=sizeof(a)/sizeof(a[0]);
QuickSort3(a,0,sz-1);
Printf(a,sz);
return 0;
}
4.測試結果:
5.前後指標法快速排序的深刻理解
prev好人cur兩個指標之間有兩種狀態:
狀態一:prev始終緊跟著cur,這種狀態的保持條件是cur向右走時沒有遇到比key值答的資料;每次遇到一個小的資料就停下來讓prev++;和自己處在同一位置;
狀態二:prev好人cur分離,prev在一個小資料位置原地等待,cur勇敢的朝前取尋找自己的獵物—小資料,當在尋找的過程中碰見了比key大的資料,此時cur不會停止,繼續向右尋找,而prev始終在當前第一個比key值大的資料的緊前面,直到cur找到比key小的值,此時將prev++;來到區間從左到右第一個大於key值得資料的位置,然後prev位置的值和cur位置的小於key的值交換;讓小的值房左邊,大的值放右邊,交換完畢之後,prev所處位置的值小於key;當cur到達區間的尾端時,將prev++使其來到第一個大於key的值得位置和key值交換;這時一趟前後指標的快排完成;小資料全在prev的右面,大資料全在prev的左邊,
四、快排的優化
(1)三數取中法
每次取待排序區間的左端,右端,中間,這三個數中的中間大小數作為key值;
當我們取得基準值key為要排序區間的最大值或者最小值的時候,這時就是快排的最壞情況,時間複雜度最大位O(N^2);這種情況最明顯的例子就是要排序的區間本身就是一個有序區間時,此時該區間的左右兩端的值就位最大值和最小值;
那麼即便不是有序區間,我們在取key值時,也有可能取到最大或者最小值;
那麼如何避免取到的key值為最大值或者最小值呢?方法就是三數區中法,
三數區中法保證了所取得key值不是最大也不是最小,避免了快排最壞情況的出現,當資料有序是,利用三數取中法,可以將原來的這種情況為快排的最壞情況變為快排的最好情況,因為,這時,利用三數取中,每次取到的key值都是該待排序有序區間的中間的數,沒趟都不用交換,且遞迴的次數會少一點;這樣時間複雜度將達到最低;
- 三數取中法的程式實現
int GetMidIndex(int* arr,int left,int right)
{
assert(arr);
int mid=(left+right)/2;
if (arr[left]>arr[mid])
{
if (arr[mid]>arr[right])//arr[left]>arr[mid]>arr[right]
{
return mid;
}
//arr[mid]<=arr[right]
else if (arr[left]>arr[right])//arr[left]>arr[right]=>arr[mid]
{
return right;
}
else//arr[right]>=arr[left]>=arr[mid]
{
return left;
}
}
else//arr[left]<=arr[mid]
{
if (arr[left]>arr[right])//arr[mid]>=arr[left]>arr[right]
{
return left;
}
//arr[left]<=arr[right]
else if(arr[mid]>arr[right])//arr[mid]>arr[right]>arr[left]
{
return right;
}
//arr[mid]<=arr[right]
else//arr[right]>=arr[mid]=>arr[left]
{
return mid;
}
}
}
(2)小區間優化法
我們知道,遞迴程式碼雖然看起來比較簡單,但是遞迴時的函式進行壓棧的開銷是比較大的,效率很低,所以,我們可以對排序進行優化:如果區間比較小時,我們可以採用插入排序。下邊給出程式碼實現:
- 程式碼:
QuickSort(int* arr,int left ,int right)
{
//由於遞迴太深會導致棧溢位,效率低,所以,區間比較小時(小於13)採用插入排序。
if (right-left>13)
{
//三種快排的有效函式體
}
else
InsertSort(a+begin,end-begin + 1);//要排的區間小於13,則使用選擇排序進行排序;
}
(3)使用兩個優化以後的前後指標法快排的程式
#include<iostream>
#include<assert.h>
using namespace std;
//三數取中
int GetMidIndex(int* arr,int left,int right)
{
assert(arr);
int mid=(left+right)/2;
if (arr[left]>arr[mid])
{
if (arr[mid]>arr[right])//arr[left]>arr[mid]>arr[right]
{
return mid;
}
//arr[mid]<=arr[right]
else if (arr[left]>arr[right])//arr[left]>arr[right]=>arr[mid]
{
return right;
}
else//arr[right]>=arr[left]>=arr[mid]
{
return left;
}
}
else//arr[left]<=arr[mid]
{
if (arr[left]>arr[right])//arr[mid]>=arr[left]>arr[right]
{
return left;
}
//arr[left]<=arr[right]
else if(arr[mid]>arr[right])//arr[mid]>arr[right]>arr[left]
{
return right;
}
//arr[mid]<=arr[right]
else//arr[right]>=arr[mid]=>arr[left]
{
return mid;
}
}
}
//左右指標法
void QuickSort1(int* arr,int left,int right)
{
assert(arr);
if (right-left<13)//小區間優化法
{
int key=GetMidIndex(arr,left,right);//三數取中法,優化key的最壞情況的取值
int begin=left;
int end=right;
while (left>=right)//遞迴返回條件
{
return ;
}
while(left<right)
{
while (left<right&&arr[left]<=arr[key])//從左邊開始找到比基準值大的數停下來
{
++left;
}
while (right>left&&arr[right]>=arr[key])//從右邊開始找到比基準值小的數停下來
{
--right;
}
if (left<right&&arr[left]!=arr[right])//如果左右指標都停下來了,並且不是同一個值,自身和自身交換,沒意義,降低效率
{
swap(arr[left],arr[right]);
}
}
//一趟排序完成,此時left=right,交換相遇點位置的資料和key位置的資料
swap(arr[left],arr[key]);
//遞迴地把小於基準值元素的子數列和大於基準值元素的子數列排序
QuickSort1(arr,begin,left-1);
QuickSort1(arr,left+1,end);
}
else
InsertSort(a+begin,end-begin + 1);
}
五、快排的非遞迴實現
#include<iostream>
#include<stack>
#include<assert.h>
using namespace std;
int QuickSort(int* arr,int left,int right)
{
int key=right;
while(left<right)
{
while (left<right&& arr[left]<=arr[key])
{
++left;
}
while(right>left&&arr[right]>=arr[key])
{
--right;
}
if (left<right&&arr[left]!=arr[right])
{
swap(arr[left],arr[right]);
}
}
if (left==right&&arr[left]!=arr[key])
{
swap(arr[left],arr[key]);
}
return left;
}
//**************************核心程式碼*********************************
void NoRecursiveQuickSort(int* arr,int left,int right)
{
assert(arr);
stack<int> s;
s.push(left);//先壓入區間的左邊
s.push(right);//再壓入區間的右邊
while(!s.empty())
{
int end=s.top();
s.pop();
int begin=s.top();
s.pop();
int div=QuickSort(arr,begin,end);
if (begin<div-1)
{
s.push(begin);
s.push(div-1);
}
if (div+1<end)
{
s.push(div+1);
s.push(end);
}
}
}
//**************************核心程式碼*********************************
void Rrintf(int* arr,int left,int right)
{
assert(arr);
while (left<=right)
{
cout<<arr[left++]<<" ";
}
}
int main()
{
int a[]={5,2,7,3,4,1,6,9,8};
int sz=sizeof(a)/sizeof(a[0]);
NoRecursiveQuickSort(a,0,sz-1);
Rrintf(a,0,sz-1);
return 0;
}
六:遞迴快排的時間複雜度
(1)一般情況(含最優情況):
快速排序的時間複雜度取決於它的遞迴的深度;一般情況下為O(N*logN)
(2)最壞情況:
當基準值key取到待排序區間的最小值或者最大值時(其中最典型也是一定出現最壞情況的例子就是待排序區間的資料為有序資料),就會出現最壞情況;
假設有N個數據,這樣總共需要遞迴N-1次;
遞迴總共需要比較的次數為:
(N-1)+(N-2)+(N-3)+…+1=N(N-1)/2
每層比較除去自身和前一層已經排序好的元素
所以最壞情況的時間複雜度為O(N^2);
演算法的時間複雜度一般說的是最壞情況的時間複雜度。
然而,有例外:
有時時間複雜度並不看最壞情況,而看最好情況,比如雜湊表的查詢(雜湊衝突出現的概率很小),快速排序(有相應的優化機制)。
END!!!