最近在啃算法导论,看完了关于快排的章节,用代码实现了一遍,决定写个blog记录一下,日后忘记了好拿来复习。
一、快速排序的思想
快速排序使用了分治思想——假设现在手头有一个待排序的数组A:
int a[] = { 1,2,5,6,7,4,2,10,9,2,30,1,4,11 }; //任意数组
快速排序首先从A中任取一个基准值A0,遍历整个数组,将整个数组元素以A0为基准分为左右两个区域,左边的都小于A0,右边的都大于A0。举个例子,倘若取最后一个元素11作为基准值A0,以11为界限划分数组会得到如下结果:
{1,2,5,6,7,4,2,10,9,2,1,4,11,30}
现在我们只能说对数组进行了微调,为什么是微调?因为整个数组中只有11这一个元素(也就是基准值)走到了它最终的位置,而且大部分元素位置并无改变。我们先忽略微调算法的实现过程(一会再讲),继续思考如何实现整个数组的排序。
接下来,我们以11为界,将整个数组分成左右两个半区,我们就得到了一模一样的两个子问题:
A1 = {1,2,5,6,7,4,2,10,9,2,1,4}
A2 = {30}
这里的两个“{}”只是方便理解,并非物理上两个新的数组,我们的“分割”操作都是在原数组A上通过下标访问进行的。
接下来怎么做?对每个新的子问题重复进行“选基准值——局部微调——分割区域”的操作,显然可以递归实现,递归的出口就是只剩下一个元素时,不需要再微调。结果如下:
A = {1,1,2,2,2,4,4,5,6,7,9,10,11,30};
总结一下,快速排序的思想就是分治法+递归,通过一步步微调实现整个数组的有序。下面来看具体的实现方法:
二、快速排序算法实现
由于算法是递归的,所以关键就是如何实现“选基准值——局部微调——分割区域”这一操作。
- 选基准值
为了简单起见,我们仍然选取当前数组(或者子数组)的最后一个元素为基准值 A0。接下来就是通过一次遍历剩下的元素完成微调过程。
- 局部微调
在这里我们维护两个下标指针 i 和 j,i 之前保存小于基准值的序列,i 之后 j 之前保存大于基准值的序列,当 j = size(A) - 1停止遍历,最后将基准值和 A[i+1] 作交换,初始时 i = -1,j = 0,让 j 指针向后滑动。
倘若 A[j] < A0,发现一个应当前移的元素,将i 指针向后移动(i += 1),然后交换 A[j] 与 A[i] 。
图看不懂的可以直接看代码:
int Partition(int* array, int begin, int end) //左闭右开
{
int ref = array[end - 1]; //参考值
int i = begin-1;
for (int j = begin; j < end - 1; j++) //遍历插入
{
if (array[j] <= ref)
{
i += 1;
int temp = array[j];
array[j] = array[i];
array[i] = temp;
}
}// 指针i之前的元素是<end的
int temp = array[end - 1];
array[end - 1] = array[i + 1];
array[i + 1] = temp;
return i + 1;
}
- 分割区域
分割的工作很简单,因为我们已知当前数组的起始和结束,所以只要在函数中返回基准值的下标就OK了。
最后的最后,我们直接用递归调用子函数实现整个数组的全部有序:
void Quick_Sort(int* array, int begin, int end)
{
if (begin < end-1) //最少2个元素,否则直接返回
{
int q = Partition(array, begin, end);
Quick_Sort(array, begin, q);
Quick_Sort(array, q+1, end);
}
}
主函数调用一下打印结果看看:
int main()
{
int array[] = { 1,2,5,6,7,4,2,10,9,2,30,1,4,11 };
Quick_Sort(array, 0, size(array));
for (int i : array)
cout << i << " ";
return 0;
}
以上就是学习快排的一些收获,有用的话就点个赞吧~