寻找最小的k个数
问题描述:给定n个整数,求出其中最小的k个数。
分析:这是一个经典的问题了,存在多种解法,各个解法的效率不一样,这里我列举四种常见的解法。
解法一:可以将所有的数进行排序,然后直接输出前k个数即可。排序算法有很多,读者可以自己选择,如快速排序。
解法二:利用容器(可以是数组,集合等)实现,
我们可以先遍历k个数存入到大小为k的数组中,然后假设这k个数就是最小的k个数;
对这k个数,利用选择或交换排序找到这k个元素中的最大值kmax(找最大值需要遍历这k个数,时间复杂度为O(k)
);
继续遍历剩余n-k个数。假设每一次遍历到的新的元素的值为x,把x与kmax比较: 如果x<kmax ,用x替换kmax,并回到上一步重新找出k个元素的数组中最大元素kmax;如果x>kmax,则继续遍历不更新数组。
每次遍历,更新或不更新数组的所用的时间为O(k)或O(0)。故整趟下来,时间复杂度为n*O(k)=O(nk)。
解法三:利用堆结构实现:其实这个方法与解法二相似,只不过这里的容器采用堆这个数据结构,之所以用堆是因为建k个节点的初始堆的时间复杂度为O(k),每维护一次的代价为O(logk),这一步比数组的时间复杂度低。
至于后面的都一样,因此这种方法的总时间复杂度为O(nlogk)。
这里我给出详细的Java代码,写法比较通用,读者可以很容易的转换为其他语言实现(代码中下标从1开始存数,下标0忽略)。
1 import java.util.Scanner; 2 public class Qinkxiao { 3 public static void heapadjust(int H[],int s,int m){ //调整堆 4 int j,k; 5 int rc=H[s]; 6 for( j=2*s;j<=m;j*=2) 7 { 8 if(j<m && H[j]<H[j+1])++j; 9 if(rc>=H[j])break; 10 H[s]=H[j]; 11 s=j; 12 } 13 14 H[s]=rc; 15 16 } 17 public static void createdui(int array[],int k){ //建初始堆 18 for(int i=k/2;i>=1;--i) 19 heapadjust(array,i,k); 20 } 21 public static void main(String[] args) { 22 int array[]={4,5,9,2,1,5,3,-5,20}; 23 int n=array.length-1; 24 int k=3; 25 System.out.print("n个整数为:"); 26 for(int i=1;i<=n;i++) 27 { 28 System.out.print(array[i]+","); 29 } 30 System.out.println(); 31 createdui(array,k); //建k个节点的初始堆 32 for(int i=k+1;i<=n;i++) //依次遍历后面的n-k个数 33 if(array[i]<array[1]) 34 { 35 array[1]=array[i]; 36 heapadjust(array,1,k); 37 } 38 System.out.print("前"+k+"小个整数为:"); 39 for(int i=1;i<=k;i++) 40 System.out.print(array[i]+","); 41 } 42 43 }
输出结果为:
n个整数为:5,9,2,1,5,3,-5,20,
前3小个整数为:2,1,-5,
解法四:借助快速排序的思想:利用快速排序中的分割函数。可以证明这种方法的时间度为O(n).
具体的Java代码如下:
1 import java.util.Scanner; 2 public class Qinkxiao { 3 4 public static int partition(int array[],int low,int high){ //快速排序中的划分方法 5 int q=array[low]; 6 int m; 7 while(low<high){ 8 while(low<high && array[high]>q)high--; 9 m=array[low];array[low]=array[high];array[high]=m; 10 while(low<high && array[low]<q)low++; 11 m=array[low];array[low]=array[high];array[high]=m; 12 } 13 return low; 14 } 15 16 public static void main(String[] args) { 17 int array[]={4,5,9,2,1,5,3,-5,20}; 18 int n=array.length; 19 int k=3; 20 System.out.print("n个整数为:"); 21 for(int i=0;i<n;i++) 22 { 23 System.out.print(array[i]+","); 24 } 25 System.out.println(); 26 27 int start=0,end=n-1; 28 int index=partition(array,start,end); 29 while(index!=k-1) //判断索引位置石佛符合要求 30 { 31 if(index>k-1 ) 32 { end=index-1; 33 index=partition(array,start,end); 34 } 35 else 36 { start=index+1; 37 index=partition(array,start,end); 38 } 39 } 40 41 System.out.print("前"+k+"小个整数为:"); 42 for(int i=0;i<k;i++) 43 System.out.print(array[i]+","); 44 } 45 46 }
输出结果为:
n个整数为:4,5,9,2,1,5,3,-5,20,
前3小个整数为:-5,1,2,
上述四种解法比较:
解法一:思想最简单,就是将数据排序,直接输出前k个数,编程简单,但是时间复杂度较高,适合数据量少的场合。
解法二:编程稍微复杂,时间复杂度略低,可以用于海量数据处理。
解法三:比解法二好一点,时间复杂度又降低了,并且不需要交换原来数组中的顺序,可以用于n很大k很小的场合,尤其是海量数据处理。
解法四:时间复杂度最低,线性时间即可解决,但是编程比较复杂,而且需要交换原来数组中数据的顺序,破坏了原来数组的顺序。
这四种解法,要选择适当的场合使用。解法三和解法四大家要牢牢掌握,可以用于其他类似的题目。
除了上述集中解法,还有其他的解法,比如利用哈希表,红黑树等其他数据结构,这里不再讲述。