关于Top K算法和寻找第K个最小的数这种经典问题网上已经说的很详细了,不过毕竟不是自己的,这里自己总结一下,而且这两个问题又稍稍有点区别。
1.Top K算法:即寻找一列数中K个最小值或K个最大值,这里仅以寻找K个最小值为例(算法类似)。
(1)普通排序:最直观的算法就是给整列数排序,然后取前K个数。这里主要对各种排序算法的时间复杂度进行分析:
插入排序:由于嵌套循环的每一次迭代都花费N次迭代,因此插入排序的时间复杂度为O(N^2)。
快速排序:对于已经排序的数列采用快速排序时间复杂度为O(N^2),采用随机数或者三数中值法可以避免这种情况,平均时间复杂度T(N)=O(NlogN)。代码如下:
void Swap(int *a, int *b)
{
int Tmp = *a;
*a = *b;
*b = Tmp;
}
void Qsort(int A[], int Left, int Right)
{
int i, j, Pivot;
if(Left < Right)
{
Pivot = A[Left];
i = Left + 1;
j = Right;
while(1)
{
while(A[i] < Pivot)
i++;
while(A[j] > Pivot)
j--;
if(i < j)
Swap(&A[i++], &A[j--]);
else
break;
}
Swap(&A[j], &A[Left]);
Qsort(A, Left, j - 1);
Qsort(A, j + 1, Right);
}
}
void quickSort(int A[], int N)
{
Qsort(A, 0, N - 1);
}
(2)部分排序:如果对最小的K个数顺序没有要求,只要找出最小的K个数即可,则没必要对所有数都进行排序,找出前K个最小的数就可以了。
数组方法:维护一个大小为K的数组,K以数列的前10个数初始化,按照从小到大排列。然后继续从数列中的第11个数开始遍历,若小于数组中最大的数,则舍弃这个最大数,更新当前数组,重新排列。插入新的数要遍历这个数组,因此最坏情况时间复杂度为N*K.
//数组方法实现输出最小的K个数
int* MinK(int *a, int n, int k)
{
int* MinArry = new int[k+1];
for(int i=0; i<k+1; ++i)
MinArry[i] = a[i];
int pos = k + 1;
//K个数插入排序
for(int i=1; i<k; ++i)
{
int Tmp = MinArry[i];
int j;
for(j=i; j>0 && MinArry[j-1]>Tmp; j--)
MinArry[j] = MinArry[j-1];
MinArry[j] = Tmp;
}
for(int pos=k+1; pos<n; ++pos)
{
MinArry[k+1] = a[pos]; //不断读入数列中的数到数组末尾
int InsertNum = MinArry[k+1];
if(InsertNum < MinArry[k])
{
int i;
for(i=k+1; i>0 && MinArry[i-1]>InsertNum; --i)
MinArry[i] = MinArry[i-1];
MinArry[i] = InsertNum; //插入合适的位置
}
}
return MinArry;
}
快速排序算法的变种:如果选定了基准值,一趟循环后基准值的位置为j,j左边的元素都是小于它的,j右边的元素都是大于它的。如果j正好等于k-1,那么数列下标为0到k-1的元素就是最小的k个数,函数返回;如果j小于k-1,那么在j的右边递归的使用快速排序,它会使新的基准值的位置右移,同理j大于k-1时在j的左侧使用快排,让新的基准值位置左移,直到基准值的最终在位置k-1上;
void Swap(int &a, int &b)
{
int c = a;
a = b;
b = c;
}
void Qsort(int *a, int Left, int Right, int k)
{
if(Left < Right)
{
int pivot = a[Left];
int i = Left + 1, j = Right;
for(;;)
{
while(a[i] < pivot)
++i;
while(a[j] > pivot)
--j;
if(i > j)
break;
Swap(a[i++], a[j--]);
}
Swap(a[j], a[Left]);
if(j < k-1)
Qsort(a, j+1, Right, k);
else if(j > k-1)
Qsort(a, Left, j-1, k);
else
return;
}
}
void Select(int *a, int n, int k)
{
Qsort(a, 0, n-1, k);
}
时间复杂度分析:假设每次快排数组长度都是上一次的一半,又T(N)=N+N/2+N/4+N/8+…=2N=O(N)。最坏情况分析:如果数列是已经排序的,每次排序只能排除掉一个数,有T(N)=N+N-1+N-2+N-3+…+0=O(N^2)。
堆排序:通过建立一个K个数的堆的方式,最小的K个数利用最大堆(反之求最大的K个数用到最小堆),不断读入数列与堆顶比较。如果小于堆顶表明堆顶不是最小的K个元素之一了,淘汰堆顶并插入。
void swap(int *v, int i, int j)
{
int tmp = v[i];
v[i] = v[j];
v[j] = tmp;
}
void siftup(int *v, int n)
{
int c;
for(int i = n-1; i>0 && v[i] > v[c=(i-1)/2]; i = c)
swap(v, i, c);
}
void siftdown(int *v, int n)
{
int c;
for(int i = 0; (c = 2*i+1) <= n-1; i = c)
{
if(c+1 <= n-1 && v[c] < v[c+1])
++c;
if(v[i] >= v[c])
break;
swap(v, i, c);
}
}
void TopK(int *a, int n, int k)
{
int *heap = new int[k];
for(int i = 0; i < k; ++i)
heap[i] = a[i]; //堆初始化数据
for(int i = 1; i < k; ++i)
siftup(heap, i+1); //建堆
for(int i = k; i < n; ++i) //与堆顶元素比较,如果小于堆顶则替换堆顶
{
if(a[i] < *heap)
{
*heap = a[i];
siftdown(heap, k);
}
}
for(int i = 0; i < k; ++i)
cout << heap[i] << " ";
cout << endl;
delete[] heap;
}
时间复杂度分析:堆赋值耗时k,建堆时间不超过klogk,遍历数组时间为O((n-k)logk),所以最终时间复杂度为O(nlogk)