1.快速排序
快速排序的基本思想是,每次选取一个数组元素中的基准值(通常为首元素)作为参考,将这个数组分为左右两部分,左边的部分全部小于这个基准值的右边的部分全部大于这个基准值。然后在左右子序列重复这个步骤最后就能得到有序序列。从这个算法描述来看,很显然这是一种不稳定的排序算法。并且如果选择的基准值不合适就可能会导致数据全部在左边或者右边,这时候会使得快排的效率极大降低。
因此我们可以通过以下常用的两种方式来解决这个问题:第一种方式就是每次选基准值的时候都是从数组中选取一个随机的元素放到第一个位置来做基准值。但是这种方式仍然可能会出现上述的情况。第二种方式就是我们每次可以选择在数组的首尾以及中间三个元素中取中间的值放到首元素位置上,这种方式比选取随机值的方式更有效。
下面先给出三数取中的方式
/*
int GetMid(vector<int>& a,int left,int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if(a[right] < a[left])
{
return left;
}
else
{
return right;
}
}
else//a[left]>a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[right] > a[left])
{
return left;
}
else
{
return right;
}
}
}
显而易见的是,这种思想很容易使用递归实现,下面给出一个霍尔版本的递归快排。
/*void QuickSort(vector<int>& a,int left,int right)
{
if(left >= right)
return;
int begin = left;
int end = right;
int midi = GetMid(a,left,right);
swap(a[left],a[midi]);
int keyi = left;
while(begin < end)
{
//先走右边,找小于key的
while(begin < end && a[end] >= a[keyi])
{
--end;
}
//后走左边,找大于key的
while(begin < end && a[begin] <= a[keyi])
{
++begin;
}
swap(a[begin],a[end]);
}
swap(a[keyi],a[begin]);
QuickSort(a,left,begin - 1);
QuickSort(a,begin + 1,right);
}
接下来我们来实现它的非递归实现,在实现之前,有3个问题需要解决。
第一个问题是:我们先来分析快排到底是怎么控制元素的?通过上面的代码可以看到,实际上快排是每次通过控制它的区间来控制递归排序的。从最后两行的递归调用代码就可以看到这一点。
第二个问题是:我们非递归实现的思路是什么呢?最容易想到的就是我们也去模拟它的递归的过程,将每次排序单独写一个函数放在外面,每次控制好区间之后调用这个函数并且顺带返回它选取的基准值,这里的基准值还是使用三数取中的方式,这样就能达到模拟递归实现的效果。
第三个问题是:我们如何将它每次排序完的区间保存下来并且更新呢?这里我们可以利用栈的特性实现这个效果,这里的思路就是我们可以在每次排序之前先把之前排好序的区间出栈,把下一次将排序的区间入栈,每次top取栈顶元素得到待排序的区间。只要栈不为空就重复上面的过程。
下面给出具体的代码:
int _QuickSort(vector<int>& a,int left,int right)
{
int begin = left;
int end = right;
int midi = GetMid(a,left,right);
swap(a[left],a[midi]);
int keyi = left;
while(begin < end)
{
//先走右边
while(begin < end && a[keyi] <= a[end])
{
--end;
}
//后走左边
while(begin < end && a[keyi] >= a[begin])
{
++begin;
}
swap(a[end],a[begin]);
}
swap(a[begin],a[keyi]);
return begin;
}
void QuickSort(vector<int>& a,int left,int right)
{
if(left >= right)
return;
stack<pair<int,int>> st;
st.push({left,right});
while(!st.empty())
{
pair<int,int> interval = st.top();
st.pop();
int keyi = _QuickSort(a,interval.first,interval.second);//得到分区的那个元素下标
// 检查区间的合法性
if(interval.first < keyi - 1)
st.push({interval.first,keyi-1});
if(interval.second > keyi + 1)
st.push({keyi+1,interval.second});
}
}
*/
2.归并排序
归并排序的基本思想是:将带排序的序列分为子序列,使得每一个子序列有序之后再合并这些子序列就能得到有序的序列。这是分治法的经典应用
同样从算法描述来看使用递归很容易实现归并排序,我们只需要关心分组后的子序列的问题即可。
下面给出归并排序的递归实现:
void mergSort(vector<int>& a,int left,int right,vector<int>& _temp)
{
if(left >= right)
return;
int mid = (left + right)/2;
mergSort(a,left,mid,_temp);
mergSort(a,mid+1,right,_temp);
//只关心分组后的子问题
int begin1 = left,end1 = mid;
int begin2 = mid+1,end2 = right;
int i = left;
while(begin1 <= end1 && begin2 <= end2)
{
if(a[begin1] < a[begin2])//第一组的当前元素小
{
_temp[i++] = a[begin1++];
}
else
{
_temp[i++] = a[begin2++];
}
}
//要么是第一组没完,要么是第二组没完
while(begin1 <= end1)
{
_temp[i++] = a[begin1++];
}
while(begin2 <= end2)
{
_temp[i++] = a[begin2++];
}
for (i = left; i <= right; ++i)
{
a[i] = _temp[i];
}
}
接下来我们来实现归并排序的非递归实现。首先对于归并的非递归我们需要将数组分为若干个子数组,然后将子数组两两合并直到整个数组有序。这里我们可以控制一个步长来对于数组进行分组,每次归并排完子数组之后更新gap进入下一次分组。但是在这里需要注意分组之后的下标越界问题,每次分分组之后都要对于下标进行检查更新。具体的检查逻辑在我在代码中给出了详细的注释,读者根据代码理解可能更好理解
void mergSort(vector<int>& a,int n,vector<int>& temp)
{
//归并排序的非递归事实上也是模拟递归的分组实现的过程
int gap = 1;//第一次排序时每组只有一个元素进行排序,这里代表的是分组后子数组的大小
while(gap < n)
{
for(int i = 0;i < n; i += gap * 2)
{
int begin1 = i,end1 = i + gap -1;
int begin2 = i + gap,end2 = i + 2 * gap -1;
int j = i;//这里设置一个中间数组的小标能够存入排序的数组
//注意检查越界
//1.begin1不会越界
//2.end1,begin2,end2会越界
//当end1和begin2越界时,都可以说明第二个子数组不存在也就不需要归并
//当end2越界时,这时候把下标调整到整个数组的最后一个元素即可
if(end1 >= n || begin2 >= n)
{
break;
}
if(end2 >= n)
{
end2 = n - 1;
}
while(begin1 <= end1 && begin2 <= end2)
{
if(a[begin1] < a[begin2])
{
temp[j++] = a[begin1++];
}
else
{
temp[j++] = a[begin2++];
}
}
while(begin1 <= end1)
{
temp[j++] = a[begin1++];
}
while(begin2 <= end2)
{
temp[j++] = a[begin2++];
}
for (int k = i; k <= end2; k++)
{
a[k] = temp[k];
}
}
gap *= 2;
}
}