快速排序的三種方式以及快排的優化
一.快速排序的基本思想
關於快速排序,它的基本思想就是選取一個基準,一趟排序確定兩個區間,一個區間全部比基準值小,另一個區間全部比基準值大,接著再選取一個基準值來進行排序,以此類推,最後得到一個有序的數列。
二.快速排序的步驟
- 1.選取基準值,通過不同的方式挑選出基準值。
- 2.用分治的思想進行分割,通過該基準值在序列中的位置,將序列分成兩個區間,在準值左邊的區間裡的數都比基準值小(預設以升序排序),在基準值右邊的區間裡的數都比基準值大。
- 3.遞迴呼叫快速排序的函式對兩個區間再進行上兩步操作,直到呼叫的區間為空或是隻有一個數。
三.關於選取基準值的方式
1.固定位置選取基準值
基本思想:選取第一個或最後一個元素作為基準值。
如上是以第一個數作為選取基準的方式的第一趟排序的結果,接著就是對分好的兩個區間再進行遞迴的快排。
但是,這種選取基準值的方法在整個數列已經趨於有序的情況下,效率很低。比如有序序列(0,1,2,3,4,5,6,7,8,9),當我們選取0為基準值的時候,需要將後面的元素每個都交換一遍,效率很低。所以這種以固定位置選取基準值的方式,只適用於該序列本身並不是趨於有序的情況下,比如一串隨機數列,此時的效率還能夠差強人意。
為了避免這種已經有序的情況,於是有了下面兩種選取基準值的方式
下面是關於選取固定基準值快排的程式碼
int SelectPivot(int* a, int left, int right)//選取基準值函式
{
return a[left];
}
void QuickSort(int* a, int left, int right)
{
assert(a);
int i, j;
int pivot = SelectPivot(a, left, right);//確定基準值
if (left < right)
{
i = left + 1;//以第一個數left作為基準數,從left+1開始作比較
j = right;
while (i < j)
{
if (a[i] > pivot)//如果比較的數比基準數大
{
swap(a [i], a[j]);//把該比較數放到陣列尾部,並讓j--,比較過的數就不再比較了
j--;
}
else
{
i++;//如果比較的數比基準數小,則讓i++,讓下一個比較數進行比較
}
}
//跳出while迴圈後,i==j
//此時陣列被分成兩個部分,a[left+1]-a[i-1]都是小於a[left],a[i+1]-a[right]都是大於a[left]
//將a[i]與a[left]比較,確定a[i]的位置在哪
//再對兩個分割好的部分進行排序,以此類推,直到i==j不滿足條件
if (a[i] >= a[left]) //這裡必須要用>=,否則相同時會出現錯誤
{
i--;
}
swap(a[i], a[left]);
QuickSort(a, left, i);
QuickSort(a, j, right);
}
}
//測試函式
int main()
{
int a[] = { 2,5,4,9,3,6,8,7,1,0};
const size_t n = sizeof(a) / sizeof(a[0]);
QuickSort(a, 0, n - 1);
Print(a, n);
system("pause");
return 0;
}
2.隨機選取基準
基本思想:選取待排序列中任意一個數作為基準值。
因為快排函式部分的程式碼是一樣的,只是選取基準值部分的函式不相同,下面只附上選取基準值函式的程式碼
int SelectPivot(int* a, int left, int right)//選取基準值函式
{
srand((unsigned)time(NULL));
int pivotPos;
if (left < right)//這裡需要保證傳進來的left必須小於left
{
pivotPos = rand() % (right - left) + left;
}
else
{
pivotPos = left;//在遞迴呼叫裡走到這一步,肯定是left=right,直接讓pivotPos=left
}
swap(a[pivotPos], a[left]);
return a[left];
}
引入隨機化快速排序的作用,就是當該序列趨於有序時,能夠讓效率提高,大量的測試結果證明,該方法確實能夠提高效率。但在整個序列數全部相等的時候,隨機快排的效率依然很低,它的時間複雜度為O(N^2),但出現這種最壞情況的概率非常的低,所以它還是一種效率比較好的方法,一般情況下都能夠達到O(N*lgN)。
3.三數取中法選取基準值
基本思想:取第一個數,最後一個數,第(N/2)個數即中間數,三個數中數值中間的那個數作為基準值。舉個例子,對於int a[] = { 2,5,4,9,3,6,8,7,1,0};
,‘2’、‘3’、‘0’,分別是第一個數,第(N/2)個是數以及最後一個數,三個數中3最大,0最小,2在中間,所以取2為基準值。
下面也是附上三數取中法的程式碼
int SelectPivot(int* a, int left, int right)//選取基準值函式
{
int mid;
if (left < right)
{
mid = (right - left) / 2;
}
else
{
return a[left];//在遞迴呼叫裡走到這一步,肯定是left=right,直接讓pivotPos=left
}
if (a[mid] > a[right])
{
swap(a[mid], a[right]);
}
if (a[left] > a[right])
{
swap(a[left], a[right]);
}
if (a[mid] > a[left])
{
swap(a[mid], a[left]);
}
//上面三步完成之後,a[left]就是三個數中最小的那個數
return a[left];
}
採用三數取中法很好的解決了很多特殊的問題,但對於很多重複的序列,效果依然不好。於是在這三種選取基準值的方法下,另外地還有三種優化方法。
四.快速排序的優化
優化一:當待排序序列的長度分割到一定大小後,使用插入排序。
優化原因:對於待排序的序列長度很小或是基本趨於有序時,快排的效率還是插排好。
自定義截止範圍:序列長度N=10。當待排序的序列長度被分割到10時,採用快排而不是插排。
if (n <= 10)//當整個序列的大小n<=10時,採用插排
{
InsertSort(a, n);
}
優化二:在一次排序後,可以將與基準值相等的數放在一起,在下次分割時可以不考慮這些數。
因為這一次改動的程式碼比較多,所以再繼續把所有的程式碼全部拿出來看一下
int SelectPivot(int* a, int left, int right)//選取基準值函式
{
int mid;
if (left < right)
{
mid = (right - left) / 2;
}
else
{
return a[left];//在遞迴呼叫裡走到這一步,肯定是left=right,直接讓pivotPos=left
}
if (a[mid] > a[right])
{
swap(a[mid], a[right]);
}
if (a[left] > a[right])
{
swap(a[left], a[right]);
}
if (a[mid] > a[left])
{
swap(a[mid], a[left]);
}
//上面三步完成之後,a[left]就是三個數中最小的那個數
return a[left];
}
void QuickSort(int* a, int left, int right)
{
assert(a);
if (n <= 10)//當整個序列的大小n<=10時,採用插排
{
InsertSort(a, n);
return;
}
int i, j;
int pivot = SelectPivot(a, left, right);//確定基準值
if (left < right)
{
i = left + 1;//以第一個數left作為基準數,從left+1開始作比較
j = right;
while (i < j)
{
if (a[i] == pivot)//處理與基準值相等的數,都放到陣列末尾
{
swap(a[i], a[j]);
--j;
}
else if (a[i] > pivot)//如果比較的數比基準數大
{
while (1)
{
if (a[j] == pivot)//如果要換的數值等於基準值,讓j--,與前一個交換
{
--j;
}
else
{
break;
}
}
swap(a[i], a[j]);//把該比較數放到陣列尾部,並讓j--,比較過的數就不再比較了
--j;
}
else
{
++i;//如果比較的數比基準數小,則讓i++,讓下一個比較數進行比較
}
}
//跳出while迴圈後,i==j
//此時陣列被分成兩個部分,a[left+1]-a[i-1]都是小於a[left],a[i+1]-a[right]都是大於a[left]
//將a[i]與a[left]比較,確定a[i]的位置在哪
//再對兩個分割好的部分進行排序,以此類推,直到i==j不滿足條件
if (a[i] >= a[left]) //這裡必須要用>=,否則相同時會出現錯誤
{
i--;
}
swap(a[i], a[left]);
int tmp = right;//用tmp表示從後往前第一個不是基準值的數
while (tmp > i)
{
if (a[tmp] == pivot)
{
--tmp;
}
else//else表示沒有與基準值重複的值
{
QuickSort(a, left, i - 1);
QuickSort(a, i + 1, right);
return;
}
}
int pos = tmp;//因為後面要用到tmp,所以運算的話用一個pos來代替tmp進行運算
int r = right;//因為後面要保證right還是先前的右邊界,所以運算的話用另外一個變數來表示
int count = 0;
while (pos > i&&r > tmp)
{
swap(a[pos], a[r]);
--r;
--pos;
++count;//換了一次讓count++
}
QuickSort(a, left, i-1);//對左區間快排,i-1是左區間的最後一個數
QuickSort(a, right-count+1, right);//對右區間快排,right-count+1是右區間的第一個數
}
}
這種聚集與基準值相等的值的優化方法,在解決資料冗餘的情況下非常有用,提高的效率也是非常多。
優化三:優化遞迴操作
快排函式在函式尾部有兩次遞迴操作,我們可以對其使用尾遞迴優化
優點:如果待排序的序列劃分極端不平衡,遞迴的深度將趨近於n,而棧的大小是很有限的,每次遞迴呼叫都會耗費一定的棧空間,函式的引數越多,每次遞迴耗費的空間也越多。優化後,可以縮減堆疊深度,由原來的O(n)縮減為O(logn),將會提高效能。
只是在尾部的遞迴呼叫的時候做了以下改變
while (left < right)
{
QuickSort(a, left, i - 1);//對左區間快排,i-1是左區間的最後一個數
left = right - count + 1;
}
其實這種優化編譯器會自己優化,相比不使用優化的方法,時間幾乎沒有減少。
所以到這裡,總結一下,對於快速排序(一組隨機陣列),效率最快的優化方案應該是三數取中法+插排+聚集相等元素,對於尾遞迴可以有也可以沒有,對於效率的變化改變不大。