排序演算法之快速排序以及優化
顧名思義,就是一種速度特別快的快速排序也叫分割槽排序,是目前應用最廣泛的排序演算法,人如其名,就是很快,而且快速排序演算法在空間上只使用一個小的輔助棧,其內部迴圈也很小,另外快排很容易實現,消耗的資源也很小。
【基本思想】
(1)任取待排序元素序列中的某個元素(例如取第一個元素)作為基準
(2)按照該元素的排序碼大小,將整個元素序列劃分為左右兩個子序列;左側序列中所有元素的排序碼都小於基準值,右側都大於或等於基準值,基準值在中間;
(3)然後分別對著兩個子序列重複執行上述方法,直到所有元素都排在相應的位置上為止
【圖解】
【程式碼實現】
int Pation1(int* array, const int left,const int right) { int ret = left; int key = array[left];//基準元素 for (int i = left + 1; i <= right-1; i++)//檢查整個序列,進行劃分 { if (array[i] < key) { ret++; if (ret != i) { swap(array[ret], array[i]);//小於基準值的交換到後面去 } } } array[left] = array[ret]; array[ret] = key;//將基準值元素就位 return ret;//返回到基準值元素位置 }
void QuickSort(int *array, int left, int right)
{
if (left < right)//元素序列大於1時
{
size_t div = Pation1(array, left, right);//劃分
QuickSort(array, left, div);//對左側進行同樣處理
QuickSort(array, div + 1, right);//對右側進行同樣處理
}
}
【測試】
void Print(int *array,int sz) { for (int i = 0; i < sz; i++) { printf("%d ", array[i]); } printf("\n"); } int main() { int array[] = { 21,25,49,25,16,8 }; Print(array, sizeof(array) / sizeof(array[0])); QuickSort(array, 0, sizeof(array) / sizeof(array[0])); Print(array, sizeof(array) / sizeof(array[0])); system("pause"); return 0; }
【效能分析】
如上圖所知,快排的趟數取決於遞迴樹的深度
最理想的情況:如果每次劃分一個元素定位後,該元素的左側子序列與右側子序列的長度相同,則下一步時對於兩個長度減半的子序列進行排序
假設在n個元素中,對一個元素定位所需時間為O(n),若T(n)是對n個元素的序列進行排序所需的時間(這裡假設為理想狀態下),總的時間為:
最後可得快排的平均複雜度為,最優情況為最壞將到達。
下面介紹最常用的三種快排方法~
一、左右指標法
【基本思想】
利用兩個指標,先設定一個基準值key,設定一個begin指標,從前向後依次找比基準值大的值,設定一個end指標,從後向前依次找比基準值小的值,找到之後將兩個值交換,直到begin跟end的位置重合為止,這時候還要判斷begin的位置是否到達right,如果沒有則與基準值交換
【圖解】
【程式碼實現】
size_t Potion1(int *arr, int left, int right)
{
int begin = left;
int end = right-1;
int key = arr[end];
while (begin < end)
{
while (begin < end&&arr[begin] <= key)//從左向右依次找比基準值大的位置
{
begin++;
}
while (begin<end&&arr[end] >= key)//從右向左依次找比基準值小的位置
{
end--;
}
if (begin < end)//如果找到了一對,就相互交換
{
swap(arr[begin], arr[end]);
}
}
if (begin != right)//這時候判斷begin的位置是否到達right,如果沒有則與基準值交換
{
swap(arr[begin], arr[right-1]);
}
return begin;
}
二、挖坑法
【基本思想】顧名思義,就是挖坑啊
(1)先找一個基準值,將基準值儲存起來,將基準值所在的位置設定為坑;
(2)設定一個指標begin,從前向後找比基準值大的數字並將該數字填入坑中,那麼該數字之前所在的位置就變為新坑;
(3)再設定一個指標end,從後向前找比基準值小的數字,找到之後將該數字填入坑中;
(4)保證begin指標先走,end指標再走,然後迴圈(2)(3)步驟;
(5)直到left<right之後,將儲存的基準值放入begin所在的位置;
【圖解】
【程式碼實現】
size_t Potion2(int* arr, int left, int right)
{
int begin = left;
int end = right-1;
int key = arr[right - 1];//儲存基準值
while (begin < end)
{
while (begin < end&&arr[begin] <= key)
begin++;//從前向後找比基準值大的
if (begin < end)
arr[end--] = arr[begin];//找到之後填坑
while (begin < end && arr[end] >= key)
end--;//從後往前找比基準值小的
if (begin < end)
arr[begin++] = arr[end];//找到之後填上個坑
}
arr[begin] = key;
return begin;
}
三、前後指標法
【基本思想】
【程式碼實現】
int Potion3(int*arr, int left, int right)
{
int key = arr[right - 1];
int cur = left;
int pre = cur - 1;
while (cur < right)
{
if (arr[cur] < key&& ++pre != cur)
{
swap(arr[cur], arr[pre]);
}
cur++;
}
if (++pre != right)
{
swap(arr[pre], arr[right - 1]);
}
return pre;
}
四、非遞迴實現
#include<stack>
void QuickSortNor(int *arr, int size)
{
int left = 0;
int right = size;
stack<int> s;
s.push(right);
s.push(left);
while (!s.empty())
{
int begin = s.top();
s.pop();
int end = s.top();
s.pop();
if (begin < end)
{
int mid = Potion2(arr, begin, end);
//儲存右邊區間
s.push(end);
s.push(mid + 1);
//儲存左邊區間
s.push(mid);
s.push(begin);
}
}
}
五、兩種優化方案
優化一:上述的幾種方法,我們都是以陣列最後一個元素為基準的,這樣勢必為造成誤差,例如陣列若為arr={1,6,5,3,4,6,7,8}或者arr={7,4,3,2,4,2,6,1},這樣的陣列在分割槽時,勢必會造成分割槽沒有作用,陣列還是集中在左邊區域或者右邊區域,那麼為了避免出現這種左右分割槽不均勻的情況出現,我們採用了一種叫做三數取中法的方式優化程式碼。
三數取中法程式碼實現
int GetMidNum(int* arr, int left, int right)//從陣列的最左邊,最右邊以及中間選擇一箇中間的數字作為基準值
{
assert(arr);
int mid = left + ((right-left) >> 1);
if (arr[left] < arr[right - 1])
{
if (arr[left]>arr[mid])
return left;
else if (arr[right - 1] < arr[mid])
return right - 1;
else
return mid;
}
else
{
if (arr[left] < arr[mid])
return left;
else if (arr[right - 1] > arr[mid])
return right - 1;
else
return mid;
}
}
優化二:插入排序+快速排序
void InsertSort(int *array, size_t size)//插入排序
{
for (size_t i = 0; i < size; i++)
{
int key = array[i];
int end = i - 1;
while (end >= 0 && key < array[end])
{
array[end + 1] = array[end];
end--;
}
array[end + 1] = key;//插入元素
}
}
void QuickSort(int* arr, int left, int right)
{
if (right-left < 5)//當分割槽小於一定數字之後
{
InsertSort(arr, right-left);
}
else
{
size_t mid = Potion2(arr, left, right);
QuickSort(arr, left, mid);
QuickSort(arr, mid + 1, right);
}
}
五、完整的程式碼
#define _CRT_SECURE_NO_WARNINGS 1
using namespace std;
#include<stdio.h>
#include<string.h>
#include<iostream>
#include<assert.h>
void InsertSort(int *array, size_t size)
{
for (size_t i = 0; i < size; i++)
{
int key = array[i];
int end = i - 1;
while (end >= 0 && key < array[end])
{
array[end + 1] = array[end];
end--;
}
array[end + 1] = key;//插入元素
}
}
//三數取中法
int GetMidNum(int* arr, int left, int right)
{
assert(arr);
int mid = left + ((right-left) >> 1);
if (arr[left] < arr[right - 1])
{
if (arr[left]>arr[mid])
return left;
else if (arr[right - 1] < arr[mid])
return right - 1;
else
return mid;
}
else
{
if (arr[left] < arr[mid])
return left;
else if (arr[right - 1] > arr[mid])
return right - 1;
else
return mid;
}
}
//左右指標法
size_t Potion1(int *arr, int left, int right)
{
int begin = left;
int end = right-1;
//int key = arr[end];
int index = GetMidNum(arr, left, right);
if (index != right-1)
{
swap(arr[index], arr[right - 1]);
}
int key = arr[right - 1];
while (begin < end)
{
while (begin < end&&arr[begin] <= key)
{
begin++;
}
while (begin<end&&arr[end] >= key)
{
end--;
}
if (begin < end)
{
swap(arr[begin], arr[end]);
}
}
if (begin != right)
{
swap(arr[begin], arr[right-1]);
}
return begin;
}
//挖坑法
size_t Potion2(int* arr, int left, int right)
{
int begin = left;
int end = right-1;
int index = GetMidNum(arr, left, right);
if (index != right - 1)
{
swap(arr[index], arr[right - 1]);
}
int key = arr[right - 1];
while (begin < end)
{
while (begin < end&&arr[begin] <= key)
{
begin++;
}
if (begin < end)
arr[end--] = arr[begin];
while (begin < end && arr[end] >= key)
end--;
if (begin < end)
arr[begin++] = arr[end];
}
arr[begin] = key;
return begin;
}
//前後指標法
int Potion3(int*arr, int left, int right)
{
//int key = arr[right - 1];
int cur = left;
int pre = cur - 1;
int index = GetMidNum(arr, left, right);
if (index != right - 1)
{
swap(arr[index], arr[right - 1]);
}
int key = arr[right - 1];
while (cur < right)
{
if (arr[cur] < key&& ++pre != cur)
{
swap(arr[cur], arr[pre]);
}
cur++;
}
if (++pre != right)
{
swap(arr[pre], arr[right - 1]);
}
return pre;
}
void QuickSort(int* arr, int left, int right)
{
if (right-left < 4)
{
InsertSort(arr, right-left);
}
else
{
size_t mid = Potion2(arr, left, right);
QuickSort(arr, left, mid);
QuickSort(arr, mid + 1, right);
}
}
void Print(int* arr, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[] = { 2, 0, 4, 6, 5, 7, -1, -3, 5, 8, 9,10};
Print(arr, sizeof(arr) / sizeof(arr[0]));
QuickSort(arr, 0, sizeof(arr) / sizeof(arr[0]));
Print(arr,sizeof(arr) / sizeof(arr[0]));
getchar();
return 0;
}