1.前言:
看到这篇标题,没有学过快排的人自然是不知道Partition函数的意思和作用,这里附上学习的连接
Lantian的快排总结
我们现在都是被快排蒙蔽了双眼,没有意识到快最核心的划分函数Partition,当然Partition函数也就不止于快排这里,本文就从多方面来为展示Partition函数的本质和扩展作用
2.快排中的Partition及其优化:
在算法导论中我们对Partition函数的定义是这样的:
PARTITION(A, p, r)
x = A[r]//选择最后一个元素作为比较元素
i = p – 1//这个慢速移动下标必须设定为比最小下表p小1,否则两个元素的序列比如2,1无法交换
for j = p to r-1//遍历每个元素
{
if (A[j] <= x)//比较
{
i = i + 1//移动慢速下标
Exchange A[i] with A[j ]//交换
}
}
Exchange A[i+1] with A[r]//交换
return i + 1//返回分割点
这里我就不再过多的赘述算法的具体细节,我在这里强调的是对快排中的Partition的优化
首先,在原本的算法中,假如我们已经选定了一个枢纽元,那么对于所有的比枢纽元小的元素我们都执行了swap操作
这是非常的低效的
在这里我们引入双向的Partition算法,在双向的算法中,我们对于比枢纽院小的元素并不是都执行了swap操作,只有数组右边的一部分的比枢纽元小的元素我们才执行了swap操作,大大减少了swap操作的次数从而降低时间复杂度
含有Partition函数的快速排序,标准算法导论上的描述实现
#include"iostream"
#include"cstdio"
#include"cstring"
#include"cstdlib"
#define N 1005
using namespace std;
int data[N];
int number;
void init_data()
{
scanf("%d",&number);
for(int i=0;i<number;i++) scanf("%d",&data[i]);
}
void partition(int left,int& mid,int right)
{
int i=left-1;
int j=left;
int var=data[right];
while(j<right)
{
if(data[j]<var)
{
i++;
int temp=data[i];
data[i]=data[j];
data[j]=temp;
}
j++;
}
i++;
int temp=data[i];
data[i]=var;
data[right]=temp;
mid=i;
return ;
}
void quicksort(int left,int right)
{
if(left>=right) return ;
else
{
int mid;
partition(left,mid,right);
quicksort(left,mid-1);
quicksort(mid+1,right);
return ;
}
}
void print()
{
for(int i=0;i<number;i++) printf("%d ",data[i]);
printf("\n");
}
int main()
{
init_data();
quicksort(0,number-1);
print();
return 0;
}
双向的Partition我在我的连接中已经写明了
你们可能对这一点还是有所怀疑,我们来做一做实验:
注意,一下的代码是Python编写的,为了方便实验
import random
from time import *
global data
data=[]
def partition_1():
global data
i=-1
j=0
var=data[len(data)-1]
while j<len(data)-1:
if data[j]<var:
i+=1
temp=data[i]
data[i]=data[j]
data[j]=temp
j+=1
i+=1
temp=data[i]
data[i]=var
data[len(data)-1]=temp
def partition_2():
global data
i=0
j=len(data)-1
temp=data[0]
while i!=j:
while i<j and data[j]>=temp:
j-=1
while i<j and data[i]<=temp:
i+=1
if i!=j:
t=data[i]
data[i]=data[j]
data[j]=t
data[0]=data[i]
data[i]=temp
def test_init():
global data
data=eval(input())
def init_data():
global data
for i in range(1000000):
data.append(random.randint(-100000,100000))
init_data()
time1=clock()
partition_1()
print("单向partition耗时:%f"%(clock()-time1))
time1=clock()
partition_2()
print("双向partition耗时:%f"%(clock()-time1))
#test_init()
#partition_2()
实验结果:
单向partition耗时:0.833947
双向partition耗时:0.277176
很显然,在数据量越来越大的时候,我们的双向Partition与单向的Partition比是非常的高效的
双向partition耗时:0.277176
很显然,在数据量越来越大的时候,我们的双向Partition与单向的Partition比是非常的高效的
3.Top-k Question:
对于Top-k问题已经被问烂了,解法也是很多的
当然我们优秀的解法有:
O(n*logk)的堆排序实现
BST/SBT二叉查找树,自平衡二叉查找树实现:忽略建树时间的话,我们的而时间复杂度是O(logn)
现在我们完全可以利用Partition函数来进行实现:
对于Partition函数的该操作的实现,很多人又叫这种方法快速选择算法(注意选择之后得到的不是有序的数组只是一个有了划分的数组)
算法的本质是通过每次遍历Partition分组操作之后确定Tok-k的位置所在,然后递归查找所在的那一边区域(相当于是BST或者二分查找的思路)
时间复杂度是O(n)
时间复杂度的推导:
T(n)=O(n)+T(n/2)
...log2n=k次迭代
T(n)=O(n)+O(n/2)...O(1)
T(n)=O(n*(1+1/2+...1/n))=O(2*n-2)
#include"iostream"
#include"cstdio"
#include"cstring"
#include"cstdlib"
#define N 1005
using namespace std;
int data[N];
int number;
int k;
int pos=-1; //最终确定的地址
void print()
{
for(int i=0;i<=pos;i++) printf("%d ",data[i]);
printf("\n");
}
void init_data()
{
scanf("%d%d",&number,&k);
for(int i=0;i<number;i++) scanf("%d",&data[i]);
}
void quickfind(int left,int right,int n)
{
if(left>=right) return ;
else
{
int i=left-1;
int j=left;
int var=data[right];
while(j<right)
{
if(data[j]<var)
{
i++;
int temp=data[i];
data[i]=data[j];
data[j]=temp;
}
j++;
}
int temp=data[i+1];
data[i+1]=data[j];
data[right]=temp;
if(i+1-left+1==n)
{
pos=i+1;
return ;
}
else if(i+1-left+1<n) quickfind(i+2,right,n-(i+1-left+1));
else quickfind(left,i,n);
}
}
4.Dutch national flag problem
荷兰三色旗问题,这种问题是用Partition的典型例子
定义一个数组序列有红白蓝三种颜色,我们要求尽可能快的对数组进行排序,是的相同颜色的元素相邻,并且整体存在红白蓝额顺序
本题是Partition函数的扩展应用,时间复杂度和空间复杂度处理的非常的优秀
算法详见:
Leetcode-75
/*
Partiton函数的作用
简易模拟 Dutch national flag problem */
#include"iostream"
#include"cstdio"
#include"cstring"
#include"cstdlib"
#define N 1005
using namespace std;
//0<1<2为例
int data[N];
int number;
void init_data()
{
scanf("%d",&number);
for(int i=0;i<number;i++) scanf("%d",&data[i]);
}
void print()
{
for(int i=0;i<number;i++) printf("%d ",data[i]);
printf("\n");
}
void swap(int x,int y,int atom)
{
int temp;
if(atom==0)
{
temp=data[x];
data[x]=data[y];
data[y]=temp;
}
else
{
temp=data[x];
data[x]=data[y];
data[y]=temp;
}
}
void sortColors() {
int i=-1;
int j=0;
int k=number;
while(j<k)
{
if(data[j]==0)
{
i++;
int temp=data[i];
data[i]=data[j];
data[j]=temp;
}
else if(data[j]==2)
{
k--;
while(j<k&&data[k]==2) k--;
int temp=data[j];
data[j]=data[k];
data[k]=temp;
if(data[j]==0)
{
i++;
int temp=data[i];
data[i]=data[j];
data[j]=temp;
}
}
j++;
}
}
int main()
{
init_data();
print();
sortColors();
print();
return 0;
}
5总结:
Partition函数真的是非常的强大,我们如果只是了解快排的话真的是没有学透Partition函数的精髓,也没有学到快排的算法的本质