Bit(位操作)

1.1一个整型数组里除了一个数字之外,其他的数字都出现了两次。请写程序找出这一个只出现一次的数字。

思路:所有数字异或,最后结果就是只出现一次的数。

    public int singleNumber(int[] nums) {
        int length = nums.length;
        if(length == 1){
            return nums[0];
        }
        int bitResult = 0;
        for(int i = 0; i < length; ++i){
            bitResult ^= nums[i];
        }
        return bitResult;
    }
View Code

1.2一个整型数组里除了一个数字之外,其他的数字都出现了三次。请写程序找出这一个只出现一次的数字。

1.3 一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

思路:位运算中异或的性质:两个相同数字异或=0一个数和0异或还是它本身

只有一个数出现一次时,我们把数组中所有的数,依次异或运算,最后剩下的就是落单的数,因为成对儿出现的都抵消了。
依照这个思路,我们来看两个数(我们假设是AB)出现一次的数组。我们首先还是先异或,剩下的数字肯定是A、B异或的结果,这个结果的二进制中的1,表现的是A和B的不同的位。我们就取第一个1所在的位数,假设是第3位,接着把原数组分成两组,分组标准是第3位是否为1。如此,相同的数肯定在一个组,因为相同数字所有位都相同,而不同的数,肯定不在一组。然后把这两个组按照最开始的思路,依次异或,剩余的两个结果就是这两个只出现一次的数字
//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        int length = array.length;
        if(length == 2){
            num1[0] = array[0];
            num2[0] = array[1];
            return;
        }
        int bitResult = 0;
        for(int i = 0; i < length; ++i){
            bitResult ^= array[i];
        }
        //此时bitResult是两个只出现一次的数字相异或的结果
        int index = findFirst1(bitResult);//z知道第一个1出现的位置
        for(int i = 0; i < length; ++i){
            if(isBit1(array[i], index)){
                num1[0] ^= array[i];
            }else{
                num2[0] ^= array[i];
            }
        }
    }
    private int findFirst1(int bitResult){
        int index = 0;
        while(((bitResult & 1) == 0) && index < 32){
            bitResult >>= 1;
            index++;
        }
        return index;
    }
    private boolean isBit1(int target, int index){
        return ((target >> index) & 1) == 1;
    }
}
View Code

另一种解法:

public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        ArrayList<Integer> list = new ArrayList<Integer>();
        for (int i = 0; i < array.length; i++)
        {
            if (!list.contains(array[i])) {
                list.add(array[i]);
            } else {
                list.remove(new Integer(array[i]));
            }
        }
        if (list.size()>1){
            num1[0] = list.get(0);
            num2[0] = list.get(1);
        }
    }
}
View Code

1.给定两个32位的整数M与N,以及表示比特位置的i与j。编写一个方法,将M插入N,使得M从N的第j位开始,到第i位结束。假定从j位到i位足以容纳M,也即若M=10011,那么j和i之间至少可容纳5个位。例如,不可能出现j=3和i=2的情况,因为第3位和第2位之间放不下M。

示例输入:N=10000000000,M=10011,i=2,j=6,输出:N=10001001100

思路:问题的解决分为三个步骤:(1)将N中从j到i之间的位清零:利用掩码来清零,除j到i之间的位为0外,这个掩码的其余位均为1。先创建掩码的左半部分,然后是右半部分,最终得到整个掩码。(2)对M执行移位操作,与j和i之间的位对齐(3)合并M与N

public class bitInsert {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        System.out.println(updateBits(4, 1, 1, 1));
    }
    
    public static int updateBits(int n, int m, int i, int j) {
        //创建掩码,用来清除n中i到j的位  例:i=2, j=4  掩码应该为11100011
        int allones = ~0;//等同于一连串的1
        //在位置j之前的位均为1,其余为0 11111111左移5位 left=11100000
        int left = allones << (j + 1);
        //在位置i之后的位均为1 right=00000100-00000001=00000011
        int right = ((1 << i) - 1);
        //除i到j的位为0,其余位均为1 mask= 11100011
        int mask = left | right;
        //清除位置j到i的位,然后将m放进去
        int n_cleared = n & mask;//清除j到i的位
        int m_shifted = m << i;//将m移至相应的位置
        return n_cleared | m_shifted;//对两者执行位或操作
    }

}
View Code

