快速排序(QuickSort):ios
快速排序是實踐中最快的排序算法,它的平均運行時間是O(NlogN) ,它的最壞情形的性能是 O(N^2),但稍加努力就能夠避免這種情形。像歸併排序同樣,快速排序也是一種分治的遞歸算法。算法
將數組S 排序的基本算法由下面的四個簡單步驟組成:數組
1) 若是S 的個數爲0 或 1,則直接返回安全
2) 取S 中任意元素 v ,稱之爲樞紐元(pivot)dom
3) 將S 中剩餘的元素分紅兩個不相交的集合: S1 和 S2性能
4) 返回{quicksort(S1)} 繼隨 v ,繼而{quicksort(S2)}優化
Why 快速排序比歸併排序快?ui
如同歸併排序那樣,快速排序遞歸的解決兩個子問題並須要線性的附加工做(第(3)步),不過,與歸併算法不一樣的是,兩個子問題並不保證具備相等的大小,這是個潛在的隱患。快速排序更快的緣由在於,第(3)步分割成兩組其實是在適當位置上進行而且很是高效,它的高效彌補了大小不等的遞歸調用的缺憾並且還有超出。spa
樞紐元的選取:code
兩種經常使用的錯誤方法:
將第一個元素做爲樞紐元,若是輸入是隨機的,那麼能夠接受,可是若是輸入是預排序的或者是反序的,那麼這樣的樞紐元就產生了一個劣質的分割,由於全部的元素不是都被劃入 S1 中就是都被劃入S2 中。更有甚者,這種狀況可能發生在全部的遞歸調用中。實際上,若是第一個元素用作樞紐元並且輸入是預先排序的,那麼快速排序花費的時間將是二次的O(N^2)。
選取前兩個互異的關鍵字中的較大者做爲樞紐元,這和選取第一個元素做爲樞紐元具備相同的害處。
一種安全的作法: 隨機選取樞紐元。通常來講這種策略很是安全,由於隨機的樞紐元不可能總在接連不斷的產生劣質的分割,另外一方面,隨機數的生成通常是昂貴的,根本減小不了算法其他部分的平均運行時間。
三數中值分割法:一組N 個數的中值是第 2/N (上取整)個最大的數,樞紐元最好的選擇是數組的中值。不幸的是,這很難算出,且明顯減慢快速排序的速度。這樣的中值估計量能夠經過隨機選取三個數並用他們的中值做爲樞紐元而獲得。事實上隨機性並無多大的幫助,所以通常的作法是使用左端、右端和中心位置上的三個元素的中值做爲樞紐元。
2. 分割策略:
在分割階段要作的就是把全部小元素移到數組的左邊而把全部大元素移到數組的右邊。(大小是相對樞紐元素而言的)
首先,將數組的最後一個元素與樞紐元互換,i 指向數組的第一個元素,j 指向數組的倒數第二個元素,當i 在 j 的左邊時,將 i 右移,移過那些小於樞紐元的元素,並將j 左移,移過那些大於樞紐元的元素。當 i 和 j 中止時,i 指向一個大元素而 j 指向一個小元素。若是i 在 j 的左邊,那麼將這兩個元素互換,其效果是把一個大元素移向右邊而把一個小元素移向左邊。當 i 大於 j 時,再也不交換。分割的最後一步是將樞紐元與 i 所指向的元素交換。
考慮一個重要的細節是如何處理那些等於樞紐元的關鍵字:問題在於當 i 遇到一個等於樞紐元的關鍵字時,是否應該中止。直觀地看,i 和 j 應該有一樣的操做,不然分割將出現偏向一邊的傾向。例如,若是 i 停, j 不停,則全部等於樞紐元的元素將被分到 S2中。考慮數組中全部元素都相等的狀況。若是i 和 j 都中止,那麼相等的元素間將有不少次交換,雖然這彷佛沒有什麼意義,可是其正面的效果則是 i 和 j 將在中間交錯,所以當樞紐元被代替時,這種分割創建了兩個幾乎相等的子數組,此時總的運行時間爲
O(NlogN)。若是 i 和 j 都不中止,那麼就應該有相應的程序防止 i 和 j 越出數組的界限,不進行交換的操做。雖然這樣彷佛不錯,可是這樣的作法將產生兩個很是不均衡的子數組。若是全部的關鍵字都相同,那麼運行時間爲 O(N^2)。所以,若是i 和j 遇到等於樞紐元的關鍵字時,那麼就讓 i 和 j 都中止。
代碼實現:
1. 隨機選取樞紐元的方法:
//樞紐元的選取:產生某一範圍內的隨機數(包括邊界)
int RandomInRange(int start, int end)
{
srand(time(NULL));
return start + rand() % (end - start + 1);
}
//交換兩個數
void Swap(int *elem1, int *elem2)
{
int temp;
temp = *elem1;
*elem1 = *elem2;
*elem2 = temp;
}
// 選取樞紐元,將數組劃分爲兩部分
int Partition(int data[], int length, int start, int end)
{
if(data == NULL || length <= 0 || start < 0 || end >= length) //異常輸入
throw exception("Invalid input!");
int index = RandomInRange(start, end); //隨機產生一個數index,將數組中第index個元素做爲基準點
Swap(&data[index], &data[end]); //將樞紐元與最後一個元素交換,以便在遍歷整個數組時,對數組進行劃分
int small = start - 1; //small 指向已劃分的小於樞紐元的那一部分的最後一個元素
for(index = start; index < end; index ++)
{
if(data[index] < data[end]) //若某一元素小於樞紐元,則small就要+1
{
++small; // small != index 說明small與index中間是大於樞紐元的元素,而index位置處的元素卻小於基準點
if(small != index) //若一劃分好的小於樞紐元的那一部分的最後一個元素的下一個不等於當前所訪問的元素[index],
//則須要將二者進行互換
Swap(&data[small], &data[index]);
}
}
++small;
Swap(&data[end], &data[small]);
return small; //small 爲最後的樞紐元
}
//快速排序
void QuickSort(int data[], int length, int start, int end)
{
if(start == end)
return;
int index = Partition(data, length, start, end); //選取樞紐元元素,並將數組進行劃分
if(index > start) //有小於樞紐元的元素存在
{
QuickSort(data, length, start, index - 1);
}
if(index < end) //有大於樞紐元的元素存在
{
QuickSort(data, length, index + 1, end);
}
}
2. 三數中值分割法(選取樞紐元)
void InsertSort_1(int A[], int N)
{
int i, j, tmp;
for(i = 1; i < N; i++)
{
tmp = A[i];
for(j = i; j > 0 && A[j - 1] > tmp; j--)
{
A[j] = A[j - 1];
}
A[j] = tmp;
}
}
void swap(int *a, int *b)
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
//三數中值分割法
ElemType Median3(ElemType A[], int Left, int Right)
{
int Center = (Left + Right) / 2;
if(A[Left] > A[Center])
swap(&A[Left], &A[Center]);
if(A[Left] > A[Right])
swap(&A[Left], &A[Right]);
if(A[Center] > A[Right])
swap(&A[Center], &A[Right]); //Left處的爲最小值,Right爲最大值,Center爲中值
swap(&A[Center], &A[Right - 1]); //將center處的值做爲樞紐元,並與倒數第二個元素互換【此時數組最後一個元素必定大
//於樞紐元,所以沒必要參與比較】
return A[Right - 1];
}
// 遞歸快排
void Qsort(ElemType A[], int Left, int Right)
{
int i, j;
ElemType Pivot;
if(Left + Cutoff <= Right)
{
Pivot = Median3(A, Left, Right);
i = Left;
j = Right - 1;
for(;;)
{
while(A[++i] < Pivot){} //i 右移
while(A[--j] > Pivot){} //i 左移
if(i < j)
swap(&A[i], &A[j]); // A[i] >= Pivot and A[j] <= Pivot and i 在j 的左邊時交換位置
else
break;
}
swap(&A[i], &A[Right - 1]); //將樞紐元Pivot插入到數組的適當位置
Qsort(A, Left, i - 1); //左邊數組遞歸調用快速排序
Qsort(A, i + 1, Right); //右邊數組遞歸調用快速排序
}
else
InsertSort_1(A + Left, Right - Left + 1); //元素個數小於3時,使用直接插入排序
}
void QuickSort(int A[], int N)
{
Qsort(A, 0, N-1);
}
排序算法的總結:高度優化的快速排序算法便是是對不多的輸入數據也能和希爾排序同樣快。快速排序的改進算法仍然有 O(N^2)的最壞情形,可是這種狀況出現的概率是微不足道的,以致於不能成爲影響算法的因素。若是須要對一些大型的文件進行排序,則快速排序算法應該是可取的辦法。可是永遠不能由於省事而將第一個元素選做樞紐元,對輸入數據隨機的假設是不安全的。若是不想過多的考慮這個問題,則能夠選擇希爾排序。希爾排序有些小缺陷,可是仍是能夠接受的。希爾排序的最壞狀況也只不過是O(N^(4/3));這種最壞狀況的概率也是微不足道的。
堆排序要比希爾排序慢,儘管他是一個帶有明顯緊湊的內循環O(NlogN), 可是,對該算法的考察揭示,爲了移動數據, 堆排序要進行兩次比較。
插入排序只用在小的或是很是接近排好序的輸入數據上。而 歸併排序的性能對於主存排序並無快速排序那麼好,【合併時外部排序的中心思想】
整體代碼:
#include<iostream> using namespace std; #define Cutoff 3 //最少的元素個數不得小於3 typedef int ElemType; void InsertSort_1(int A[], int N) { int i, j, tmp; for(i = 1; i < N; i++) { tmp = A[i]; for(j = i; j > 0 && A[j - 1] > tmp; j--) { A[j] = A[j - 1]; } A[j] = tmp; } } void swap(int *a, int *b) { int tmp; tmp = *a; *a = *b; *b = tmp; } //三數中值分割法 ElemType Median3(ElemType A[], int Left, int Right) { int Center = (Left + Right) / 2; if(A[Left] > A[Center]) swap(&A[Left], &A[Center]); if(A[Left] > A[Right]) swap(&A[Left], &A[Right]); if(A[Center] > A[Right]) swap(&A[Center], &A[Right]); //Left處的爲最小值,Right爲最大值,Center爲中值 swap(&A[Center], &A[Right - 1]); //將center處的值做爲樞紐元,並與倒數第二個元素互換【此時數組最後一個元素必定大於樞紐元,所以沒必要參與比較】 return A[Right - 1]; } void Qsort(ElemType A[], int Left, int Right) { int i, j; ElemType Pivot; if(Left + Cutoff <= Right) { Pivot = Median3(A, Left, Right); i = Left; j = Right - 1; for(;;) { while(A[++i] < Pivot){} //i 右移 while(A[--j] > Pivot){} //i 左移 if(i < j) swap(&A[i], &A[j]); // A[i] >= Pivot and A[j] <= Pivot and i 在j 的左邊時交換位置 else break; } swap(&A[i], &A[Right - 1]); //將樞紐元Pivot插入到數組的適當位置 Qsort(A, Left, i - 1); //左邊數組遞歸調用快速排序 Qsort(A, i + 1, Right); //右邊數組遞歸調用快速排序 } else InsertSort_1(A + Left, Right - Left + 1); //元素個數小於3時,使用直接插入排序 } void QuickSort(int A[], int N) { Qsort(A, 0, N-1); } int main() { int arr[10] = {5, 2, 8, 6, 3, 1, 7, 9, 4, 10}; QuickSort(arr, 10); for(int i = 0; i < 10; i++) cout << arr[i] << " "; cout << endl; system("pause"); return 0; }