1.归并排序
基本思想:不断的将数据进行二分(逻辑上的二分,即将数组元素索引分块,并不是真的把数组内存进行二分),当二分到只有一个数组元素时开始归并,数组的每一个元素由其二分后数组的两个元素进行比较确定:
首先进行二分:
二分到第三层数组中仅有一个元素(即数组有序),然后开始向上进行归并:第二层左边的数组 [ 0 ] 位置元素由其二分后数组元素比较得出,显然应该是3,那么 [ 1 ]位置就是6,同理第二层右边数组归并后是1,4。再次向上归并,第一层数组[0]位置由其二分数组【3,6】【1,4】的第一个元素比较确定,3>1所以[ 0 ]位置元素是1,现在由【3,6】【4】确定[1]位置元素, 3<4所以[1]位置是3,现在由【6】【4】确定[ 3 ]位置元素。。。。
下面是代码实现:
template<class T>
static void mergSort (T arr[],int size)
{
int l = 0;
int r = size-1;
p_mergeSort(arr,l,r);
}
template<typename T>
static void p_mergeSort(T arr[],int l,int r)
{
if(l>=r) //递归结束的标记,即二分后数组中只有一个元素
return ;
int mid = (l+r)/2;
p_mergeSort(arr,l,mid);
p_mergeSort(arr,mid+1,r);
p_merge(arr,l,mid,r);
}
//归并,使用两个已经有序的二分数组确定上一级数组的顺序
template<typename T>
static void p_merge(T arr[],int l,int mid,int r)
{
T *cpyArr = new T[r-l+1];
for(int i=0;i<r-l+1;i++)
cpyArr[i] = arr[l+i];
//i,j分别表示二分后的数组当前参与比较的元素的索引(即二分后数组中的最小值)
int i=l,j=mid+1;
for(int k=l;k<=r;k++)
{
//当前数组某一二分后的数组元素全部比较完之后,i(j)任然会加1,所以先进行判断下标
if(i>mid)
{
arr[k] = cpyArr[j-l];
j++;
}
else if(j>r)
{
arr[k] = cpyArr[i-l];
i++;
}else if(cpyArr[i-l]<cpyArr[j-l]) //判断当前数组元素应该是哪个二分数组中的最小值
{
arr[k]=cpyArr[i-l];
i++;
}
else
{
arr[k] = cpyArr[j-l];
j++;
}
}
delete cpyArr;
}
归并排序优化
再次整理归并排序的基本思想:首先将数组逐级二分,二分到只有一个元素时二分后的数组就是有序的,然后使用两个有序的二分子数组归并(即合成一个有序数组),那么归并后的数组元素一定是两个二分数组当前最小值的其中之一。
上述代码中归并的过程可以从两个方面来优化:1.在两个二分数组都是有序的情况下,如果索引靠前的数组的最大值小于另一数组的最小值,那么该数组本身就是有序的。2.当数据元素个数小于某一阈值的情况下,O(n^2)算法的效率更高,即可以提前结束归并的递归,不再是二分后元素个数为1开始归并,而是数据元素小于15(确定最合适的阈值还需要大量思考,有兴趣可以深入研究)时使用插入排序将二分数组进行排序(为什么使用插入排序,参考我上一篇博客https://blog.csdn.net/j_oop/article/details/106380136)。下面给出代码:
template<typename T>
static void p_mergeSortPlus(T arr[],int l,int r)
{
if(r-l<=15) //当数组元素少于某一值时,O(n^2)算法的排序效率更高,提前结束归并使用插入排序
{
insertSort2(arr,l,r);
return ;
}
int mid = (l+r)/2;
p_mergeSortPlus(arr,l,mid);
p_mergeSortPlus(arr,mid+1,r);
if(arr[mid]>arr[mid+1]) //如果二分后的两个数组本身就有序的那么不用再进行处理
p_merge(arr,l,mid,r);
}
值得注意的是:判断索引靠前的数组的最大值是否小于另一数组的最小值本身就需要开销,在实际使用中是否加入这一优化要视具体情况甄别。
2.快速排序:
基本思想:每次遍历将数组分成两部分:存在索引k,k之前的元素小于arr[k],k之后的元素大于arr[k],然后再将两个子数组分别迭代。
template<class T>
static void quickSort(T arr[],int size)
{
p_quickSort(arr,0,size-1);
}
template<class T>
static void p_quickSort(T arr[],int l,int r)
{
if(l>=r)
return ;
int k = p_partion(arr,l,r);
p_quickSort(arr,l,k-1);
p_quickSort(arr,k+1,r);
}
template<typename T>
static int p_partion(T arr[],int l,int r)
{
int k = l+1;
T val = arr[l]; //使用第一个元素作为比较的对象
for(int i = l+1;i<=r;i++)
{
if(arr[i]<val)
{
swap(arr[i],arr[k]);
k++;
}
}
swap(arr[l],arr[k-1]);
return k-1;
}
快速排序优化
1.和归并排序一样,当数组元素少于15时采用插入排序;2.上诉代码存在一个问题,当数组时接近有序数组时时间复杂度接近O(n^2),因为相当于每次遍历将数组分成了一个长度为0和长度为n两个子数组。解决方法:每次用于比较的数组元素不从第一个索引处取值,而是从数组随机位置取值,代码如下:
template<class T>
static void p_quickSort(T arr[],int l,int r)
{
//优化1:数组元素少于15时使用插入排序
/*if(l>=r)
return ;*/
if(r-l<=15)
{
insertSort2(arr,l,r);
return ;
}
int k = p_partion(arr,l,r);
p_quickSort(arr,l,k-1);
p_quickSort(arr,k+1,r);
}
template<typename T>
static int p_partion(T arr[],int l,int r)
{
int k = l+1;
//优化2:不使用数组的开始元素作为比较对象,使用随机位置元素,避免了对接近有序数组进行排序时效率降低
swap(arr[l],arr[l+rand()%(r-l+1)]);
T val = arr[l];
for(int i = l+1;i<=r;i++)
{
if(arr[i]<val)
{
swap(arr[i],arr[k]);
k++;
}
}
swap(arr[l],arr[k-1]);
return k-1;
}
双路快速排序:
上面的优化虽然解决了对接近有序数组排序时的问题,但是对于拥有大量重复元素的数组,排序后还是会出现二分的数组极度不平均,使得算法效率接近O(n^2)。要解决这个问题思路之一是:把重复元素分别放在二分后的两个数组中,即双路快速排序
基本思想:由数组两端向中间遍历,每当遇到左边的元素大于v,右边的元素小于v时,交换两者位置,这样把数组分成了<=v和>=v的两部分且两部分,再分别迭代处理两部分,代码如下:
///快速排序(双路排序):从数组两端向中间遍历将数组分成>v和小于v的两部分,解决了当数组中有大量重复元素时效率低的问题
template<class T>
static void quickSort2(T arr[],int size)
{
p_quickSort2<int>(arr,0,size-1);
}
template<class T>
static void p_quickSort2(T arr[],int l,int r)
{
if(r-l<=15)
{
insertSort2(arr,l,r);
return ;
}
int k = p_partion2<T>(arr,l,r);
p_quickSort2(arr,l,k-1);
p_quickSort2(arr,k+1,r);
}
template<class T>
static int p_partion2(T arr[],int l,int r)
{
swap(arr[l],arr[l+rand()%(r-l+1)]);
T val = arr[l];
int i = l+1;
int j = r;
while(true)
{
//注意:此处判断必须是<和>而不是<=和>=,否则当遇到大量重复元素时仍然会将数据很不平均的分成两部分,
//使得算法效率接近O(n^2),即使这样增加了交换的次数
while(arr[i]<val && i<=r)
i++;
while(arr[j]>val && j>= l+1)
j--;
if(i>j)
break;
swap(arr[i],arr[j]);
i++;
j--;
}
swap(arr[l],arr[j]);
return j;
}
三路快速排序:
双路快速排序通过将重复元素分开存放在两个子数组中解决了大量重复元素的问题,还有一种更高效的思路是:从头至尾遍历数组,将数组分成>v,==v,<v三部分,并迭代>v和<v的部分
///快速排序(三路排序):从两端向中间遍历,将数组分为三部分:<v,==v,>v,再分别遍历,<v和>v的部分
template<class T>
static void quickSort3(T arr[],int size)
{
p_quickSort3(arr,0,size-1);
}
template<typename T>
static void p_quickSort3(T arr[],int l,int r)
{
srand(time(NULL));
if(r-l<=15)
{
insertSort2(arr,l,r);
return ;
}
swap(arr[l],arr[rand()%(r-l+1)+l]);
T v = arr[l];
int i,j,lt,gt;
i = l+1;
j = r;
lt = l;
gt = r+1;
while(i<gt)
{
if(arr[i]<v)
{
swap(arr[i],arr[lt+1]);
i++;
lt++;
}else if(arr[i]>v)
{
swap(arr[i],arr[gt-1]);
gt--;
}else{
i++;
}
}
swap(arr[l],arr[lt]);
p_quickSort3(arr,l,lt-1);
p_quickSort3(arr,gt,r);
}