快速排序的底层递归优化和针对基本有序序列的优化
2017-09-10 18:46
543 查看
在归并排序算法的优化这一节提到过,高级排序算法几乎有一个通用的优化方法,就是对递归底层的操作进行优化。先来看快速排序算法:
测试两个基本有序的数组用归并排序和快排排序的时间性能:
SortTestHelper.h文件(包含辅助函数)
main.cpp文件(包含归并排序和快排排序算法)
快排的时间性能相比归并排序相差甚远,这是我们不希望得到的。其中的原因是,归并排序时,是采用二分法对序列进行划分,得到的递归树深度为logn,而每一层上的算法时间复杂度为O(n),所以归并排序的时间复杂度为O(nlogn),而快排虽然也有将序列进行划分的操作,但是我们是选择序列的第一个元素作为标定点,在找到标定点的位置j后将序列进行划分,得到的左右两个子序列可能长度相差比较大,然后接着又对左右两个子序列找到标定点的位置继续划分,这样得到的递归树是不平衡的,树的深度可能超过logn,于是快排的时间复杂度会超过O(nlogn)。当待排序列是一个完全有序的序列时,得到递归树的深度为n,此时快排的时间复杂度退化为O(n^2),所以快排对于基本有序或者完全有序的序列来说,时间性能是很差的。
序列基本有序的情况:
序列完全有序的情况:
所到底就是一个递归树平衡性好坏的问题,那么应该如何改进快排算法呢?
我们之前是选取每个子序列的第一个元素作为标定点,这样做很容易在序列基本有序的情况下得到平衡性较差的递归树,实际上我们可以随机选取待排序列中的一个元素作为标定点,可以通过数学方法证明,当我们随机选取一个元素作为标定点时,快排算法的时间复杂度的期望值为O(nlogn),此时其退化成O(n^2)的概率几乎为0。
随机选择标定点的快排算法的C++实现可以在快速排序算法的基础上进行一些小的改动即可实现:即选择随机的一个元素,然后将其和序列的第一个元素进行交换,其他代码不变。
完整的程序代码如下,并将优化过的快排算法和归并算法用来测试基本有序的序列,观察其时间性能的变化。
SortTestHelper.h文件(包含辅助函数)
main.cpp文件
相比选序列第一个元素作为标定的快排算法,这里选取随机元素作为标定点的时间性能和归并排序差不多,这样优化以后,快排算法无论是在待排序列完全无序的情况下还是在待排序列基本有序的情况下,时间性能基本都能接近O(nlogn),退化为O(n^2)的概率是非常小的。
但是到这里快排算法任然存在缺陷,这点将在下一节进行讲解。
//对arr[l...r]进行patition操作 //返回p,使得arr[l...p-1]<arr[p],arr[p]>arr[p+1...r] template<typename T> 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<typename T> 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<typename T> void quickSort(T arr[], int n) { __quickSort(arr, 0, n - 1); }在__quickSort()中,对底层递归语句进行优化:
//对arr[l...r]进行插入排序 template<typename T> 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<typename T> 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 <iostream> #include <cstdlib> #include <ctime> //clock()、CLOCKS_PER_SEC #include <cassert> //包含函数assert() using namespace std; namespace SortTestHelper { //辅助函数 - 随机产生一个数组 int* generateRandomArray(int n, int RangeL, int RangeR) //返回数组首地址 { //判断RangeL是否<=RangeR assert(RangeL <= RangeR); //参数为表达式,表达式为真时返回true,否则打印错误信息 int *arr = new int ; 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 ; 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 ; for(int i = 0; i < n; i++) { arr[i] = i; } return arr; } //辅助函数 - 打印数组 template<typename T> void printArray(T arr[], int n) { for(int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; } //辅助函数 - 判断数组是否有序(升序) template<typename T> 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<typename T> 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 ; //使用C++函数copy() copy(a, a + n, arr); return arr; } }
main.cpp文件(包含归并排序和快排排序算法)
#include <iostream> #include"SortTestHelper.h" using namespace std; //对arr[l...r]进行patition操作 //返回p,使得arr[l...p-1]<arr[p],arr[p]>arr[p+1...r] template<typename T> 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<typename T> 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<typename T> 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<typename T> void quickSort(T arr[], int n) { __quickSort(arr, 0, n - 1); } //归并排序 template<typename T> 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 4000 (j <= r) { arr[k++] = aux[j - l]; j++; } } template<typename T> 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<typename T> 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 <iostream> #include <cstdlib> #include <ctime> //clock()、CLOCKS_PER_SEC #include <cassert> //包含函数assert() using namespace std; namespace SortTestHelper { //辅助函数 - 随机产生一个数组 int* generateRandomArray(int n, int RangeL, int RangeR) //返回数组首地址 { //判断RangeL是否<=RangeR assert(RangeL <= RangeR); //参数为表达式,表达式为真时返回true,否则打印错误信息 int *arr = new int ; 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 ; 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 ; for(int i = 0; i < n; i++) { arr[i] = i; } return arr; } //辅助函数 - 打印数组 template<typename T> void printArray(T arr[], int n) { for(int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; } //辅助函数 - 判断数组是否有序(升序) template<typename T> 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<typename T> 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 ; //使用C++函数copy() copy(a, a + n, arr); return arr; } }
main.cpp文件
#include <iostream>测试结果如下:
#include <ctime>
#include <cstdlib>
#include"SortTestHelper.h"
using namespace std;
//对arr[l...r]进行patition操作
//返回p,使得arr[l...p-1]<arr[p],arr[p]>arr[p+1...r]
template<typename T>
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<typename T> 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<typename T> 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<typename T>
void quickSort(T arr[], int n)
{
srand(time(0)); //设置时间种子
__quickSort(arr, 0, n - 1);
}
//归并排序
template<typename T>
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<typename T>
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<typename T>
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)的概率是非常小的。
但是到这里快排算法任然存在缺陷,这点将在下一节进行讲解。
相关文章推荐
- 快速排序基本操作的优化(完整代码)
- 快速排序 优化 (QuickSort)Java数据结构与算法
- java快速排序,优化
- 快速排序基本问题
- 基本排序之快速排序
- C代码学习,快速排序,左右并进,递归调用(另注释)
- 快速排序lua实现 递归和栈两种实现
- 笔试算法题(56):快速排序实现之非递归实现,最小k值选择(non-recursive version, Minimal Kth Selection of Quick Sort)
- 每日编程7之快速排序非递归版本
- java实现快速排序-递归
- 百度面试,快速排序非递归实现
- 快速排序Java源码(递归和非递归)
- 快速排序的递归方法总结
- 基本排序方法的优化
- 排序-快速排序-优化-使用三向切分(优化重复元素的情况
- 快速排序的非递归实现
- 递归分治问题之找出两个有序序列的中间值
- java基本排序算法-插入排序-快速排序-选择排序-冒泡排序
- 快速排序的分析与优化
- 快速排序基本思路