位运算(Java实现BitMap)

前言:这是第三次写位运算的文章的。昨天线上笔试,对方公司发了一个world文档,题目是这样:

已有一个存储了百万条座机号码的ArrayList,要求写一个方法,返回有序和去重的集合。要求至少两种实现,性能优先。

我想着,你为什么不直接用TreeSet?对于“0133-3023978”,“9764-8977878”这样的字符串,直接用TreeSet还不用自己额外实现sort方法。虽然我不能理解他为什么要用ArrayList,不过我也搞不懂他要求的性能要先到底要怎么优。

我当时计算了下,如果用long(注:将座机号码的连接符“-”删除,就是一个11位的long类型整数),那么最多1千万的数据量,也就80M内存不到(8 * 1000 * 10000/1024/1024M)。

第一种方式:我直接用的TreeSet的构造方法,把ArrayList传递进去。

第二种方式:之前有了解过Redis的BitMap类型,想用该方式实现。但是昨天就不在状态。出去吃了个晚饭,回来才发现这个笔试要求我1个小时内将world文档重新发回对方邮箱。(⊙o⊙)…

开始正文:

BitMap:定义一个足够长的数组,数组的下标代表具体的某个数据,该下标所存储的值用1和0表示是否存在,一个1或0占用1bit,即使从0到1亿,也就是定义new bit[1亿],占用12M内存。

最开始的时候,我脑袋有点昏,想着一千万个最大一千亿的数字(注:百万级,电话号码11位)用BitMap应该怎么设计和存储,以及Java没有bit这个类,如何使用其他byte或long来代替bit。

当时我犯蠢的时候是这样想的(主要是有点饿得厉害,头已经开始晕了。)

定义一千亿个长度的bit数组,当时我想着,怎么不对劲,怎么需要12G内存,大那么多。而且一千万的数量,我定义一千亿长度,结果只用了一千万个下标,就用了万分之一啊,绝对有什么问题。

实际上是我没能完全理解BitMap的问题

网络上很多博客都直接是这样描述的:对10亿个数字去重和排序

他们只提到了数据量,却没有说这10亿个数字都是什么数字,他们直接简单的把10亿个数字理解成0-10亿。

如果你没有正确理解这点,想当然的顺着他们的思路直接用BitMap存储一千亿个索引,那么当然内存会溢出。

实际上,可以以10亿为标准进行判断,按照BitMap,10亿最大数值,需要用10亿的length的bit数组,也就是120M内存占用。

使用Java来实现BitMap:

1M=1024KB;1KB=1024Byte; 1Byte=8bit

一个int占4Byte,long占8byte。基本类型的所占存储空间的大小并不随机器硬件架构的变化而变化,boolean类型所占存储空间的大小没有明确指定,仅定义为能够取字面值true或false。

题目:有随机生成10亿的0-10亿大小的正整数,请使用Java实现BitMap进行去重和排序

@Slf4j
public class BitMap {

    final List<Integer> randomNumberList;

    byte[] bitMap;

    static char[] charNegative128 = new char[]{'1', '0', '0', '0', '0', '0', '0', '0'};

    public BitMap(List<Integer> randomNumberList) {
        this.randomNumberList = randomNumberList;
        bitMap = new byte[10000 * 10000 * 10 / Byte.SIZE];
    }


    /**
     * Integer.MAX_VALUE是2147483647,21亿
     */
    public void addAll() {
        /*
         由于是10亿个数字,可以优化下
         int index = number / 8;
         int bitOffset = number % 8;
         */
        for (Integer number : randomNumberList) {
            int index = number >> 3;
            int bitOffset = number & 0x7;
            // 这里比较特殊,余0是00000001,余1是00000010,因为一共是8位,但是余数只有7,为了统一减少运算步骤,将余0划分给一个1
            bitMap[index] |= 1 << bitOffset;
            log.debug("number: " + number + "index: " + index + "bitOffset: " + bitOffset + "bitMap[i]" + bitMap[index]);
        }
    }

    public List<Integer> getAll() {
        addAll();
        List<Integer> numberList = new ArrayList<>();
        for (int i = 0; i < bitMap.length; i++) {
            if (bitMap[i] != 0) {
                char[] bitOffsets = bitMap[i] == - 128 ? charNegative128 : Integer.toBinaryString(bitMap[i]).toCharArray();;
                log.debug(Arrays.toString(bitOffsets));
                for (int j = 0; j < bitOffsets.length; j++) {
                    if (bitOffsets[j] == '1') {
                        log.debug("index: " + i + " bitOffset: " + (bitOffsets.length - j));
                        numberList.add(i << 3 | (bitOffsets.length - j - 1));
                    }
                }
            }
        }
        return numberList;
    }

