文章目錄
閒話休說,自從前幾天寫了排序問題中的兩個排序之後。然後我又開始了快排代碼的研究當中。當時學習的時候學的十分粗糙,今天重新來總結總結,把一些關於快排的問題進行總體上的描述。主要的內容有以下的這些:
一、快排的三種選擇基準的方式
1.邊界基準法
這種方式最簡單,就是直接使用原數組中的第一個元素或者最後一個元素當作快排的基準。詳細見以下的代碼:
int select_pivot(int *array, int left, int length)
{
return array[left];
}
這樣的基準選擇方法在待排列數據是隨機的時候效率是不錯的,但是當待排序的數列是有序的,也就是說,我們每次的選擇都是最倒黴的,要麼是全數組中最小的,要麼是全數組中最大的。此時快排退化成爲冒泡排序。
2.隨機基準法
基於以上的我們可能成爲最倒黴的“倒黴蛋”,我們採取隨機的方式來選取基準,此時我們就與輸入數組的序列無關了。隨機基準法的代碼如下:
int select_pivot_random(int *array, int left, int length)
{
srand((unsigned)time(0));
int index = rand() % length + left;
swap(array[left], array[index]);
return array[left];
}
雖然說,隨機基準法解決了絕大部分的基準選取問題,但是我們仍然有望成爲那個“倒黴蛋”。雖然每次取到最小值的概率比較小,但是理論上還是存在這種可能性,而且一旦存在這種情況就直接退化爲冒泡排序,時間複雜度太高。概率比較小,但是理論上還是存在這種可能性,而且一旦存在這種情況就直接退化爲冒泡排序,時間複雜度太高。
3. 三數取中基準法
爲了從理論上消除以上隨機基準存在的最後一點不好的情況,我們採用的是三數取中基準法,完全解決這個問題,這樣就算最壞的情況也是分成1和n - 2號,一般的想法就是隨機取出三個數,然後取中間的那個數作爲基準,但是在這種情況之下,隨機已經不是那麼重要了,我們通常採用的方法是將左端、右端和中間的三個數取他們的中間值,代碼如下:
int select_pivot_medium(int *array, int left, int right)
{
int half = left + (right - left + 1) / 2;
if (array[left] > array[right])
swap(array[left], array[right]);
if (array[half] > array[right])
swap(array[half], array[right]);
if (array[left] < array[half])
swap(array[left], array[half]);
return array[left];
}
以上的代碼實際上就是先將三數中的最大值放到最右邊,然後把剩下的較大的那一個數放到左端上就好。
爲了從理論上消除以上隨機基準存在的最後一點不好的情況,我們採用的是三數取中基準法,完全解決這個問題,這樣就算最壞的情況也是分成1和n - 2號,一般的想法就是隨機取出三個數,然後取中間的那個數作爲基準,但是在這種情況之下,隨機已經不是那麼重要了,我們通常採用的方法是將左端、右端和中間的三個數取他們的中間值,代碼如下:
int select_pivot_medium(int *array, int left, int right)
{
int half = left + (right - left + 1) / 2;
if (array[left] > array[right])
swap(array[left], array[right]);
if (array[half] > array[right])
swap(array[half], array[right]);
if (array[left] < array[half])
swap(array[left], array[half]);
return array[left];
}
以上的代碼實際上就是先將三數中的最大值放到最右邊,然後把剩下的較大的那一個數放到左端上就好。
二、幾種快排的優化
1. 當排序規模較小的時候採用插入排序
其實對於很小的數組或者部分有序的數組來說,插入排序的效率其實要比快排的效率高一些。因此我們可以考慮當排序規模小到一定程度的時候,我們可以使用插入排序代替快排,由此可以提高效率。
一般我們選擇序列長度在5~20之間的數據,在這裏我們選擇的是10。代碼如下:
if (right - left + 1 < 10)
{
insert_sort(array, left, right);
return;
}
2. 在一次分割結束之後,可以把key相等的元素在一起沒下次分割,就不分割key相同的元素
這個優化主要是針對那些在數組中出現了大量的重複元素的情況的優化。思路就是將與基準相等的數字首先移動到數組的兩端。之後再將兩端的相等的數據全部集中到中間。
這樣就大大降低了對重複元素的處理。具體的代碼如下:
/**
* 這個方法主要是用於解決在數組中存在大量的重複數據的問題
* 主要思路就是在前後兩個指針相對移動的時候順便處理和pivot相等的數據
* 1. 將他們移動到數組的兩端
* 2. 將他們從數組兩端移動到中間
* 3. 對剩下的數據進行快排
* **/
int partition_optimize(int *array, int left, int right, int& left_len, int& right_len)
{
int pivot = select_pivot_medium(array, left, right);
int first = left, last = right;
left_len = 0;
right_len = 0;
int left_item = left, right_item = right;
while (left_item < right_item)
{
while (left_item < right_item && array[right_item] >= pivot)
{
if (array[right_item] == pivot)
{
swap(array[right_item], array[right]);
-- right;
++ right_len;
}
-- right_item;
}
array[left_item] = array[right_item];
while (left_item < right_item && array[left_item] <= pivot)
{
if (array[left_item] == pivot)
{
swap(array[left], array[left_item]);
++ left;
++ left_len;
}
++ left_item;
}
array[right_item] = array[left_item];
}
array[left_item] = pivot;
int low = first, high = left_item - 1;
while (low < left && array[high] != pivot)
{
swap(array[low], array[high]);
++ low;
-- high;
}
low = left_item + 1;
high = last;
while (high > right && array[low] != pivot)
{
swap(array[low], array[high]);
++ low;
-- high;
}
return left_item;
}
三、非遞歸方式實現快排
我們知道對於遞歸的內部實現,就是自己調用自己,多半是使用棧這個數據結構來實現的,因此我們可以使用棧來替代遞歸程序。進行非遞歸快排的代碼。主要思路就是模擬遞歸的實現。具體的代碼實現如下:
/**
* 其實真正的排序是由partition完成的
* 棧的數據結構只是提供了一個空間
* */
void no_recurrence_quick_sort(int *array, int left, int right)
{
std::stack<int> stack;
stack.push(right);
stack.push(left);
while (! stack.empty())
{
left = stack.top();
stack.pop();
right = stack.top();
stack.pop();
if (left < right)
{
int middle = partition(array, left, right);
if (left < middle - 1)
{
stack.push(middle - 1);
stack.push(left);
}
if (right > middle + 1)
{
stack.push(right);
stack.push(middle + 1);
}
}
}
}