1、快速排序的基本思想:
快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字
均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
2、快速排序的三个步骤:
(1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 "基准"(pivot)
(2)分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素
都比该基准小,在基准右边的元素都比基准大
(3)递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。
3、选择基准的方式
对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。
也就是说,基准的选择是很重要的。
选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。
最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列
/*
3种选择基准
固定位置:
如果数组已经有序时,此时的分割就是一个非常不好的分割
随机选择基准:
前提 待排序列是部分有序序列
三数取中:
虽然随机选取枢轴时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),
要缓解这种情况,就引入了三数取中选取枢轴.使用三数中值分割法消除了预排序
输入的不好情形,并且减少快排大约14%的比较次数
使用三数取中选择枢轴优势还是很明显的,但是还是处理不了重复数组
4种优化
优化1:
当待排序序列的长度分割到一定大小后,使用插入排序
优化2:
在一次分割结束后,可以把与Key相等的元素聚在一起,
继续下次分割时,不用再对与key相等元素分割
优化3:
优化递归操作
快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化
*/
//共用函数
#define MAX_LENGTH_INSERT_SORT 7 //数组长度阈值
void printArray(int array[], int len)
{
int i = 0;
for (i = 0; i<len; i++)
{
printf("%d ", array[i]);
}
printf("\n");
}
void swap(int array[], int i, int j)
{
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
//插入排序 要点 1 拿出要插入的元素 2 符合条件的元素后移
void inertionSort(int array[], int len)// O(n*n)
{
int i = 0;
int j = 0;
int k = -1;
int temp = -1;
for (i = 1; i < len; i++)
{
k = i;//待插入位置
temp = array[k]; //拿出要插入的元素
/
for (j = i - 1; (j >= 0) && (array[j] > temp); j--)
{
array[j + 1] = array[j]; //元素后移
k = j; //k需要插入的位置
}
///
array[k] = temp;//元素插入
}
}
//使用三数取中法选择枢轴
int SelectPivotMedianOfThree(int array[], int low, int high)
{
//完成三数取中
int mid = (low + high) / 2; //计算数组中间的元素下标
if (array[high] < array[low]){
swap(array, high, low); //交换左端与右端的数据,保证左端较小
}
if (array[high] < array[mid]){
swap(array, high, mid); //交换中间与右端的数据,保证中间较小
}
if (array[mid] > array[low]){
swap(array, mid, low); //交换中间与左端数据,保证左端较小
}
//完成三数取中,此时array[low]已经成为整个序列左中右三个关键字的中间值
return array[low];
}
/*
固定位置
思想:取序列的第一个或最后一个元素作为基准
*/
#if 0
//分组 数组作为函数参数,将退化为指针
//划分过程 第一个元素当枢轴,分成2个有序子序列
int partition(int array[], int low, int high)
{
int arrayLen = high+1;
int pv = array[low];//默认选取序列中的第一个元素作为pv值(中轴元素)
printf("pv = %d\n", pv);//枢轴
while (low < high)//两个指针不重合
{
while ((low < high) && (array[high] >= pv))
{
high--;//比基准大,本来就在右边,所以high向左移动
}
swap(array, low, high);//否则就交换
while ((low < high) && (array[low] <= pv))
{
low++;//比基准小,本来就在左边,所以low向右移动
}
swap(array, low, high);//否则就交换
}
printArray(array, arrayLen);
return low;//返回pv值(枢轴)得位置
}
//让n个元素 依此减少 减少到1个元素的时候,因为1个元素可以看成一个有序的序列
void QSort(int array[], int low, int high)
{
if (low < high)
{
//找出 pv值 中心点
int pivot = partition(array, low, high);
//对中轴元素左边进行排序
QSort(array, low, pivot - 1);
//对中轴元素右边进行排序
QSort(array, pivot + 1, high);
}
}
void QuickSort(int array[], int len) // O(n*logn)
{
QSort(array, 0, len - 1);
}
/*
测试数据分析:
如果输入序列是随机的,处理时间可以接受的。如果数组已经有序时,此时的分割就是一个非常不好的分割。
因为每次划分只能使待排序序列减一,此时为最坏情况,快速排序沦为起泡排序,时间复杂度为Θ(n^2)。
而且,输入的数据是有序或部分有序的情况是相当常见的。因此,使用第一个元素作为枢纽元是非常糟糕的,
为了避免这个情况,就引入了下面两个获取基准的方法。
*/
#endif
/*
随机选取基准
引入的原因:在待排序列是部分有序时,固定选取枢轴使快排效率底下,要缓解这种情况,
就引入了随机选取枢轴
思想:取待排序列中任意一个元素作为基准
*/
#if 0
int partition(int array[], int low, int high)
{
int arrayLen = high+1;
srand((unsigned)time(NULL));//随机选择枢轴的位置,区间在low和high之间
int pvPos = rand() % (high - low) + low;//把枢轴位置的元素和low位置元素互换,此时可以和普通的快排一样调用划分函数
swap(array, pvPos, low);
int pv = array[low];
printf("----------随机中枢值pv = %d\n", pv);//枢轴
while (low < high)//两个指针不重合
{
while ((low < high) && (array[high] >= pv))
{
high--;//比基准大,本来就在右边,所以high向左移动
}
swap(array, low, high);//否则就交换
while ((low < high) && (array[low] <= pv))
{
low++;//比基准小,本来就在左边,所以low向右移动
}
swap(array, low, high);//否则就交换
}
printArray(array, arrayLen);
return low;//返回pv值(枢轴)得位置
}
//让n个元素 依此减少 减少到1个元素的时候,因为1个元素可以看成一个有序的序列
void QSort(int array[], int low, int high)
{
if (low < high)
{
//找出 pv值 中心点
int pivot = partition(array, low, high);
//对中轴元素左边进行排序
QSort(array, low, pivot - 1);
//对中轴元素右边进行排序
QSort(array, pivot + 1, high);
}
}
void QuickSort(int array[], int len) // O(n*logn)
{
QSort(array, 0, len - 1);
}
/*
测试数据分析:
如果输入序列是随机的,处理时间可以接受的。如果数组已经有序时,此时的分割就是一个非常不好的分割。
因为每次划分只能使待排序序列减一,此时为最坏情况,快速排序沦为起泡排序,时间复杂度为Θ(n^2)。
而且,输入的数据是有序或部分有序的情况是相当常见的。因此,使用第一个元素作为枢纽元是非常糟糕的,
为了避免这个情况,就引入了下面两个获取基准的方法。
*/
#endif
/*
快速排序优化1:三数取中并且优化不必要的交换
(1)优化选取中轴元素
普通快排pv是选取数组第一个元素,若pv的大小不在整个数组的中间位置,
会大大降低快排的性能,因此快速排序的速度还取决于pv关键元素在数组
中的位置。
[改进方法]
三数取中法:取三个元素先进行排序,将中间数作为中轴元素,一般是取左端、右端、和
中间三个数,也可以随机选取。这样,至少这个中间数一定不会是最小或者最大的数。
随机选取三个数和从左中右选取三个数是一回事,而且随机数生成器本身还会带来时间上的
开销,因此随机生成不予考虑。
(2)优化交换
将swap交换改成直接赋值,减少多次交换数据操作,在性能上得到部分提高
//取三个元素先进行排序,将中间值作为中轴元素,选取左端、右端、中间三个数,也可以随机选取
*/
#if 0
int three_choose_one_optimize_fast_partition(int array[], int low, int high)
{
int arrayLen = high + 1;
//完成三数取中
int pv = SelectPivotMedianOfThree(array, low, high);
printf("pv = %d\n", pv);//枢轴
while (low < high)//两个指针不重合
{
while ((low < high) && (array[high] >= pv))
{
high--;
}
array[low] = array[high];//减少多次交换数据的操作,采用替换而不是交换的方式进行操作
while ((low < high) && (array[low] <= pv))
{
low++;
}
array[high] = array[low];//采用替换而不是交换的方式进行操作
}
array[low] = pv;
printArray(array, arrayLen);
return low;//返回pv值(枢轴)得位置
}
//让n个元素 依此减少 减少到1个元素的时候,因为1个元素可以看成一个有序的序列
void QSort(int array[], int low, int high)
{
if (low < high)
{
//三数取中 快排
int pivot = three_choose_one_optimize_fast_partition(array, low, high);
//对中轴元素左边进行排序
QSort(array, low, pivot - 1);
//对中轴元素右边进行排序
QSort(array, pivot + 1, high);
}
}
void QuickSort(int array[], int len) // O(n*logn)
{
QSort(array, 0, len - 1);
}
/*
三数取中对小数组来说有很大的概率选择到一个比较好的pv,但是对于非常大的
待排序的序列来说还不足以保证能够选择出一个好的pv,因此可以九数取中。
先从数组中分三次取样,每次取三个数,三个样品各取出中数,然后从这三个数
再取出一个中数作为枢轴
测试数据分析:
使用三数取中选择枢轴优势还是很明显的,但是还是处理不了重复数组
*/
#endif
/*
优化小数组时的排序方案
以上两种优化方法是充分利用了快速排序解决大数组排序强大的功能,
那么相反的情况,当数组非常小,其实快速排序不如直接插入排序来的好。
本质原因还是因为快速排序用到了递归,在大量数据排序时,这点性能影响
可以通过上面的优化方法忽略掉,但是对于一个只有几个元素的数组需要
排序时,用快排就像“大炮打蚊子”。
解决方案就是设定一个判断分支:
当数组长度大于7,则使用优化2的快排
当数组长度小于7,则使用直接插入排序
*/
#if 0
int three_choose_one_optimize_fast_partition(int array[], int low, int high)
{
int arrayLen = high + 1;
//完成三数取中
int pv = SelectPivotMedianOfThree(array, low, high);
printf("pv = %d\n", pv);//枢轴
while (low < high)//两个指针不重合
{
while ((low < high) && (array[high] >= pv))
{
high--;
}
array[low] = array[high];//减少多次交换数据的操作,采用替换而不是交换的方式进行操作
while ((low < high) && (array[low] <= pv))
{
low++;
}
array[high] = array[low];//采用替换而不是交换的方式进行操作
}
array[low] = pv;
printArray(array, arrayLen);
return low;//返回pv值(枢轴)得位置
}
//让n个元素 依此减少 减少到1个元素的时候,因为1个元素可以看成一个有序的序列
void QSort(int array[], int low, int high)
{
int arrayLen = high + 1;
int pivot;
if ((high - low) < MAX_LENGTH_INSERT_SORT){//当high-low大于常数时用快速排序
pivot = three_choose_one_optimize_fast_partition(array, low, high);
QSort(array, low, pivot - 1); //对中轴元素左边进行递归排序
QSort(array, pivot + 1, high); //对中轴元素右边进行递归排序
}
else{//当high-low 小于等于常数时用直接插入排序
inertionSort(array, arrayLen);// O(n*n)
}
}
void QuickSort(int array[], int len) // O(n*logn)
{
QSort(array, 0, len - 1);
}
#endif
/*
优化递归操作:快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化
递归对性能是有一定影响的,快排在其尾部有两次递归操作,如果待排序的序列划分极端不平衡,
递归深度将趋近于n,而不是平衡时的logn。这就不仅仅是速度快慢的问题了,栈的大小是有限的,
每次递归调用都会耗费一定的栈空间,函数的参数越多,每次递归耗费的空间也越多,因此,
减少递归会大大提高性能
优化:
用一次迭代来换取一次递归的代码,此代码主要对快排的递归部分修改,中轴元素的选取
和排序部分代码沿用优化1方案的。
优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(logn),将会提高性能。
*/
#if 0
int three_choose_one_optimize_fast_partition(int array[], int low, int high)
{
int arrayLen = high + 1;
//完成三数取中
int pv = SelectPivotMedianOfThree(array, low, high);
printf("pv = %d\n", pv);//枢轴
while (low < high)//两个指针不重合
{
while ((low < high) && (array[high] >= pv))
{
high--;
}
array[low] = array[high];//采用替换而不是交换的方式进行操作
while ((low < high) && (array[low] <= pv))
{
low++;
}
array[high] = array[low];//采用替换而不是交换的方式进行操作
}
array[low] = pv;
printArray(array, arrayLen);
return low;//返回pv值(枢轴)得位置
}
//让n个元素 依此减少 减少到1个元素的时候,因为1个元素可以看成一个有序的序列
void QSort(int array[], int low, int high)
{
int arrayLen = high + 1;
if ((high - low) < MAX_LENGTH_INSERT_SORT){//当high-low大于常数时用快速排序
//当high-low 小于等于常数时用直接插入排序
inertionSort(array, arrayLen);// O(n*n)
return;
}
//if (low < high)
int pivot = -1;
while (low < high)//迭代换递归,从if换成while
{
//三数取中 快排
pivot = three_choose_one_optimize_fast_partition(array, low, high);
//对中轴元素左边进行排序
QSort(array, low, pivot - 1);
low = pivot + 1;//尾递归
}
}
void QuickSort(int array[], int len) // O(n*logn)
{
QSort(array, 0, len - 1);
}
#endif
/*
在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,
不用再对与key相等元素分割,能减少迭代次数,效率会提高不少
具体过程:在处理过程中,会有两个步骤
第一步,在划分过程中,把与key相等元素放入数组的两端
第二步,划分结束后,把与key相等的元素移到枢轴周围
*/
#if 1
int three_choose_one_optimize_fast_partition(int array[], int low, int high)
{
int arrayLen = high + 1;
//完成三数取中
int pv = SelectPivotMedianOfThree(array, low, high);
printf("pv = %d\n", pv);//枢轴
while (low < high)//两个指针不重合
{
while ((low < high) && (array[high] >= pv))
{
high--;
}
array[low] = array[high];//采用替换而不是交换的方式进行操作
while ((low < high) && (array[low] <= pv))
{
low++;
}
array[high] = array[low];//采用替换而不是交换的方式进行操作
}
array[low] = pv;
printArray(array, arrayLen);
return low;//返回pv值(枢轴)得位置
}
//让n个元素 依此减少 减少到1个元素的时候,因为1个元素可以看成一个有序的序列
void QSort(int array[], int low, int high)
{
int arrayLen = high + 1;
int first = low;
int last = high;
int left = low;
int right = high;
int leftLen = 0;
int rightLen = 0;
if ((high - low) < MAX_LENGTH_INSERT_SORT){//当high-low大于常数时用快速排序
//当high-low 小于等于常数时用直接插入排序
inertionSort(array, arrayLen);// O(n*n)
return;
}
if (low < high)
{
//三数取中 快排
//int pivot = three_choose_one_optimize_fast_partition(array, low, high);
//一次分割
int pv = SelectPivotMedianOfThree(array, low, high);
printf("pv = %d\n", pv);//枢轴
while (low < high)//两个指针不重合
{
while ((low < high) && (array[high] >= pv))
{
/*********************************/
//处理相等元素
if (array[high] == pv){
swap(array, right, high);
right--;
rightLen++;
}
/*********************************/
high--;
}
array[low] = array[high];//采用替换而不是交换的方式进行操作
while ((low < high) && (array[low] <= pv))
{
/*********************************/
//处理相等元素
if (array[low] == pv){
swap(array, left, low);
left++;
leftLen++;
}
/*********************************/
low++;
}
array[high] = array[low];//采用替换而不是交换的方式进行操作
}
array[low] = pv;
//一次快排结束
//把与枢轴key相同的元素移到枢轴最终位置周围
int i = low - 1;
int j = first;
while (j < left&&array[i] != pv){
swap(array, i, j);
i--;
j++;
}
i = low + 1;
j = last;
while (j > right&&array[i] != pv){
swap(array, i,j);
i++;
j--;
}
printArray(array, arrayLen);
//对中轴元素左边进行排序
QSort(array, first, low - 1-leftLen);
//对中轴元素右边进行排序
QSort(array, low + 1+rightLen, last);
}
}
void QuickSort(int array[], int len) // O(n*logn)
{
QSort(array, 0, len - 1);
}
#endif
int main()
{
//int array[] = {9, 1, 5, 8, 3, 7, 4, 6, 2};
int array[] = { 1, 4, 6, 7, 6, 6, 7, 6, 8, 6 };
//int array[] = {12, 5, 433};
//int array[] = { 12, 5 };
int len = sizeof(array) / sizeof(*array);
printf("------待排序数组------\n");
printArray(array, len);
printf("------------快排-----------\n");
QuickSort(array, len);
printf("------------快排后-----------\n");
printArray(array, len);
system("pause");
return 0;
}