几个经典的算法
10亿个数字里里面找最小的10个
首先我们得先了解什么是**TOP K问题**
Top K指的是从n(很大)个数据中,选取最大(小)的k个数据。例如学校要从全校学生中找到成绩最高的500名学生,再例如某搜索引擎要统计每天的100条搜索次数最多的关键词
对于这种问题,效率比较高的解决方法是使用最小堆
- 先去源数据中的K个元素放到一个长度为K的数组中去,再把数组转换成最小堆。再依次取源数据中的K个之后的数据和堆的根节点(数组的第一个元素)比较,根据最小堆的性质,根节点一定是堆中最小的元素,如果小于它,则直接pass,大于的话,就替换掉跟元素,并对根元素进行Heapify,直到源数据遍历结束
其他方法:
- 对源数据中所有数据进行排序,取出前K个数据,就是TopK
(缺点:当数据量很大时,只需要k个最大的数,整体排序很耗时,效率不高) - 维护一个K长度的数组a[],先读取源数据中的前K个放入数组,对该数组进行升序排序,再依次读取源数据第K个以后的数据,和数组中最小的元素(a[0])比较,如果小于a[0]直接pass,大于的话,就丢弃最小的元素a[0],利用二分法找到其位置,然后该位置前的数组元素整体向前移位,直到源数据读取结束
(缺点:比方法一效率会有很大的提高,但是当K的值较大的时候,长度为K的数据整体移位,也是非常耗时的)
有1亿个数字,其中有2个是重复的,快速找到它,时间和空间要最优
通过计数排序联想到:
把数字值直接映射到数组下标(时间最优),这里重复的数字只有两次,为了空间最优,就用bit来表示(只有0和1),1byte=8bit,一个byte可以存储8个数字的计数。所以建立数组 byte[] bucket=new byte[(最大值-最小值)/8+1];
public class Test{
public static void main(String[] args){
long time=new Date().getTime();
int[] arr=new int[100000000];//1亿长度
for(int i=0;i<arr.length;i++){
arr[i]=i+1;
}
arr[99999999]=2020;
int min=arr[0];
int max=arr[0];
for(int i=0;i<arr.length;i++){
if(arr[i]<min)
min=arr[i];
if(arr[i]>max)
max=arr[i];
}
byte[] bucket=new byte[(max-min)/8+1];
for(int i=0;i<arr.length;i++){
int num=arr[i];
int j=(num-min)/8;
int k=(num-min)%8;
if(((bucket[j]>>k)&1)>0){//重复了
System.out.println("Number of repeats:"+num);
break;
}else{
bucket[j]|=(1<<k);
}
}
long time2=new Date().getTime();
System.out.println("millisecond:"+(time2-time));
}
}
2亿个随机生成的无序整数,找出中间大小的值
2亿个随机整数还可以分为两种情况,一种是2亿个不重复的随机整数,另一种是2亿个有重复项的随机整数
2亿个(有重复项的)随机整数
思想:二分查找
- 可以在最开始的时候,以0为分界线,对所有整数进行遍历并统计小于0的整数个数和大于等于0的整数个数,假设小于0的整数有94,632,563个,那么大于等于0的数就有105,367,437个,这也就意味着,我们需要的那两个数是大于等于0的
- 可以向第一次遍历那样对数据进行拆分了,我们知道int类型正整数最大值是231,那么第二次遍历我们就以230作为分界线吧,统计小于230的数与大于等于230的数,假设小于230的数有36,524,163个,大于230次方的数有68,843,274个,那么我们需要的那两个数处于0到2^30-1这个闭区间内
- 按照这个方法,一次又一次第进行遍历,当我们统计出我们需要的那两个数处于某一个区间内,并且这个区间内的数比较少,至少能让我们直接在内存中进行排序时,我们就可以将符合这个区间的数全部读取到内存中排序
2亿个(不重复的)随机整数
思想:位图排序(是一种空间换时间的排序算法,时间复杂度仅为O(n),但它的限制很多,比如数据不能有重复项,在排序之前必须知道数据的范围(最小值及最大值,或者大致范围),范围越宽广,占用的内存空间就越大)
例子:有[19, 36, 3, 42, 11, 26, 5, 9, 24]这样一个数组,假设我们已经知道这个数组最小值为3,最大值为42,这时候我们就可以申请一个长度为40位的内存空间,如下:
00000 00000 00000 00000 00000 00000 00000 00000
(为了看起来方便,这里以5位相隔一个空格来表示)
对该数组进行遍历,并且将每个整数的值减去3之后,将对应位置设为1,结果如下:
10100 01010 00000 01000 01010 00000 00010 00001
从这串0和1中,我们能看到这里的第0位、第2位、第6位都为1,这几个1的位置加上数组中的最小值3则表示的是3, 5, 9这几个数。
说到这应该就能明白了吧,在排序完成后,只需要遍历一遍这个内存空间中的每一位就能输出排序后的数组。
在数据不重复的情况下,我们可以使用位图排序来对题目中的2亿个数据进行排序,随后遍历内存空间,遍历到第一亿个1的时候,这个1及下一个1所在的位置的平均值则为中间值
给一个不知道长度的(可能很大)输入字符串,设计一种方案,将重复的字符排重(原地)
问清楚:不能使用额外的一份数组拷贝是指根本就不允许开一个数组,还是说可以开一个固定大小, 与问题规模(即字符串长度)无关的数组
**方法一:**如果根本就不允许你再开一个数组,只能用额外的一到两个变量。那么,最先想到的方法就是暴力求解法了。
你可以依次访问这个数组的每个元素,每访问一个,就将该元素与前面的元素进行比较,如果相同就去掉,如果不相同就添加到前面序列中。时间复杂度为O(n^2)
**方法二:**如果根本就不允许你再开一个数组,只能用额外的一到两个变量。第二种方法就是先排序,再去重。
排序之后重复元素必定是相邻的,这样去重就简单多了。
排序时间复杂度最快为快速排序为O(nlogn),去重时间复杂度为O(n),最终为O(nlogn)
方法三: 如果可以开一个固定大小,与问题规模(即字符串长度)无关的数组,那么可以用一个数组来 表征每个字符的出现(假设是ASCII字符,则数组大小为256),这样的话只需要遍历一遍字符 串即可,时间复杂度O(n)
如果字符集更小一些,比如只是a-z,即字符串里只包含小写字母,那么使用一个int变量中 的每一位来表征每个字符的出现,用位运算来实现。也可以在O(n)的时间里移除重复字符,而且还不需要额 外开一个数组
有3n+1个数字,其中3n个中是重复的,只有1个是不重复的,怎么找出来
public class Test{
public static void main(String[] args){
int[] arr=new int[200000000];
for(int i=0;i<arr.length;i++){
arr[i]=i/2;
}
arr[199999998]=1;
arr[199999999]=100005099; //不重复的数
int min=arr[0];
int max=arr[0];
for(int i=0;i<arr.length;i++){
if(arr[i]<min)
min=arr[i];
if(arr[i]>max)
max=arr[i];
}
byte[] buckets=new byte[(max-min)/4+1];
for(int i=0;i<arr.length;i++){
int num=arr[i];
int j=(num-min)/4;
int k=(num-min)%4;
if(((buckets[j]>>(k*2))&3)<3)
buckets[j]+=(1<<(k*2));
}
for(int i=0;i<buckets.length;i++){
byte b=buckets[i];
int num=i*4+min;
if((b&3)==1) {
num += 0;
System.out.println("num:"+num);
}
if(((b>>2)&3)==1) {
num += 1;
System.out.println("num:"+num);
}
if(((b>>4)&3)==1) {
num += 2;
System.out.println("num:"+num);
}
if(((b>>6)&3)==1) {
num += 3;
System.out.println("num:"+num);
}
}
}
}