位图法与N个数中找最大10个数

前言

今日,听得同学间讨论两个问题,觉得甚是有趣,一个是找到n个数找最大10个数,另一个是位映射的问题。


一、N个数找最大10个数

引入
给定n个数据,比如10万,又或着100万,让你找到最大前10个数,怎么找呢?
我心中不免一惊,真的是很巧,之前我在做数字手写识别时就考虑过这个问题,knn算法有涉及从n个邻居中找到k个最近的邻居,二者问题本质是一样的。
听到有人不假思索,直说:“排序”。我们分析是不必的,我们只需要找到最大的10个,对次序并无要求,排序让整个数据有序是没有必要的。

想法
看过我那篇文章的同志应该知道我是用了一个“最值淘汰”的方法,想法很简单,我们只要不断把10个数中的最小值和剩余数比较,淘汰掉小的数,这个过程可以看作是一个不断提升最小值下限的过程。基本思路是这样,如何实现则又是一个优化过程。
因为剩余所有数我们肯定都要遍历一遍,已经无法进一步优化,那么唯一能优化的只有一个问题了——我们如何找到10个数中的最小值?

  • 数组每次遍历10个数

遍历m个数操作复杂度为O(m)。从N个数中找到最大M个数,则总的复杂度为O(n*m);

  • 10个数用堆存储

建堆复杂度为O(m),我们先将前m个数建堆,因为是不断提高最小值下限,我们要建一个最小堆,每次都是和根节点比较,如果交换了值,就要对堆重新维护一次,因为维护的代价为O(logm),所以总的代价为O(nlogm);
关于堆排序的实现,可以参考我的另一篇博客:基于比较的内排序——温故而知新

最后讨论,还是堆结构比较方便,后来想,对于大数据,数据的重复量也是影响效率的因素,我们对数据的预处理去掉重复数据,也潜在可能提高效率。那么关于大量数据的重复性我们如何检验呢?

顺水推舟,我们又来考虑大量数据怎么检查重复性呢?


二、位映射

也是假设1亿个int数据,在32位机内存上运行,让你过滤掉重复的数。

讨论:
同样,我们否定掉每找一个数就和后面的数进行比较的思路,int型数据可表示的数一共有232=4294967296种,这样最差情况下复杂度达到O(n2),是不理想的。

我提出的是采用Hash表的方法,我们将数值作为key,出现次数作为value,只需要遍历一遍,value不为1就是重复的,后来一想,这样也是不理想的,因为32位机的内存只有4GB,如果重复较少,根本存不下。

经老师提点,采用位图法
我们知道int一共有232种,我们可以申请232bit 的数组空间,这样对于每个int数我们都可以有对应的位置映射,当出现了int最小值,我们就将数组0号位置置1,如果再次出现0,因为已经置1,所以判定为重复。
这个方法,我们存储空间就缩小为 229bit=512MB。
但是考虑到实现部分时,我们知道计算机最小可操作单位是1Byte,位图法则要求到bit精度,所以这又涉及到一个byte和bit之间的转换。

算法实现思路:

  1. 既然最小操作单位是byte,我们分组即可,每8个bit就是一个byte,所以我们申请长度为229的byte数组即可。
  2. 如果没有申请byte数组,我们是从左往右直接数来确定某个bit位,有了byte数组,我们没有本质改变,只要先确定组号,再确定组内偏移一样可以定位。
  3. 怎么才能实现对单个bit位进行赋值修改呢? 又涉及到一次转换,因为操作单位是byte,我们只能通过对这个byte单位进行赋值修改某个bit,通过十进制和二进制的转换我们可以实现,比如对于某个byte,我们要让第三个0置1,我们只要对该byte赋值4即可。

    0000 0100 = 4

  4. 我们怎么判定重复呢?对于某个数必须要知道它的位置是否被置1,一种方法是通过移位单独取出该位,第二种方法则是 按位相与—— &。
    比如某个byte内部:

    0000 1100 = 12

    我们要判断4的重复性
    Method 1:逻辑移位

    对12左移2位后取出剩余首位是否为1
    0000 0011

    Method 2:按位相与

     0000 1100
    & 0000 0100
    = 0000 0100
    可见,只要重复,那么按位相与结果必定不为0;

实现如下:
环境:IDEA
语言:Java

public class Repetetive{
    public static void main(String[] args){
        byte[] res=new byte[1<<29];  //申请byte数组
        //考虑到int存在负数,我们要加上一个bias偏移值,使得最小的负数编号为0
        double bias=2147483648d; 
        //测试数据
        int arr[]={166,233,15555,2,4,23,2,23,5,2,6,-10000,-10000};
        int index;
        int relativePos;
        for(int i=0;i<arr.length;i++){
            //tmp是该数的位置编号
            double tmp=arr[i]+bias;
            //index是组号
            index=(int)(tmp/8);
            //relative是组内偏移
            relativePos=(int)(tmp%8);
            //按位相与判断结果
            if(((1<<relativePos) & (res[index]))!= 0) 
                System.out.println(arr[i]+"重复");
            else res[index]+=1<<relativePos;
        }
    }
}
    输出:
    2重复
    23重复
    2重复
    -10000重复
posted @ 2018-08-20 18:35  顾杰伟  阅读(294)  评论(0编辑  收藏  举报