本文主要是关于快速排序算法的优化,想了解各种排序算法的基础原理请看
图解常用十大排序算法
快速排序算法优化
我们直到快速排序算法利用了分治的思想,即将一组序列分割成独立的两部分(俗称划分),其中一部分的数据比另一部分的数据小,然后再按照此方法对两部分序列再分别进行快速排序,当划分的两部分包含的数据个数越接近时,快速排序的时间复杂度越小(这也就是为什么我们说快速排序不适合对基本有序的序列进行排序,因为这样会出现划分的两部分数据个数差别大的情况)因此基准的选择对快速排序来说是很重要的。
基准的选取
固定基准:
选择序列首元素、序列中间元素或序列尾元素作为基准元素。此基准选择的方法就是开头的文章中讲的内容,因此不细讲了。即:
int Partition(elemtype arr[],int low,int high)
{
int left=low,right=high;
int base = arr[low];//选择序列首元素作为基准元素
...
随机基准:
当待排序序列已经基本有序时,如果使用固定基准作为基准元素,会大大降低快速排序的效率,因此可以生成一个序列长度范围内的随机数作为基准元素的索引下标(因为快排在划分过程中low是变化的,因此索引下标是相对于low的偏移量)
int Swap(int arr[],int low,int tar)
{
int temp = arr[low];
arr[low] = arr[tar];
arr[tar] = temp;
return arr[low];
}
int Partition_random(elemtype arr[],int low,int high)//随机数基准
{
srand((unsigned)time(NULL));
int left=low,right=high;
int r_index = rand()%(high - low)+low; //获得随机基准数的偏移地址
int base = Swap(arr,low,r_index); //讲基准数作与Arr[low]交换
while(left<right)
{
while(left<right && arr[right]>=base)
{
right--;
}
while(left<right && arr[left]<=base)
{
left++;
}
if(left<right)
{
Swap(arr,right,left);
right --;
left ++;
}
}
if(arr[left]>base)
{
Swap(arr,low,left-1);
return left-1;
}else
{
Swap(arr,low,left);
return left;
}
}
三数取中:
目前一个比较好的基准数选择是选择序列首元素、中间元素和为元素中的中间值作为基准元素。这种方式能很好的解决待排数组基本有序的情况,而且选取的基准没有随机性,算是对上述两种基准选择方式的升华吧~
int Get_MidNumer_index(int arr[],int left,int right)//去中间数并返回
{
int temp,mid;
mid = (right-left)/2+left;
if(arr[left] > arr[mid])
{
temp = arr[left];
arr[left] = arr[mid];
arr[mid] = temp;
}
if(arr[left] > arr[right])
{
temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
if(arr[mid] > arr[right])
{
temp = arr[mid];
arr[mid] = arr[right];
arr[right] = temp;
}
return mid;
}
//算法优化部分//
int Partition_mid(elemtype arr[],int low,int high)//三数取中作基准
{
int left=low,right=high;
int mid = Get_MidNumer_index(arr,low,high);
int base = Swap(arr,low,mid);
while(left<right)
{
while(left<right && arr[right]>=base)
{
right--;
}
while(left<right && arr[left]<=base)
{
left++;
}
if(left<right)
{
Swap(arr,right,left);
right --;
left ++;
}
}
if(arr[left]>base)
{
Swap(arr,low,left-1);
return left-1;
}else
{
Swap(arr,low,left);
return left;
}
}
绝对中值:
上述的三数取中并不能取到序列中的绝对中间值,如果能够取到绝对中间值,那么对于划分成两个长度相等的序列是有很大帮助的!那么如何高效求取序列的中间值也是一个问题(因为也会涉及到排序),可以将序列划分为多个子序列,将每个子序列的中间值存到一个新的Mid序列中,再求取Mid序列中间值即为原本序列中间值。
int Get_AbsMid(elemtype arr[],int left,int right)
{
int len = right - left + 1;
int bufNums = (len%5 == 0)?(len/5):(len/5+1);
elemtype * mids = (elemtype*)malloc(sizeof(elemtype)*bufNums);
int index = 0;
for(int i=0;i<bufNums;i++)
{
if(i == bufNums-1)
{
BinInsert_Sort(arr,left+i*5,right);
mids[index++] = arr[(left+i*5+right)/2];
}else
{
BinInsert_Sort(arr,left+i*5,left+i*5+4);
mids[index++] = arr[(left+i*5+2)/2];
}
}
BinInsert_Sort(mids,0,bufNums-1);
for(int i = left;i <= right;i++)
{
if(arr[i] == mids[(bufNums-1)/2])
return i;
}
}
int Partition_AbsMid(elemtype arr[],int low,int high)//三数取中作基准
{
int left=low,right=high;
int mid = Get_AbsMid(arr,low,high);
int base = Swap(arr,low,mid);
while(left<right)
{
while(left<right && arr[right]>=base)
{
right--;
}
while(left<right && arr[left]<=base)
{
left++;
}
if(left<right)
{
Swap(arr,right,left);
right --;
left ++;
}
}
if(arr[left]>base)
{
Swap(arr,low,left-1);
return left-1;
}else
{
Swap(arr,low,left);
return left;
}
}
void Quick_AbsMid_Sort(elemtype arr[],int low,int high)
{
int mid;
if(high - low < LIMIT)
{
BinInsert_Sort(arr,low,high);
return;
}
if(low<high)
{
mid = Partition_AbsMid(arr,low,high);
QuickSort(arr,low,mid-1);
QuickSort(arr,mid+1,high);
}
}
算法优化:
快速排序+插入排序
我们知道插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,那么插入排序还是一个不错的选择。尤其当数据基本有序时,采用插入排序可以明显减少数据交换和数据移动次数,进而提升排序效率(比如希尔排序就是利用了插入排序在数据小且基本有序时的优点)
快速排序每次均会划分两个小于原长度的序列,那么当序列长度减小到小于一个设定值时,我们便可以使用插入排序替换快速排序,进而使排序效率提高!(当然,也可以使用折半插入排序)
void BinInsert_Sort(elemtype arr[],int left,int right)//适应快速排序的折半插入排序
{
int j,x,low,high,mid;
for(int i=left+1;i<=right;i++)
{
arr[0] = arr[i]; //哨兵记录
low = left;
high = i-1;
while(low <= high) //确定插入的位置
{
mid = (low + high)/2;
if(arr[0] < arr[mid])
high = mid - 1;
else
low = mid + 1;
}
for(j = i-1;j>=low;j--) //将该位置之后的元素后移
arr[j+1]=arr[j];
arr[low] = arr[0]; //插入
}
}
void Quick_Insert_Sort(elemtype arr[],int low,int high)//快速排序+插入排序
{
int mid;
if(high - low < LIMIT)//序列长度小于一定值时进行折半插入排序
{
BinInsert_Sort(arr,low,high);
return;
}
if(low<high)
{
mid = Partition_mid(arr,low,high);
QuickSort(arr,low,mid-1);
QuickSort(arr,mid+1,high);
}
}
基数聚集
有时侯我们往往会遇到序列中有许多和基准数重复的数,可以把与基准数相等的元素聚在一起,继续下次分割时,不用再对与基准数相等元素进行分割。
例如有如下序列8 1 6 7 6 4 6 6 9 6
通过三数取中选取基准元素8 6 6
中6作为基准元素,arr[low]和基准元素交换后序列为6 1 6 7 8 4 6 6 9 6
然后将与基准元素相等的元素放到数组的两端,得到序列6 1 4 6 8 7 9 6 6 6
再将两端的与基准数据相等的数聚集到基准数两边4 1 6 6 6 6 6 9 7 8
然后再分别对序列4 1
和9 7 8
进行快速排序
void Quick_Gather_Sort(elemtype arr[],int low,int high)
{
int first = low; //first暂存本次快排的左界限
int last = high; //last暂存本次快排的右界限
int left_Gather_Num = 0; //左边与基准数相同的个数
int right_Gather_Num = 0; //右边与基准数相同的个数
int left = low;
int right = high;
if (high - low < LIMIT) //小于一定个数进行插入排序代替
{
BinInsert_Sort(arr,low,high);
return;
}
int mid = Get_AbsMid(arr,low,high); //取三数中数下标
int base = Swap(arr,low,mid); //交换元素并获得基准元素
while(low < high) //将与基准元素相同的数放置到序列两边
{
while(high > low && arr[high] >= base) //右放置
{
if (arr[high] == base) //处理相等元素
{
Swap(arr,right,high);
right--;
right_Gather_Num++;
}
high--;
}
arr[low] = arr[high];
while(high > low && arr[low] <= base) //左放置
{
if (arr[low] == base) //处理相等元素
{
Swap(arr,left,low);
left++;
left_Gather_Num++;
}
low++;
}
arr[high] = arr[low];
}
arr[low] = base;
//开始聚集
int i = low - 1;
int j = first;
while(j < left && arr[i] != base)
{
Swap(arr,i,j);
i--;
j++;
}
i = low + 1;
j = last;
while(j > right && arr[i] != base)
{
Swap(arr,i,j);
i++;
j--;
}
//再对两边的序列进行快速排序
Quick_Gather_Sort(arr,first,low - 1 - left_Gather_Num);
Quick_Gather_Sort(arr,low + 1 + right_Gather_Num,last);
}