一、快速排序的基本思想
快速排序利用了分治的思想,分而治之。通過一趟排序將序列分爲兩部分,其中一部分比較關鍵字小,一部分比關鍵字大。之後繼續對這兩個子序列重複此過程,直到整個序列都有序。
二、快速排序的三個步驟
- 選基準值:在要排序的序列中選一個幾個基準值,作爲劃分區間的 關鍵字。通常選取關鍵字的方式對效率有很大的影響。
- 分割序列:按照基準值把序列分爲兩部分,左邊的小於基準值,右邊的大於基準值。
- 重複分割:對子序列重複上面的步驟,知道序列爲空或者只有一個元素。當遞歸處理的所有子序列都返回時,序列有序。
三、三種實現方式
注:下面代碼中所有區間都是左閉右開,[ left, right),且排序都是從大到小。且基準值默認選擇序列的最後一個元素。
一、版本一填充法
思路:兩個指針 begng作爲序列的開始 和 end 序列的結束。
將序列的最後一個元素保存起來,作爲基準值。
開始劃分序列:直到begin < end 不成立
begin 從左往右找比 基準值 大的元素,將該值填充到 end 的位置(end 位置上的值已經保存到 key),end-- ,因爲已經劃分好了一個元素。
end 從右往左找比 基準值小的元素,如將該值填充到 begin 的位置(begin 位置上的值已經保存到 end位置),begin++,已經劃分好一個元素。
循環結束時,將 key 填充到 end 的位置,然後將 end 返回,作爲下一次劃分序列的邊界。
// 版本一 填充法
int partition(vector<int> &v, int left, int right)
{
int begin = left;
int end = right - 1;
int key = v[end]; // 選基準
while (begin < end)
{
while (begin < end && v[begin] <= key)
begin++;
if (begin < end)
v[end--] = v[begin];
while (begin < end && v[end] >= key)
end--;
if (begin < end)
v[begin++] = v[end];
}
v[end] = key;
return end;
}
void QuickSort(vector<int>& v, int left, int right)
{
if (left < right)
{
int boundary = partition(v, left, right);
QuickSort(v, left, boundary);
QuickSort(v, boundary + 1, right);
}
}
二、版本二 交換法
和上面的版本一類似,不同之處是,這裏不是填充,而是將找到的元素交換。
// horn版本
int partition(vector<int> &v, int left, int right)
{
int begin = left;
int end = right - 1;
int key = v[end];
while (begin < end)
{
while (begin < end && v[begin] <= key)
begin++;
while (begin < end && v[end] >= key)
end--;
if (begin < end)
std::swap(v[begin], v[end]);
}
if(end != right -1)
std::swap(v[end], v[right-1];)
return end;
}
void QuickSort(vector<int>& v, int left, int right)
{
if (left < right)
{
int boundary = partition(v, left, right);
QuickSort(v, left, boundary);
QuickSort(v, boundary + 1, right);
}
}
我認爲一,二的主要不同在於一個是填充一個是交換,當比較對象較大時,填充的效率是高於交換,不需要在構造一個臨時變量。
三、版本三 雙指針前移法
該方法短小精悍。
思路:
兩個指針從同一個方向走。
cur 指向當前元素,每次循環都要往前走,直到遍歷完序列,所以循環結束的條件就是 cur < right ,right 爲 序列的右邊界。
prev 指向cur的前一個位置。
如果cur 指向的值小於 基準值, 那麼就讓 prev前進一個位置,並判斷 cur 是否等於 prev,如果不等,說明它兩不在同一位置,那麼交換位置上的值。
cur 要麼相鄰,要麼中間間隔的都是別基準值大的元素。cur 的目的就是替 prev 清理前進路上的障礙—“比基準值小的元素”。剩下的都是比基準值大的元素, 只要當 cur 找到比基準值小的,就放心的交換。
循環結束時,需要將基準值,即序列的最後一個元素,與 prev下一個位置上的元素 交換。因爲這個prev 所在的位置是比基準值小的元素,那麼prev 的下一個位置一定比 prev 大,將這個比基準值大的元素與基準值交換就完成了一次劃分。
int partition1(vector<int> &v, int left, int right)
{
int prev = left - 1;
int cur = left;
int key = v[right - 1];
while (cur < right)
{
if (v[cur] < key && ++prev != cur)
std::swap(v[cur], v[prev]);
cur++;
}
if (v[++prev] != key)
std::swap(v[prev], v[right - 1]);
return prev;
}
void QuickSort(vector<int>& v, int left, int right)
{
if (left < right)
{
int boundary = partition1(v, left, right);
QuickSort(v, left, boundary);
QuickSort(v, boundary + 1, right);
}
}
非遞歸版本
每次用到遞歸,大家都會擔心遞歸太深是否會棧溢出?我測試了900000000個int 整型的排序(3.35GB左右內存),棧也木有溢出啊。。
儘管如此還是給出非遞歸的實現版本吧,比較棧資源寶貴。
利用棧將序列起始位置與結束位置保存起來。
void QuickSortNor(vector<int> &v, int left, int right)
{
if (left >= right)
return;
stack<int> s;
s.push(left);
s.push(right);
while (!s.empty())
{
int right = s.top();
s.pop();
int left = s.top();
s.pop();
if (left < right)
{
int boundary = partition1(v, left, right);
// 左區間
s.push(left);
s.push(boundary);
// 右區間
s.push(boundary + 1);
s.push(right);
}
}
}
簡單總結一下:
快速排序,是一種不穩定的排序,時間複雜度平均爲 ,逆序序列下效果最差,逼近 ,其空間複雜度是 ,因爲需要空間來保存遞歸的區間範圍。
2019年03月05日11:43:34 增加測試雙指針法代碼
#include <iostream>
#include <cstdlib>
#include <vector>
#include <ctime>
#include <algorithm>
#include <functional>
using namespace std;
int partition1(vector<int> &v, int left, int right)
{
int prev = left - 1;
int cur = left;
int key = v[right - 1];
while (cur < right)
{
if (v[cur] < key && ++prev != cur)
std::swap(v[cur], v[prev]);
cur++;
}
if (v[++prev] != key)
std::swap(v[prev], v[right - 1]);
return prev;
}
void QuickSort(vector<int>& v, int left, int right)
{
if (left < right)
{
int boundary = partition1(v, left, right);
QuickSort(v, left, boundary);
QuickSort(v, boundary + 1, right);
}
}
int main()
{
// 生成隨機數, 使用自己寫的排序和系統默認排序, 在排序前後比較vector 檢測是否有效
std::vector<int> test;
std::vector<int> ok;
int test_num = 100000;
for(int i = 0; i < test_num; i++)
{
int a = std::rand()/1000000;
test.push_back(a);
ok.push_back(a);
}
if (test == ok )
{
std::cout << "sort before eq" << std::endl;
}
QuickSort(test, 0, test.size());
// 測試數據較大時, 註釋掉了輸出每一個
for (int i = 0; i < test.size(); i++)
{
//std::cout << test[i] << " ";
}
std::cout << endl;
std::sort(ok.begin(), ok.end());
for (int i = 0; i < ok.size(); i++)
{
//std::cout << ok[i] << " ";
}
std::cout << endl;
if (test == ok )
{
std::cout << "sort after eq" << std::endl;
}
return 0;
}
輸出:
sort before eq
sort after eq