2.给定一个介于0和1之间的实数(如0.72),类型为double,打印它的二进制表示。如果该数字无法准确地用32位以内的二进制表示,则打印“ERROR”。

思路:为了打印小数部分,可以将这个数乘以2, 检查2n是否大于或等于1。实质上等同于“移动”小数部分,也即:r=2(10进制)*n。若r>=1,可知n的小数点后面正好有一个1。不断重复上述步骤,可以检查每个数位。

    public static String printBinary(double num) {
        //确保是介于0和1之间的实数
        if (num >= 1 || num <= 0) {
            return "ERROR";
        }
        StringBuilder binary = new StringBuilder();
        binary.append(".");
        while (num > 0) {
            //设定长度上限:32个字符
            if (binary.length() >= 32) {
                return "ERROR";
            }
            
            double r = num * 2;
            if (r >= 1) {
                binary.append("1");
                num = r - 1;
            } else {
                binary.append("0");
                num = r;
            }
        }
        return binary.toString();
    }
View Code

3.给定一个正整数,找出与其二进制表示中1的个数相同、且大小最接近的那两个数 (一个略大,一个略小)。

解法一:蛮力法:在n的二进制表示中,数出1的个数,然后增加或减小,直至找到1的个数相同的数字。

    public static int[] getCloseNumber(int x) {
         int[] res = new int[2];//保存结果的数组
         int i = 1;
         int num = oneNumber(x);
         while (res[0] == 0 || res[1] == 0){
             if (oneNumber(x - i) == num && res[0] == 0) {
                 res[0] = x - i;
             }
             if (oneNumber(x + i) == num && res[1] == 0) {
                 res[1] = x + i;
             }
             i++;
         }
         return res;
     }
     //获取x的二进制表示中1的个数
     public static int oneNumber(int x) {
         String str = Integer.toBinaryString(x);
         int res = 0;
         for (int i = 0; i < str.length(); i++) {
             if (str.charAt(i) == '1'){
                 res++;
             }
         }
         return res;
     }
View Code

解法二:位操作法

解法三:算术解法

4.解释代码((n & (n-1)) ==0)的具体含义。

含义:检查n是否为2的某次方(或者检查n是否为0)。

5.编写一个函数,确定需要改变几个位,才能将整数A转成整数B。

思路:使用异或(XOR)操作找出两个数之间有哪些位不用,也即异或操作的结果C中有几个1。

解法一:不断对C执行移位操作,然后检查最低有效位。

    public static int bitSwapRequired(int a, int b) {
        int count = 0;
        for (int c = a ^ b; c != 0; c = c >> 1) {
            count += c & 1;
        }
        return count;
    }
View Code

解法二:不断翻转最低有效位,计算要多少次C会变成0。操作C=C&(C-1)会清除C的最低有效位。

    public static int bitSwapRequired(int a, int b) {
        int count = 0;
        for (int c = a ^ b; c != 0; c = c & (c - 1)) {
            count++;
        }
        return count;
    }
View Code

6.编写程序,交换某个整数的奇数位和偶数位,使用指令越少越好(也就是说,位0与位1交换,位2与位3交换,以此类推)。

思路:先操作奇数位,再操作偶数位。可以用10101010(即0xAA)作为掩码,提取奇数位,并将它们右移1位,移到偶数位的位置。对于偶数位,可以施以相同的操作。最后,将两次操作的结果合并成一个值。

    public int swapOddEvenBits(int x) {
        return ( ((x & 0xaaaaaaaa) >> 1) | ((x & 0x55555555) << 1) );
    }
View Code

上述代码实现的是32位整数。如果要处理64位整数,需要修改掩码。

7.数组A包含0到n的所有整数,但其中缺了一个。在这个问题中,只用一次操作无法取得数组A里某个整数的完整内容。此外,数组A的元素皆以二进制表示,唯一可用的访问操作是“从A[i]取出第j位数据”,该操作的时间复杂度是常数。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?

思路:类似的问题:给定一列0到n的数,其中只缺一个数字,把这个数找出来。直接将这列数相加,然后与0到n的总和(即n*(n-1)/2)进行比较,两者差值就是那个缺失的数。

题目中说了所有的整型数都是以二进制数表示的,而且可以以O(1)的时间来访问任意一位,这也很明显的提示了我们用位操作。

先来看一个n=13的例子,那么0到13的二进制数表示如下:

