最近关注一下数据算法面试的时候常考的题目,求topK问题。
本文从两种方法实现,并给出实际数据实验结果以及相关的分析。
以下是堆排序
// Heap sort;
void MaxHeapify(int * src, int pos,int count)
{
int l = (pos<<1)+1;
int r = l+1;
int maxpos = pos;
if (l<count && src[l]>src[pos])
{
maxpos = l;
}
if (r<count && src[r]>src[maxpos])
{
maxpos = r;
}
if (maxpos != pos)
{
int temp = src[maxpos];
src[maxpos] = src[pos];
src[pos] = temp;
MaxHeapify(src,maxpos,count);
}
}
void CreateMaxHeap(int * src,int count)
{
for (int t = count>>1;t>=0;--t)
{
MaxHeapify(src,t,count);
}
return;
}
void HeapSort(int * src,int count)
{
CreateMaxHeap(src,count);
while (count--)
{
int t = src[0];
src[0] = src[count];
src[count] = t;
MaxHeapify(src,0,count);
}
}
int _tmain(int argc, _TCHAR* argv[])
{
int intarray[1024] = {0};
for(int i = 0; i< 1024; ++i)
{
intarray[i] = i;
}
HeapSort(intarray,1024);
return 0;
}
只要把上面的HeapSort函数稍改一下,我们就可以得到一个求从小到大排序排在第k位置的数值。
int HeapSortGetMinK(int *src,int begin,int end,int k)
{
CreateMaxHeap(src,k);
int i = k;
while (i<=end)
{
if (src[i]<src[0])
{
int t = src[0];
src[0] = src[i];
src[i] = t;
MaxHeapify(src,0,k);
}
++i;
}
return src[0];
}
其中的参数名字都很好理解。从时间复杂度上算,建一个k大小的堆,o(k);小于n-k次堆的调整,o((n-k)lgk)近似为nlgk/2。整体时间复杂度数量级为nlgn;
另一种方法是类似快速排序的分法,每次使用partition将数值分为两边,根据k值与当前分值位置的对比,决定下一个partition区间;也就是说比快排少了一边递归;以下是算法的实现:
int partition(int *A,int begin,int end)
{
int i = begin;
int j = end;
int mid = A[begin];
while (i<j)
{
while (A[j]>=mid && i<j)
{
--j;
}
if (i<j)
{
A[i] = A[j];
}
while (A[i]<=mid && i<j)
{
++i;
}
if (i<j)
{
A[j] = A[i];
}
}
A[j] = mid;
return j;
}
// 求一个长为m的数组中第k(k<m)大的数值
// 输入: 数组指针A,数组长度len,k值
// 返回: 第k+1小的数
int PartitionSortGetMinK(int *A,int begin,int end,int k)
{
int mid = partition(A,begin,end);
if ( k == mid )
{
return A[mid];
}
else if (k<mid)
{
return PartitionSortGetMinK(A,begin,mid-1,k);
}
return PartitionSortGetMinK(A,mid+1,end,k);
}
注意这个partition比算法导论上面那个标准算法要来的快,来的巧妙。从时间复杂度上算,partition算法只需要一次遍历,就能把范围内的数值分两边,复杂度为lgn;而在递归的时候,每次partition的范围从平均上说都减半,算法导论上证明了以上方法的时间复杂度在平均情况下是o(n)。
那么,从实际数据检验的结果来看会怎么样呢?我们以下就给实验过程:
int getmilliseconds(SYSTEMTIME begin,SYSTEMTIME end)
{
// 请勿在23:59到00:01之间做此计算
int hour = end.wHour -begin.wHour;
int min = end.wMinute - begin.wMinute;
int seconds = end.wSecond - begin.wSecond;
int millisecond = end.wMilliseconds - begin.wMilliseconds;
return ((hour*60+min)*60+seconds)*1000+millisecond;
}
//产生数组的方法,可以使用多种不同的算法产生数组
void GetArray(int *intarray,int count,int seed)
{
// 以种子产生的随机数
srand(seed);
for(int i = 0; i< count; ++i)
{
intarray[i] = rand();
}
}
void test(int *intarray,int len,int k,int seed,int &heapsortresult,int &partitionresult)
{
int kvalue = 0;
int inter = 0;
GetArray(intarray,len,seed);
SYSTEMTIME begin1,begin2,end1,end2;
GetSystemTime(&begin1);
kvalue = HeapSortGetMinK(intarray,0,len-1,k);
GetSystemTime(&end1);
inter = getmilliseconds(begin1,end1);
heapsortresult += inter;
//printf("\t %d \t",inter);
GetArray(intarray,len,seed);
GetSystemTime(&begin2);
kvalue = PartitionSortGetMinK(intarray,0,len-1,k-1);
GetSystemTime(&end2);
inter = getmilliseconds(begin2,end2);
partitionresult +=inter;
//printf("%d\n",inter);
}
int _tmain(int argc, _TCHAR* argv[])
{
int maxlen = 1<<24;
int *intarray = (int*)malloc(maxlen*sizeof(int));
// 整体数组最长值,每次增长四倍
for (int len = 1<<2;len<=maxlen;len<<=2)
{
printf("++++++length %d \n",len);
// k值每次增长2倍
for (int k = 1;k<len;k<<=1)
{
printf("\tk %15d",k);
int partitionresult=0;
int heapsortresult = 0;
// 四个种子数方法,计算时间
for (int i = 1;i<5;i++)
{
int seed = i;
//printf("\t\tseed %d ",i);
test(intarray,len,k,seed,heapsortresult,partitionresult);
}
// 对四次堆方法的时间和取平均值
heapsortresult>>=2;
// 对四次划分方法的时间和取平均值
partitionresult>>=2;
printf("\t%d\t%d\n",heapsortresult,partitionresult);
}
printf("\n");
}
free(intarray);
return 0;
}
实验结果:
++++++length 4
k 1 0 0
k 2 0 0
++++++length 16
k 1 0 0
k 2 0 0
k 4 0 0
k 8 0 0
++++++length 64
k 1 0 0
k 2 0 0
k 4 0 0
k 8 0 0
k 16 0 0
k 32 0 0
++++++length 256
k 1 0 0
k 2 0 0
k 4 0 0
k 8 0 0
k 16 0 0
k 32 0 0
k 64 0 0
k 128 0 0
++++++length 1024
k 1 0 0
k 2 0 0
k 4 0 0
k 8 0 0
k 16 0 0
k 32 0 0
k 64 0 0
k 128 0 0
k 256 0 0
k 512 0 0
++++++length 4096
k 1 0 0
k 2 0 0
k 4 0 0
k 8 0 0
k 16 0 0
k 32 0 0
k 64 0 0
k 128 0 0
k 256 0 0
k 512 0 0
k 1024 0 0
k 2048 0 0
++++++length 16384
k 1 0 0
k 2 0 0
k 4 0 0
k 8 0 0
k 16 0 0
k 32 0 0
k 64 0 0
k 128 0 0
k 256 0 0
k 512 0 1
k 1024 0 0
k 2048 0 0
k 4096 0 0
k 8192 0 0
++++++length 65536
k 1 0 0
k 2 0 0
k 4 0 0
k 8 0 0
k 16 0 0
k 32 0 0
k 64 0 0
k 128 0 0
k 256 0 0
k 512 0 0
k 1024 0 0
k 2048 1 1
k 4096 1 0
k 8192 2 1
k 16384 2 1
k 32768 2 0
++++++length 262144
k 1 0 0
k 2 0 0
k 4 0 0
k 8 0 0
k 16 0 0
k 32 0 0
k 64 0 0
k 128 0 0
k 256 0 0
k 512 0 2
k 1024 0 1
k 2048 1 2
k 4096 1 2
k 8192 3 2
k 16384 4 2
k 32768 8 2
k 65536 10 3
k 131072 11 3
++++++length 1048576
k 1 1 0
k 2 1 1
k 4 0 0
k 8 0 0
k 16 1 0
k 32 1 0
k 64 0 0
k 128 0 1
k 256 0 0
k 512 1 1
k 1024 1 0
k 2048 2 7
k 4096 2 7
k 8192 4 7
k 16384 7 7
k 32768 13 8
k 65536 22 7
k 131072 34 8
k 262144 49 10
k 524288 55 12
++++++length 4194304
k 1 4 4
k 2 4 4
k 4 4 4
k 8 4 4
k 16 4 3
k 32 4 4
k 64 4 3
k 128 4 3
k 256 4 4
k 512 4 4
k 1024 4 4
k 2048 5 4
k 4096 6 4
k 8192 9 33
k 16384 12 33
k 32768 21 35
k 65536 35 36
k 131072 59 36
k 262144 99 35
k 524288 160 38
k 1048576 278 46
k 2097152 378 51
++++++length 16777216
k 1 15 15
k 2 15 15
k 4 15 15
k 8 15 15
k 16 15 15
k 32 15 15
k 64 15 15
k 128 15 15
k 256 15 15
k 512 16 15
k 1024 16 16
k 2048 16 15
k 4096 18 16
k 8192 20 15
k 16384 26 15
k 32768 36 141
k 65536 55 142
k 131072 92 142
k 262144 155 144
k 524288 270 145
k 1048576 528 177
k 2097152 1102 176
k 4194304 1877 171
k 8388608 2274 187
实验中设计,length值从4开始,以4为步长,一直增长到1<<24;对应 的K值从1开始,以2为步长,一直增长至length的一半值;所使用的数据分别用1,2,3,4 为种子数,生产伪随机序列,四次计算时间后得出当前方法计算的平均时间作为打印结果。
结果中,其中length是数组的总长度,k后面的第一个数字为k值,第三列为使用【最大堆方法】求出第k小数所需要的时间(单位是ms),第四列为【划分法】求第k小数所需要的时间,单位为ms。
可以看出,大部分情况下,划分法的速度是占住优势的。但有一个疑问,在以固定的N值,以最大推方法求的过程中,时间随k值稳步上升;而以划分法时间随k增大在某一个时间点增加很快,其他时间点基本稳定。
我怀疑是产生随机数的算法问题,造成数据有一定的规律,并且影响到划分排序的进行。