在归并排序算法的优化这一节提到过,高级排序算法几乎有一个通用的优化方法,就是对递归底层的操作进行优化。先来看快速排序算法:
//对arr[l...r]进行patition操作
//返回p,使得arr[l...p-1]arr[p+1...r]
template
int __patition(T arr[], int l, int r)
{
int j = l;
for(int i = l + 1; i <= r; i++)
{
if(arr[i] < arr[l])
{
swap(arr[j + 1], arr[i]);
j++;
}
}
swap(arr[l], arr[j]);
return j;
}
template
void __quickSort(T arr[], int l, int r) //对arr[l...r]进行快速排序
{
if(l >= r)
{
return;
}
else
{
int p = __patition(arr, l, r); //__patition()函数将arr[l...r]分为两部分,该函数返回一个索引值
__quickSort(arr, l, p - 1);
__quickSort(arr, p + 1, r);
}
}
template
void quickSort(T arr[], int n)
{
__quickSort(arr, 0, n - 1);
}
在__quickSort()中,对底层递归语句进行优化:
//对arr[l...r]进行插入排序
template
void insertionSort(T arr[], int l, int r)
{
for(int i = l + 1; i <= r; i++)
{
T e = arr[i];
int j;
for(j = i; j > l && arr[j - 1] > e; j--)
{
arr[j] = arr[j - 1];
}
arr[j] = e;
}
}
template
void __quickSort(T arr[], int l, int r) //对arr[l...r]进行快速排序
{
/*if(l >= r)
{
return;
}*/
//优化底层递归
if(r - l <= 15) //即只有16个元素时
{
//采用插入排序
insertionSort(arr, l, r);
return;
}
else
{
int p = __patition(arr, l, r); //__patition()函数将arr[l...r]分为两部分,该函数返回一个索引值
__quickSort(arr, l, p - 1);
__quickSort(arr, p + 1, r);
}
}
这只是一种小的优化方法,更重要的是另一种优化方法,下面先来看快排的局限性:
测试两个基本有序的数组用归并排序和快排排序的时间性能:
SortTestHelper.h文件(包含辅助函数)
#include
#include
#include //clock()、CLOCKS_PER_SEC
#include //包含函数assert()
using namespace std;
namespace SortTestHelper
{
//辅助函数 - 随机产生一个数组
int* generateRandomArray(int n, int RangeL, int RangeR) //返回数组首地址
{
//判断RangeL是否<=RangeR
assert(RangeL <= RangeR); //参数为表达式,表达式为真时返回true,否则打印错误信息
int *arr = new int[n];
srand(time(0));
for(int i = 0; i < n ; i++)
{
arr[i] = rand() % (RangeR - RangeL + 1) + RangeL; //使得产生的随机数在RangeL和RangeR之间
}
return arr;
}
//辅助函数 - 产生一个近乎有序的随机数组
int* generateNearlyOrderedArray(int n, int swapTime)
{
int *arr = new int[n];
for(int i = 0; i < n; i++)
{
arr[i] = i; //先生成一个完全有序的数组
}
//然后交换几组元素,使之变成无序但近乎有序的数组
srand(time(0));
for(int j = 0; j < swapTime; j++)
{
//随机生成一个x位置和y位置
int posx = rand() % n;
int posy = rand() % n;
//交换x和y处的元素
swap(arr[posx], arr[posy]);
}
return arr;
}
//辅助数组 - 产生一个完全有序数组
int* generateTotallyOrderedArray(int n)
{
int *arr = new int[n];
for(int i = 0; i < n; i++)
{
arr[i] = i;
}
return arr;
}
//辅助函数 - 打印数组
template
void printArray(T arr[], int n)
{
for(int i = 0; i < n; i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
//辅助函数 - 判断数组是否有序(升序)
template
bool isSorted(T arr[], int n)
{
for(int i = 0; i < n - 1; i++)
{
if(arr[i] > arr[i + 1])
{
return false;
}
}
return true;
}
//辅助函数 - 测试算法的时间
template
void testSort(string sortname, void(*sort)(T[], int), T arr[], int n) //arr[]和n是函数指针需要的参数
{
clock_t starttime = clock();
sort(arr, n); //调用函数sort()
clock_t endtime = clock();
//判断排序是否成功
assert(isSorted(arr, n)); //若是数组无序,则assert会自动调用abort()退出程序,不会执行下面的语句
cout << sortname << " needs " << double(endtime - starttime) / CLOCKS_PER_SEC << "s." << endl;
}
//辅助函数 - 拷贝数组
int* copyIntArray(int a[], int n)
{
int *arr = new int[n];
//使用C++函数copy()
copy(a, a + n, arr);
return arr;
}
}
main.cpp文件(包含归并排序和快排排序算法)
#include
#include"SortTestHelper.h"
using namespace std;
//对arr[l...r]进行patition操作
//返回p,使得arr[l...p-1]arr[p+1...r]
template
int __patition(T arr[], int l, int r)
{
int j = l;
for(int i = l + 1; i <= r; i++)
{
if(arr[i] < arr[l])
{
swap(arr[j + 1], arr[i]);
j++;
}
}
swap(arr[l], arr[j]);
return j;
}
//对arr[l...r]进行插入排序
template
void insertionSort(T arr[], int l, int r)
{
for(int i = l + 1; i <= r; i++)
{
T e = arr[i];
int j;
for(j = i; j > l && arr[j - 1] > e; j--)
{
arr[j] = arr[j - 1];
}
arr[j] = e;
}
}
template
void __quickSort(T arr[], int l, int r) //对arr[l...r]进行快速排序
{
//优化底层递归
if(r - l <= 15) //即只有16个元素时
{
//采用插入排序
insertionSort(arr, l, r);
return;
}
else
{
int p = __patition(arr, l, r); //__patition()函数将arr[l...r]分为两部分,该函数返回一个索引值
__quickSort(arr, l, p - 1);
__quickSort(arr, p + 1, r);
}
}
template
void quickSort(T arr[], int n)
{
__quickSort(arr, 0, n - 1);
}
//归并排序
template
void __merge(T arr[], int l, int mid, int r) //[l...r](前闭后闭)
{
T aux[r - l + 1];
for(int i = l; i <= r; i++) //i是aux的下标
{
aux[i - l] = arr[i];
}
//i、j是arr中的下标,k是arr中的下标
//i-l、j-l是aux中的下标
int i = l, j = mid + 1, k = l;
while(i <= mid && j <= r)
{
if(aux[i - l] < aux[j - l])
{
arr[k++] = aux[i - l];
i++;
}
else
{
arr[k++] = aux[j - l];
j++;
}
}
//出界条件
while(i <= mid)
{
arr[k++] = aux[i - l];
i++;
}
while(j <= r)
{
arr[k++] = aux[j - l];
j++;
}
}
template
void __mergeSort(T arr[], int l, int r)
{
if(l >= r)
{
return;
}
else
{
int mid = (l + r) / 2;
//对左半部分arr[l...mid]进行归并排序
__mergeSort(arr, l, mid);
//再对右半部分arr[mid + 1...r]进行归并排序
__mergeSort(arr, mid + 1, r);
//然后将排好序的左右两部分归并到一起
__merge(arr, l, mid, r);
}
}
template
void mergeSort(T arr[], int n)
{
//传递一个数组,调用归并排序算法归并arr[0...n-1]
__mergeSort(arr, 0, n - 1);
}
int main()
{
//测试 - 两个基本有序的数组
int n = 70000;
int swaptime = 100;
int *arr = SortTestHelper::generateNearlyOrderedArray(n, swaptime);
int *arr2 = SortTestHelper::copyIntArray(arr, n);
SortTestHelper::testSort("mergeSort", mergeSort, arr, n);
SortTestHelper::testSort("qucikSort2", quickSort, arr2, n);
delete[] arr;
delete[] arr2;
return 0;
}
两者的时间性能如下:
快排的时间性能相比归并排序相差甚远,这是我们不希望得到的。其中的原因是,归并排序时,是采用二分法对序列进行划分,得到的递归树深度为logn,而每一层上的算法时间复杂度为O(n),所以归并排序的时间复杂度为O(nlogn),而快排虽然也有将序列进行划分的操作,但是我们是选择序列的第一个元素作为标定点,在找到标定点的位置j后将序列进行划分,得到的左右两个子序列可能长度相差比较大,然后接着又对左右两个子序列找到标定点的位置继续划分,这样得到的递归树是不平衡的,树的深度可能超过logn,于是快排的时间复杂度会超过O(nlogn)。当待排序列是一个完全有序的序列时,得到递归树的深度为n,此时快排的时间复杂度退化为O(n^2),所以快排对于基本有序或者完全有序的序列来说,时间性能是很差的。
序列基本有序的情况:
序列完全有序的情况:
所到底就是一个递归树平衡性好坏的问题,那么应该如何改进快排算法呢?
我们之前是选取每个子序列的第一个元素作为标定点,这样做很容易在序列基本有序的情况下得到平衡性较差的递归树,实际上我们可以随机选取待排序列中的一个元素作为标定点,可以通过数学方法证明,当我们随机选取一个元素作为标定点时,快排算法的时间复杂度的期望值为O(nlogn),此时其退化成O(n^2)的概率几乎为0。
随机选择标定点的快排算法的C++实现可以在快速排序算法的基础上进行一些小的改动即可实现:即选择随机的一个元素,然后将其和序列的第一个元素进行交换,其他代码不变。
完整的程序代码如下,并将优化过的快排算法和归并算法用来测试基本有序的序列,观察其时间性能的变化。
SortTestHelper.h文件(包含辅助函数)
#include
#include
#include //clock()、CLOCKS_PER_SEC
#include //包含函数assert()
using namespace std;
namespace SortTestHelper
{
//辅助函数 - 随机产生一个数组
int* generateRandomArray(int n, int RangeL, int RangeR) //返回数组首地址
{
//判断RangeL是否<=RangeR
assert(RangeL <= RangeR); //参数为表达式,表达式为真时返回true,否则打印错误信息
int *arr = new int[n];
srand(time(0));
for(int i = 0; i < n ; i++)
{
arr[i] = rand() % (RangeR - RangeL + 1) + RangeL; //使得产生的随机数在RangeL和RangeR之间
}
return arr;
}
//辅助函数 - 产生一个近乎有序的随机数组
int* generateNearlyOrderedArray(int n, int swapTime)
{
int *arr = new int[n];
for(int i = 0; i < n; i++)
{
arr[i] = i; //先生成一个完全有序的数组
}
//然后交换几组元素,使之变成无序但近乎有序的数组
srand(time(0));
for(int j = 0; j < swapTime; j++)
{
//随机生成一个x位置和y位置
int posx = rand() % n;
int posy = rand() % n;
//交换x和y处的元素
swap(arr[posx], arr[posy]);
}
return arr;
}
//辅助数组 - 产生一个完全有序数组
int* generateTotallyOrderedArray(int n)
{
int *arr = new int[n];
for(int i = 0; i < n; i++)
{
arr[i] = i;
}
return arr;
}
//辅助函数 - 打印数组
template
void printArray(T arr[], int n)
{
for(int i = 0; i < n; i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
//辅助函数 - 判断数组是否有序(升序)
template
bool isSorted(T arr[], int n)
{
for(int i = 0; i < n - 1; i++)
{
if(arr[i] > arr[i + 1])
{
return false;
}
}
return true;
}
//辅助函数 - 测试算法的时间
template
void testSort(string sortname, void(*sort)(T[], int), T arr[], int n) //arr[]和n是函数指针需要的参数
{
clock_t starttime = clock();
sort(arr, n); //调用函数sort()
clock_t endtime = clock();
//判断排序是否成功
assert(isSorted(arr, n)); //若是数组无序,则assert会自动调用abort()退出程序,不会执行下面的语句
cout << sortname << " needs " << double(endtime - starttime) / CLOCKS_PER_SEC << "s." << endl;
}
//辅助函数 - 拷贝数组
int* copyIntArray(int a[], int n)
{
int *arr = new int[n];
//使用C++函数copy()
copy(a, a + n, arr);
return arr;
}
}
main.cpp文件
#include
#include
#include
#include"SortTestHelper.h"
using namespace std;
//对arr[l...r]进行patition操作
//返回p,使得arr[l...p-1]arr[p+1...r]
template
int __patition(T arr[], int l, int r)
{
//以序列的第一个元素arr[l]作为标定点
//T v = arr[l];
//在arr[l...r]中随机选取一个元素作为标定点,并将其和序列第一个元素交换
swap(arr[l], arr[rand() % (r - l + 1) + l]);
T v = arr[l];
int j = l;
for(int i = l + 1; i <= r; i++)
{
if(arr[i] < v)
{
swap(arr[j + 1], arr[i]);
j++;
}
}
swap(arr[l], arr[j]);
return j;
}
//对arr[l...r]进行插入排序
template
void insertionSort(T arr[], int l, int r)
{
for(int i = l + 1; i <= r; i++)
{
T e = arr[i];
int j;
for(j = i; j > l && arr[j - 1] > e; j--)
{
arr[j] = arr[j - 1];
}
arr[j] = e;
}
}
template
void __quickSort(T arr[], int l, int r) //对arr[l...r]进行快速排序
{
/*if(l >= r)
{
return;
}*/
//优化底层递归
if(r - l <= 15) //即只有16个元素时
{
//采用插入排序
insertionSort(arr, l, r);
return;
}
else
{
int p = __patition(arr, l, r); //__patition()函数将arr[l...r]分为两部分,该函数返回一个索引值
__quickSort(arr, l, p - 1);
__quickSort(arr, p + 1, r);
}
}
template
void quickSort(T arr[], int n)
{
srand(time(0)); //设置时间种子
__quickSort(arr, 0, n - 1);
}
//归并排序
template
void __merge(T arr[], int l, int mid, int r) //[l...r](前闭后闭)
{
T aux[r - l + 1];
for(int i = l; i <= r; i++) //i是aux的下标
{
aux[i - l] = arr[i];
}
//i、j是arr中的下标,k是arr中的下标
//i-l、j-l是aux中的下标
int i = l, j = mid + 1, k = l;
while(i <= mid && j <= r)
{
if(aux[i - l] < aux[j - l])
{
arr[k++] = aux[i - l];
i++;
}
else
{
arr[k++] = aux[j - l];
j++;
}
}
//出界条件
while(i <= mid)
{
arr[k++] = aux[i - l];
i++;
}
while(j <= r)
{
arr[k++] = aux[j - l];
j++;
}
}
template
void __mergeSort(T arr[], int l, int r)
{
if(l >= r)
{
return;
}
else
{
int mid = (l + r) / 2;
//对左半部分arr[l...mid]进行归并排序
__mergeSort(arr, l, mid);
//再对右半部分arr[mid + 1...r]进行归并排序
__mergeSort(arr, mid + 1, r);
//然后将排好序的左右两部分归并到一起
__merge(arr, l, mid, r);
}
}
template
void mergeSort(T arr[], int n)
{
//传递一个数组,调用归并排序算法归并arr[0...n-1]
__mergeSort(arr, 0, n - 1);
}
int main()
{
//测试 - 两个基本有序的数组
int n = 500000;
int swaptime = 100;
int *arr = SortTestHelper::generateNearlyOrderedArray(n, swaptime);
int *arr2 = SortTestHelper::copyIntArray(arr, n);
SortTestHelper::testSort("mergeSort", mergeSort, arr, n);
SortTestHelper::testSort("qucikSort2", quickSort, arr2, n);
delete[] arr;
delete[] arr2;
return 0;
}
测试结果如下:
相比选序列第一个元素作为标定的快排算法,这里选取随机元素作为标定点的时间性能和归并排序差不多,这样优化以后,快排算法无论是在待排序列完全无序的情况下还是在待排序列基本有序的情况下,时间性能基本都能接近O(nlogn),退化为O(n^2)的概率是非常小的。
但是到这里快排算法任然存在缺陷,这点将在下一节进行讲解。