0000       00100        0100       01100
00001        00101        01001        01101
00010        0011       01010
00011        00111        01011

然后观察上面数字的最低有效位Least Significant Bit (LSB),然后统计1和0的个数,可以得出一个规律:当n是奇数时,0和1的个数相等,当n是偶数时,0比1多一个。

那么当移除一个数后,就有下面四种情况:

N为偶数,0比1多一个

N为奇数,0和1个数相等

移除0

0和1个数相等

0比1个数少

移除1

0比1个数多

0比1个数多

从上表可以看出来比较LSB的1和0的个数就可以知道移除的是1还是0,只要0的个数小于等于1,就是移除0,若0的个数大于1,就是移除1.一旦知道最低有效位移除的值,那么把所有不符合要求的都去掉,然后再用相同方法来判断倒数第二低有效位,以此类推直到所有的数字都被移除了,具体过程参见如下,当n=13,且移除了一个数:

00000        00100        0100       01100
0000       0010       01001        01101
00010        00110        01010
------           00111        01011

那么我们来统计最低有效位的0和1的个数,发现0比1个数多,由此我们知道移除的数的最低有效位是1,那么我们把所有最低有效位是0的数都去掉:

00000        00100        01000        01100
00001        00101        01001        01101
00010        00110        01010
------           00111        01011

我们再来统计倒数第二低有效位的0和1的个数,我们发现0比1个数多,那么移除数的倒数第二低有效位还是1,同样把其他的去掉:

00000        00100        01000        01100
00001        00101        01001        01101
00010        00110        01010
------           00111        01011

我们再来统计倒数第三低有效位的0和1的个数,我们发现0比1个数相等,那么移除数的倒数第三低有效位是0,同样把其他的去掉,那么就只剩下一个了:

01011

那么倒数第四低有效位的0比1个数小,移除的是0,把这个不是0的删掉,则数组为空了,我们就可以停止了,把所有移除的组合起来就得到0011,也就是最后的答案。时间复杂度为O(N)。

    public static int findMissing(ArrayList<BitInteger> array) {
        //bit 0位对应于LSB(最低有效位)。以此为起点,逐步向较高的位推进
        return findMissing(array, 0);
    }
    public static int findMissing(ArrayList<BitInteger> input, int column) {
        //终止条件与错误条件
        if (column >= BitInteger.INTEGER_SIZE) {
            return 0;
        }
        //记录符合条件的倒数第column位是0或1的数的数组
        ArrayList<BitInteger> oneBits = new ArrayList<BitInteger>(input.size()/2);
        ArrayList<BitInteger> zeroBits = new ArrayList<BitInteger>(input.size()/2);
        for (BitInteger t : input) {
            //数t的第column位等于0
            if (t.fetch(column) == 0) {
                zeroBits.add(t);
            } else {
                oneBits.add(t);
            }
        }
        if (zeroBits.size() <= oneBits.size()) {
            int v = findMissing(zeroBits, column + 1);//递归的方式计算出v的其他位
            return (v << 1) | 0;//插入0
        } else {
            int v = findMissing(oneBits, column + 1);
            return (v << 1) | 1;//插入1
        }
    }
View Code

8.有个单色屏幕存储在一个一维字节数组中,使得8个连续像素可以存放在一个字节里。屏幕宽度为w,且w可被8整除(即一个字节不会分布在两行上),屏幕高度可由数组长度及屏幕宽度推算得出。请实现一个函数drawHorizontalLine(byte[] screen, int width, int x1, int x2, int y),绘制从点(x1, y)到点(x2, y)的水平线。

思路:首先计算x1和x2之间包含几个完整字节,使用screen[byte_pos]=0XFF,一次就能设定一整个字节。这条线起点和终点剩余部分的位,使用掩码设定。

分析:

绘制水平线本质上是找到水平线上的起点和终点,然后连接,起点和终点都已经确认,这个应该没什么难度。

由于是一维数组,首先明确在屏幕中:x代表水平方向,也就是实际意义上的竖轴(列),y代表垂直方向,代表了矩阵中实际意义的横轴(行) 。

假设给定的像素位置为(x,y),那么它实际在一维数组的位置p=x+w*y,所以起点(x1,y)在是screen[x1+w*y]中的字节,(x2,y)对应screen[x2+w*y]字节,找到两个字节后调用drawLine函数绘制 。

书上解法:

