Top K算法和寻找第K个最小的数
关于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)
- 上一篇: Linux执行PHP脚本(简单实例)
- 下一篇: PHP会员登录,和判断用户权限,登录超时踢除用户