将一个数组按照某个划分值,划分为三部分,即小于该值的部分,等于部分和大于部分,这个问题称为荷兰国旗问题。
由于荷兰国旗由三种颜色组成,而该问题是将数组划分为三部分,两者具有相似性,所以该问题由此得名。
简化版荷兰国旗问题
先考虑简化版问题。将一个数组划分为两部分,如小于等于划分值的部分和大于部分。这里可以想到使用双指针。两个指针分别是小于等于区的右边界和遍历时下标,这样就把数组分为三部分,小于等于区、大于区、待遍历区。
这里划分值取为5。
这样,就相当于是小于等于区推着大于区往后遍历,当数组遍历完成后,数组划分也顺利完成。
int flag1(vector<int> &arr)
{
int i=0,small=-1;//遍历数组的指针和小于等于区域的左边界
int key=arr[0];//划分值这里取数组的第一个值,也可以取其他值
while(i<arr.size())
{
if(arr[i]<=key)
{
swap(arr[small+1],arr[i]);//交换小于等于区的下个数和该遍历数
++small;
}
++i;
}
return 0;
}
这里swap()的功能是将两个数进行交换,这里代码省略。
荷兰国旗问题
现在考虑将数组划分为三部分的问题。由上面简化版问题而来,这里再多设置一个指针,表示大于区右边界。这样,数组就分为了四部分,小于区、等于区、待遍历区、大于区。
这里划分值依然取为5。
这样,就相当于是小于区推着等于区往后遍历,而大于区逐渐向左扩。当数组遍历到大于区边界时,数组划分也顺利完成。
int flag2(vector<int> &arr)
{
int i=0,small=-1;//遍历数组的指针和小于区域的右边界
int big=arr.size();//大于区域的左边界
int key=arr[0];//划分值这里取数组的第一个值,也可以取其他值
while(i<big)
{
if(arr[i]<key)
{
swap(arr[small+1],arr[i]);//交换小于区的下个数和该遍历数
++small;
++i;
}else if(arr[i]==key)
{
++i;
}else
{
swap(arr[big-1],arr[i]);
--big;
}
}
return 0;
}
快速排序
快速排序是应用较广的排序算法,其本质就是以数组中的某个值为划分值,将数组进行划分,从而确定这个值的位置,而后再进行递归或迭代。这里面就用到了荷兰国旗问题。
快速排序1.0
1.0版本的快速排序算法,就是以数组中的第一个值为划分值,将数组划分为小于等于区和大于区两部分,而后再将该值与小于等于区最后一个值交换,将该值放到最后的位置。而后继续递归或迭代。
int partition1(vector<int> &arr,int left,int right)
{
if(left>=right)
{
return 0;
}
int i=left,small=left-1;//遍历数组的指针和小于等于区域的左边界
int key=arr[left];//划分值这里依然取数组的第一个值
while(i<=right)
{
if(arr[i]<=key)
{
swap(arr[small+1],arr[i]);
++small;
}
++i;
}
swap(arr[left],arr[small]);//将划分值放到小于等于区的最后一个位置上
partition1(arr,0,small-1);
partition1(arr,small+1,right);
}
int qsort1(vector<int> &arr)
{
partition1(arr,0,arr.size()-1);
return 0;
}
快速排序2.0
2.0版本,与1.0版本的区别,在于多划分了一个等于区。1.0与2.0的区别就如同荷兰国旗问题的简化版与正式版。
int partition2(vector<int> &arr,int left,int right)
{
if(left>=right)
{
return 0;
}
int i=left,small=left-1;//遍历数组的指针和小于区域的右边界
int big=right+1;//大于区域的左边界
int key=arr[left];
while(i<big)
{
if(arr[i]<key)
{
swap(arr[small+1],arr[i]);//交换小于区的下个数和该遍历数
++small;
++i;
}else if(arr[i]==key)
{
++i;
}else
{
swap(arr[big-1],arr[i]);
--big;
}
}
partition2(arr,0,small);
partition2(arr,big,right);
return 0;
}
int qsort2(vector<int> &arr)
{
partition2(arr,0,arr.size()-1);
return 0;
}
注意arr[i]比划分值大时,i值不变。
2.0版本比1.0版本稍快,因为2.0版本一次解决了一批数。
快速排序3.0
1.0和2.0最大的弊端,在于原数组在基本有序的情况下,划分值有可能选的很偏,使得时间复杂度逼近O(N2),为了改善这一点,我们可以随机选取划分值,而后将其与原数组第一个值交换,这样就与1.0,2.0版本一致了。
int partition3(vector<int> &arr,int left,int right)
{
if(left>=right)
{
return 0;
}
int i=left,small=left-1;//遍历数组的指针和小于等于区域的左边界
int index=left+rand()%(right-left+1);//划分值在排序范围内随机抽取一个数
swap(arr[index],arr[left]);//将该抽取值与范围内第一个值交换,接下来就和1.0一样了
int key=arr[left];
while(i<=right)
{
if(arr[i]<=key)
{
swap(arr[small+1],arr[i]);
++small;
}
++i;
}
swap(arr[left],arr[small]);//将划分值放到小于等于区的最后一个位置上
partition3(arr,0,small-1);
partition3(arr,small+1,right);
}
int qsort3(vector<int> &arr)
{
partition3(arr,0,arr.size()-1);
return 0;
}
注意,如果分为四部分,可以不用交换随机选取的值和数组第一个值。
int partition4(vector<int> &arr,int left,int right)
{
if(left>=right)
{
return 0;
}
int i=left,small=left-1;//遍历数组的指针和小于区域的右边界
int big=right+1;//大于区域的左边界
int key=arr[left+rand()%(right-left+1)];
while(i<big)
{
if(arr[i]<key)
{
swap(arr[small+1],arr[i]);//交换小于区的下个数和该遍历数
++small;
++i;
}else if(arr[i]==key)
{
++i;
}else
{
swap(arr[big-1],arr[i]);
--big;
}
}
partition4(arr,0,small);
partition4(arr,big,right);
return 0;
}
int qsort4(vector<int> &arr)
{
partition4(arr,0,arr.size()-1);
return 0;
}
这样的话,由于都是随机选取划分值,时间复杂度为O(Nlog N)。
另外,快速排序的空间复杂度为O(log N)。在1.0与2.0版本中,最差情况会变为O(N)。
对数器
最后,还是把对数器代码贴出来
int compare()//对数器
{
int times=1;
vector<int> arr1;
for(;times<=TIME_N;++times)
{
for(int i=0;i<LEN;++i)
{
arr1.push_back(rand()&1023);
}
vector<int> arr2(arr1);
qsort3(arr1);
qsort4(arr2);
//比较两个排序后的数据
if(arr1!=arr2)
{
return times;
}
}
return 0;
}
这里可以用不同的排序方法对比,返回值为0则说明算法基本没有问题。