绘线的本质实际上将线上的所有像素的颜色都设置为同一个颜色。那么暴力破解解法就是遍历从(x1,y)到(x2,y)上所有经过的像素设置颜色,时间复杂度为O(x2-x1)。

注意:给定的x是像素的横坐标而不是字节,一个像素占据一个bit

关键: 

1 绘线的本质实际上将线上的所有像素的颜色都设置为同一个颜色
2
确定比特x1对应的字节x1Byte = x1 / 8,以及起始比特位x1Bit = x1 % 8
确定比特x2对应的字节x2Byte = x2 / 8, 以及结束比特位x2Bit = x2 % 8
如果x1Bit不为0,说明x1Byte不是完整的字节,令起始字节startByte = x1Byte + 1;否则,令startByte = x1Byte
如果x2Bit不为7,说明x2Byte不是完整的字节,令结束字节endByte = x2Byte - 1;否则,令endByte = x2Byte
将startByte~endByte中字节全部设置为0xff(即白色)
设置左边不完整字节(左边起始字节)掩码,用于后续“与“”运算,必须将不完整字节前面x1Bit位排除,即前面x1Bit位为0,后面8-x1Bit位为1, 所以左边掩码leftMask = 0xff >> x1Bit
设置右边不完整字节(右边最后字节)掩码,必须将x2Bit位包含,即前面x2Biit(包含x2Bit位本身)位为1,后面8-x2Bit位为0, 所以右边掩码rightMask = ~ ( 0xff >> (x2Bit + 1) )
还需要判断x1与x2对应的字节是否在同一个字节中,因为如果在同一个字节中,需要对左边掩码和右边掩码进行与操作才能得到真实的掩码
     mask = leftMask & rightMask
     对应字节在screen中位置pos = w * y/8 + x1Byte
     对应的字节为 screen[ pos ] |= mask
     [易错,容易漏掉] 
如果不在同一个字节,则对左边不完整字节,掩码处理
     posLeft = w * y/8 + x1Byte
     screen[ posLeft ] |= leftMask
右边不完整字节取掩码
     posRight = w * y/8 + x2Byte
     screen[ posRight ] |= rightMask
结束
3 screen.at(pos) |= leftMask;//注意是“或“”运算,通过1来设置颜色,其余保持字节原先的颜色
如果用与,那么掩码中0的部分会使得像素颜色发生变化,而我们真正需要的是掩码中的1,
掩码中的0应该不起作用,所以用或运算。

    public static void drawLine(byte[] screen, int width, int x1, int x2, int y) {
        int start_offset = x1 % 8;//起始比特位
        int first_full_byte = x1 / 8;//比特(bit)x1对应的字节(byte)
        if (start_offset != 0) {//比特x1对应的字节不完整
            first_full_byte++;//起始字节加1
        }
        
        int end_offset = x2 % 8;//终止比特位
        int last_full_byte = x2 / 8;//比特(bit)x2对应的字节(byte)
        if (end_offset != 7) {//比特x2对应的字节不完整
            last_full_byte--;//终止字节减1
        }
        
        // 设置完整的字节
        for (int b = first_full_byte; b <= last_full_byte; b++) {
            screen[(width / 8) * y + b] = (byte) 0xFF;
        }
        //创建用于线条起点和终点的掩码
        //将不完整字节前面start_offset Bit位排除,即前面start_offset Bit位为0,后面8-start_offset Bit位为1
        byte start_mask = (byte) (0xFF >> start_offset);
        //将end_offset Bit位包含,即前面end_offset Bit(包含end_offset Bit位本身)位为1,后面8-end_offset Bit位为0
        byte end_mask = (byte) ~(0xFF >> (end_offset + 1));
        
        // 设定线条的起点和终点
        if ((x1 / 8) == (x2 / 8)) { //x1和x2位于同一字节
            byte mask = (byte) (start_mask & end_mask);//对左边掩码和右边掩码进行与操作才能得到真实的掩码 
            screen[(width / 8) * y + (x1 / 8)] |= mask;
        } else {
            if (start_offset != 0) {
                int byte_number = (width / 8) * y + first_full_byte - 1;
                screen[byte_number] |= start_mask;
            }
            if (end_offset != 7) {
                int byte_number = (width / 8) * y + last_full_byte + 1;
                screen[byte_number] |= end_mask;
            } 
        }
    }
View Code
原文地址:https://www.cnblogs.com/struggleli/p/7891437.html