    public static void main(String[] args) {
//        BitMap bitMap = new BitMap(Arrays.asList(359395975));
//        List<Integer> all = bitMap.getAll();
//        all.forEach(System.out::println);
        // 特殊:byte的1<<7是负数,因为符号位是1了,使用Integer.toBinaryString()时会返回32位
        byte testByte = (byte) (1 << 7);
        System.out.println(testByte);
        System.out.println(1 << 7);
//        Byte.MIN_VALUE
        List<Integer> randomNumberList = new ArrayList<>();
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            randomNumberList.add(random.nextInt(10000 * 10000 * 10));
        }
        Collections.sort(randomNumberList);
        BitMap bitMap = new BitMap(randomNumberList);
        List<Integer> allNumberList = bitMap.getAll();
        for (int i = 0; i < randomNumberList.size(); i++) {
            System.out.println(randomNumberList.get(i) + "\t" + allNumberList.get(i));
        }
    }
}

下面讲下移位运算,也是本文主要的内容。因为我老是对三个移位运算有点乱。

计算机中存储的数字的二进制都是以补码的形式存储的,位运算的时候也是使用的补码。而读取显示成我们常用的基本类型则是又转换为源码

正数:原码的最高位,即符号位为0,原码=反码=补码

负数:原码的最高位,即符号位为1。

负数的原码可以看做在正数的原码基础上,将符号位由0改为1;

负数的反码:负数的原码,除符号位,其他位取反。

负数的补码:在负数的反码的最低位+1。

左移只有一种:<<。因为左移之后,一律在低位补0,和符号无关(注:低位是右边,高位是左边。和普通的数字一样理解高低位即可)。

右移有两种:>>和>>>。>>是有符号右移,>>>是无符号右移。

有符号右移>>,在高位根据符号位补入和符号位标识,即负数>>,则高位补入1;正数>>,则高位补入0;

无符号右移>>>,使用“零拓展”,在高位补入0。

移位运算分成正负数来分别记录比较好理解(以下,假定移位运算结束后,结果没有溢出)。

有正整数x,移位n,那么x<<n = x * 2^n;x>>n = x >>> n = x / 2^n

有负整数x,移位n,x<<n没有什么规律,得根据实际数字进行判断;x>>n=x/2^n;x>>>n变为正数,结果值根据实际情况判断。

位运算的>>相当于做除法,他的除法和java的直接使用/稍微有些不一致。Java的/是直接截断小数部分,位运算的>>是将低位的1抹去了,这在负数时两种情况是不一致的,稍微有点差别。

number >>1 Java(/2)
3 1 1
-3 -2 -1

其他位运算:

1.与(&):11得1,10得0,00得0.

2.或(|):11得1,10得1,00得0.

3.异或(^):11得0,10得1,00得0(异或、异或、不一样了才或,一样就直接为0,相同为0,不同为1).

4.非(~):这是一元运算符,上面3个是2元运算符,0得1,1得0(取反)。

 

为什么计算机中符号位用0代表正数,用1代表负数?

按照正常理解来说,1代表存在,应该是表示正数才对,相应的0才应该用来代表负数。

一个结论如果想要推导他是正确的,那么可以找到很多个理由。你自己找一个能让自己同意的说法就好。

 

为什么要使用补码来进行计算?

事物的发展规律都是:先有需求然后创造,再根据实际情况想办法优化。

而补码就肯定是一种优化手段,那它解决和优化了什么呢?

废话:根据只有负数的反码和补码和源码不一致,而正数则没有变化这一情况来看,并且运算都是使用补码计算,而补码则是在反码基础上+1,那么这个1就有大用,并且是先出现的反码再出现补码,那么反码应该解决了一部分问题但是没有完全解决,导致计算的时候需要用1来进行补足。

结果:在规定了符号位0代表正数,1代表负数的情况下:使用源码进行正负数相加,由于符号位参与运算,结果肯定不正确;然后探索出给负数进行补码,这样子解决了小数-大数,但是大数-小数结果始终相差1;最后探索出负数使用补码。(注:减去一个数相当于加上这个数的负数)

 

版权声明:本文为我欲皆真原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/woyujiezhen/p/16435909.html