資料結構與演算法C++之快速排序(續)
上一篇部落格資料結構與演算法C++之快速排序介紹了快速排序演算法。
但是上面實現的快速排序有兩個缺點:
(一)對於近乎有序的陣列,演算法的計算複雜度由O(nlogn)退化到O(n2)
(二)如果陣列中存在大量重複的元素,那麼演算法的計算複雜度也會退化到O(n2)
(一)對於近乎有序的陣列,演算法的計算複雜度由O(nlogn)退化到O(n2)
使用上篇部落格裡的快速排序演算法做測試,測試程式為
int main()
{
int n = 50000;
//int *arr = generateRandomArray(n, 0, n);
int *arr = generateNearlyOrderedArray (n, 100);//生成只有200個無序元素的陣列
int *arr2 = copyIntArray(arr, n);
testSorting("MergeSorting", MergeSorting, arr, n);
testSorting("quickSorting", quickSorting, arr2, n);
delete[] arr;//最後刪除陣列開闢的空間
delete[] arr2;
return 0;
}
測試結果
可以看出對於一個近乎有序的5萬個元素的陣列,快速排序用了 0.545s,比歸併排序慢了很多倍快速排序演算法中,我們一般將陣列最左邊的元素作為參考元素,這樣的話,當陣列近乎有序的時候,對整個陣列進行partition後,左邊小於參考元素的個數會很少,近乎沒有,右邊大於參考元素的個數將會接近整個陣列元素個數,在遞迴過程中,每層的partition都將近乎只有一部分,如下圖所示
解決方案很簡單,就是隨機選取參考元素,具體實現如下
#include <iostream>
#ifndef _SORTINGHELP_H_
#define _SORTINGHELP_H_
#include "SortingHelp.h"
#endif // _SORTINGHELP_H_
#include "MergeSorting.h"
using namespace std;
//對arr[l...r]進行partition操作
//返回p,使得arr[l...p-1] < arr[p]; arr[p+1...r] > arr[p]
template<typename T>
int __partition(T arr[], int l, int r){
//隨機找一個元素與最左邊元素進行交換位置
swap(arr[l], arr[rand()%(r-l+1)+l]);
T v = arr[l];
int j = l;
//arr[l+1...j] < v; arr[j+1...i) > v
for(int i = l + 1; i <= r; i++){
if (arr[i] < v){
swap(arr[i], arr[j + 1]);
j++;
}
}
swap(arr[l], arr[j]);
return j;
}
//對arr[l...r]部分進行排序
template<typename T>
void __quickSorting(T arr[], int l, int r){
if (l >= r)
return;
srand(time(NULL));
int p = __partition(arr, l, r);
__quickSorting(arr, l, p - 1);
__quickSorting(arr, p + 1, r);
}
template<typename T>
void quickSorting(T arr[], int n){
__quickSorting(arr, 0, n-1);
}
int main()
{
int n = 50000;
//int *arr = generateRandomArray(n, 0, n);
int *arr = generateNearlyOrderedArray(n, 100);//生成只有200個無序元素的陣列
int *arr2 = copyIntArray(arr, n);
testSorting("MergeSorting", MergeSorting, arr, n);
testSorting("quickSorting", quickSorting, arr2, n);
delete[] arr;//最後刪除陣列開闢的空間
delete[] arr2;
return 0;
}
輸出為
隨機設定參考元素後,對於近乎有序的陣列,快速排序演算法的速度恢復正常了
(二)如果陣列中存在大量重複的元素,那麼演算法的計算複雜度也會退化到O(n2)
首先測試一下當陣列中有大量重複元素時,演算法的執行時間
int main()
{
int n = 50000;
//生成5萬個只有0-10的陣列,這樣肯定會有很多重複的元素
int *arr = generateRandomArray(n, 0, 10);
int *arr2 = copyIntArray(arr, n);
testSorting("MergeSorting", MergeSorting, arr, n);
testSorting("quickSorting", quickSorting, arr2, n);
delete[] arr;//最後刪除陣列開闢的空間
delete[] arr2;
return 0;
}
輸出為
可以看出使用上面隨機設定參考元素的方法,計算時間還是很長
由下圖可以看出,當存在大量重複元素時,還是會出現partition後左右不平衡的現象
在上篇部落格的演算法中遍歷陣列元素時是從左到右依次遍歷,遇到大於參考元素的值不操作,遇到小於參考元素的值將其與第一個大於參考元素的值相交換,下面換一種策略
如上圖所示,同時從陣列兩邊進行遍歷,首先還是隨機設定一個參考元素
,然後與最左邊元素進行位置交換
(1)索引為
的元素從左向右遍歷,如果遍歷的元素小於
,那麼就繼續遍歷,如果大於等於
,那麼就停止
(2)此時索引為
的元素從右向左遍歷,如果遍歷的元素大於
,那麼就繼續遍歷,如果小於等於
,那麼就停止
(3)將索引為
的元素與索引為
的元素交換位置,此時左右兩邊又都符合條件了,
和
就繼續開始遍歷
這樣的話,即使有大量重複的元素,也會不斷交換分散到左右兩邊(
和
交換時有三種可能,一、
和
都等於
;二、
等於
,
不等於
;三、
不等於
,
等於
),不會出現左右不平衡的現象
上述改進原理也可檢視部落格
實現程式如下
#include <iostream>
#ifndef _SORTINGHELP_H_
#define _SORTINGHELP_H_
#include "SortingHelp.h"
#endif // _SORTINGHELP_H_
#include "MergeSorting.h"
using namespace std;
//對arr[l...r]進行partition操作
//返回p,使得arr[l...p-1] < arr[p]; arr[p+1...r] > arr[p]
template<typename T>
int __partition(T arr[], int l, int r){
//隨機找一個元素與最左邊元素進行交換位置
swap(arr[l], arr[rand()%(r-l+1)+l]);
T v = arr[l];
int j = l;
//arr[l+1...j] < v; arr[j+1...i) > v
for(int i = l + 1; i <= r; i++){
if (arr[i] < v){
swap(arr[i], arr[j + 1]);
j++;
}
}
swap(arr[l], arr[j]);
return j;
}
//對arr[l...r]部分進行排序
template<typename T>
void __quickSorting(T arr[], int l, int r){
if (l >= r)
return;
srand(time(NULL));
int p = __partition(arr, l, r);
__quickSorting(arr, l, p - 1);
__quickSorting(arr, p + 1, r);
}
template<typename T>
void quickSorting(T arr[], int n){
__quickSorting(arr, 0, n-1);
}
////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////
//下面是改進後的快速排序演算法
//對arr[l...r]進行partition操作
//返回p,使得arr[l...p-1] < arr[p]; arr[p+1...r] > arr[p]
template<typename T>
int __partition2(T arr[], int l, int r){
//隨機找一個元素與最左邊元素進行交換位置
swap(arr[l], arr[rand()%(r-l+1)+l]);
T v = arr[l];
//arr[l+1...i)<=v; arr(j...r]>=v
int i = l + 1;
int j = r;
while(true){
while(arr[i] < v && i <= r) i++;
while(arr[j] > v && j >= l + 1) j--;
if (i > j) break;
swap(arr[i], arr[j]);
i ++;
j --;
}
swap(arr[l], arr[j]);
return j;
}
//對arr[l...r]部分進行排序
template<typename T>
void __quickSorting2(T arr[], int l, int r){
if (l >= r)
return;
srand(time(NULL));
int p = __partition2(arr, l, r);
__quickSorting2(arr, l, p - 1);
__quickSorting2(arr, p + 1, r);
}
template<typename T>
void quickSorting2(T arr[], int n){
__quickSorting2(arr, 0, n-1);
}
int main()
{
int n = 50000;
int *arr = generateRandomArray(n, 0, 10);
//int *arr = generateNearlyOrderedArray(n, 100);//生成只有200個無序元素的陣列
int *arr2 = copyIntArray(arr, n);
int *arr3 = copyIntArray(arr, n);
testSorting("MergeSorting", MergeSorting, arr, n);
testSorting("quickSorting", quickSorting, arr2, n);
testSorting("quickSorting2", quickSorting2, arr3, n);
delete[] arr;//最後刪除陣列開闢的空間
delete[] arr2;
delete[] arr3;
return 0;
}
輸出為
可以看出使用左右兩個方向都遍歷,已經可以對有大量重複元素的陣列進行快速排序