听说三四年前,MSRA的面试题就是手撕鼎鼎大名的快排算法(现在估计已经男上加男了),其实如果了解了Partition的核心思想,写快排简直是分分钟啊!长话短说,今天就来讲一讲Partition算法的思想。
Partition 除了用在快速排序中,还广泛用于在无序数组中寻找第K大的值的算法中,Partition算法可以有效的将O(NlogN)降至O(N),算法思想是通过一次扫描就解决任务。
Partition的主要过程就是首先确定一个值privot,这个值就是划分数组的依据,大于等于privot的都位于privot右边,小于等于privot的都位于privot左边,最后再返回privot最终所在的位置即可。根据这个算法思路,一个实现Partition算法的简单版本如下,该算法从左到右扫描对数组进行划分,定义一个指针位置pos,在扫描的过程中,pos指向不满足条件待交换的元素的位置,一旦发现需要交换,不断交换元素即可。其中函数变量privot_posisiton表示的是对privot元素最初在数组arr中的位置。
//正向游走
void partition_pos(int arr[], int beginn, int endd, int privot_posisiton){
if (privot_posisiton >= endd && privot_posisiton < beginn){
return ;
}
int privot = arr[privot_posisiton];
int pos = beginn - 1;
for (int i = beginn; i < endd; i++){
if (arr[i] <= privot){
pos++;
if (pos < i){
swap(arr[i], arr[pos]);
}
}
/*
cout<<"i = "<<i<<" ; pos = "<<pos<<" ; ";
for (int j = beginn; j < endd; j++){
cout<<arr[j]<<" ";
}
cout<<endl;
*/
}
//swap(arr[pos], arr[privot_posisiton]);
}
除了从左到右扫描对数组进行划分,也可以从右到左扫描进行划分,本质上代码没有不同,代码如下:
//逆向游走
void partition_neg(int arr[], int beginn, int endd, int privot_posisiton){
if (privot_posisiton >= endd && privot_posisiton < beginn){
return ;
}
int privot = arr[privot_posisiton];
int pos = endd;
for (int i = endd - 1; i >= beginn; i--){
if (arr[i] >= privot){
pos--;
if (pos > i){
swap(arr[i], arr[pos]);
}
}
/*
cout<<"i = "<<i<<" ; pos = "<<pos<<" ; ";
for (int j = beginn; j < endd; j++){
cout<<arr[j]<<" ";
}
cout<<endl;
*/
}
//swap(arr[pos], arr[privot_posisiton]);
}
观察这两个算法,我们可以发现,待交换的元素总是位于pos位置,再交换到此时扫描的位置,但这是此时扫描的位置不一定位于privot之后,因此会产生对同一个元素的多次交换,算法效率不高。结合从左扫描和从右扫描的思想,我们可以在一次循环中同时从左至右和从右至左扫描,然后交换两元素的位置,直到两头扫描的指针汇合扫描结束,代码如下:
//双向游走
void partition_bot(int arr[], int beginn, int endd, int privot_posisiton){
if (privot_posisiton >= endd && privot_posisiton < beginn){
return ;
}
int privot = arr[privot_posisiton];
endd = endd - 1;
while (beginn < endd){
while (beginn < endd && arr[beginn] < privot){
beginn++;
}
while (beginn < endd && arr[endd] > privot){
endd--;
}
if (beginn >= endd){
break;
}
else{
swap(arr[beginn], arr[endd]);
beginn++;
endd--;
}
/*
cout<<"begin = "<<beginn<<" ; end = "<<endd<<" ; ";
for (int j = 0; j < 10; j++){
cout<<arr[j]<<" ";
}
cout<<endl;
*/
}
}
除此之外,我看到一篇非常不错的博客,博客里也写到了双向扫描算法,比我写的简洁,粘贴如下:
int partition(vector<int>&arr, int begin, int end)
{
int pivot = arr[begin];
while(begin < end)
{
while(begin < end && arr[--end] >= pivot);
arr[begin] = arr[end];
while(begin < end && arr[++begin] <= pivot);
arr[end] = arr[begin];
}
arr[begin] = pivot;
return begin;
}
值得注意的是,这个算法也用到了swap的思想,但是非常隐晦,程序的意思是,我先把第一个数保存下来,第二个待交换的数赋值给第一个数,第三个待交换的数赋值给第二个数,以此类推,最后把保存的第一个数赋值给最后一个数,完成了交换,算法效率明显更高。参考博客(http://selfboot.cn/2016/09/01/lost_partition/)图例如下:
这里有Partition单向游走和双向游走算法复杂度比较的博客: https://cs.stackexchange.com/questions/11458/quicksort-partitioning-hoare-vs-lomuto/11550