几个经典的算法

几个经典的算法

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);
            }
         }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值