位图法与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型数据可表示的数一共有=4294967296种,这样最差情况下复杂度达到O(),是不理想的。
我提出的是采用Hash表的方法,我们将数值作为key,出现次数作为value,只需要遍历一遍,value不为1就是重复的,后来一想,这样也是不理想的,因为32位机的内存只有4GB,如果重复较少,根本存不下。
经老师提点,采用位图法:
我们知道int一共有种,我们可以申请bit 的数组空间,这样对于每个int数我们都可以有对应的位置映射,当出现了int最小值,我们就将数组0号位置置1,如果再次出现0,因为已经置1,所以判定为重复。
这个方法,我们存储空间就缩小为 bit=512MB。
但是考虑到实现部分时,我们知道计算机最小可操作单位是1Byte,位图法则要求到bit精度,所以这又涉及到一个byte和bit之间的转换。
算法实现思路:
- 既然最小操作单位是byte,我们分组即可,每8个bit就是一个byte,所以我们申请长度为的byte数组即可。
- 如果没有申请byte数组,我们是从左往右直接数来确定某个bit位,有了byte数组,我们没有本质改变,只要先确定组号,再确定组内偏移一样可以定位。
怎么才能实现对单个bit位进行赋值修改呢? 又涉及到一次转换,因为操作单位是byte,我们只能通过对这个byte单位进行赋值修改某个bit,通过十进制和二进制的转换我们可以实现,比如对于某个byte,我们要让第三个0置1,我们只要对该byte赋值4即可。
0000 0100 = 4
我们怎么判定重复呢?对于某个数必须要知道它的位置是否被置1,一种方法是通过移位单独取出该位,第二种方法则是 按位相与—— &。
比如某个byte内部:0000 1100 = 12
我们要判断4的重复性
Method 1:逻辑移位对12左移2位后取出剩余首位是否为1
0000 0011Method 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重复