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; }
1.2一个整型数组里除了一个数字之外,其他的数字都出现了三次。请写程序找出这一个只出现一次的数字。
1.3 一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
思路:位运算中异或的性质:两个相同数字异或=0,一个数和0异或还是它本身。
//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; } }
另一种解法:
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); } } }
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;//对两者执行位或操作 } }
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(); }
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; }
解法二:位操作法
解法三:算术解法
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; }
解法二:不断翻转最低有效位,计算要多少次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; }
6.编写程序,交换某个整数的奇数位和偶数位,使用指令越少越好(也就是说,位0与位1交换,位2与位3交换,以此类推)。
思路:先操作奇数位,再操作偶数位。可以用10101010(即0xAA)作为掩码,提取奇数位,并将它们右移1位,移到偶数位的位置。对于偶数位,可以施以相同的操作。最后,将两次操作的结果合并成一个值。
public int swapOddEvenBits(int x) { return ( ((x & 0xaaaaaaaa) >> 1) | ((x & 0x55555555) << 1) ); }
上述代码实现的是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的二进制数表示如下:
00000 00100 01000 01100
00001 00101 01001 01101
00010 00110 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 01000 01100
00001 00101 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 } }
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; } } }