位运算
第一章
00课程
推荐参考书:
01位运算的奇艺技巧
1.位运算与进制基础
-
判断奇偶数
x&1 = 1 //奇数 x&1 = 0 // 偶数
对于任何一个数 & 1,因为1的二进制是1,在它的补码中,除了最后一位为1,其它全部为0,前面提到,按位与运算&只有当两个数都为1时结果才是1,因此,任何一个数与1相与,因此最终的结果只有0和1两种情况,而且结果取决于另外那个数二进制中的最后一位(若为1,最终结果便为1,若为0,结果便是0)
-
获取二进制位是0还是1(两种解决 方案)
// 先将1左移到指定的第i位,然后在用与&判断是否是1。最后右移第i位回去。 return (1 << & num) >> (i-1);
-
交换两个整数变量的 值
用三次异或运算
1,首先需要明确的是异或运算满足交换律和结合律,即有如下公式
a^b=b^a; b^a^b=a^b^b;
2,其次,异或运算还满足下面的公式
a^a=0; a^0=a;
3,使用异或运算实现两个变量交换变量值的代码如下
a=a^b; b=a^b; a=a^b;
4,对第三步的代码进行分析如下
a=a^b; b=a^b=a^b^b=a^(b^b)=a^0=a; a=a^b=a^b^a=b^(a^a)=b^0=b;
从而实现了使用异或运算将两个变量值互换。
5,在日常编程情况下,不建议使用这种方法,首先是速度不一定能够得到提高。
6,对这种方法的另一种理解:
1 a^b=diff;//diff为a,b两个变量的差值 2 a^diff=b;//a减去差值为b 3 b^diff=a;//b减去差值为a
-
不用判断语句,求整数的绝对值
public static int abs(int num) { return num * (1 - ((num >>> 31)<<1)); }
num>>>31 得到的是num的符号位。num为负则num>>>31=1;否则num>>>31=0;
(num>>>31) < <1 相当于将 num>>>31 乘以2.
1- ((num>>>31) < <1) 的作用是将 ((num>>>31) < <1)由0变为1,或者由1变为-1;
经过1- ((num>>>31) < <1) 的变换后,如果Num为负,则表达式的结果为-1;否则为1。
num* (1- ((num>>>31) < <1))的意思也就明确了……
异或,可以理解为不进位加法:1+1 =0 ,0 + 0 = 0,1 + 0 =1
性质:
-
交换律 可以任意交换因子的位置结果不变。
-
结合律:即
(a^b)^c
=a^(b^c)
-
对于任何数x,都有x^x = 0,x^0 =x,同自己求异或是0(自己和自己一定相同),同0求异或是自己(异或只有不同才出1,不同的时候肯定是因为原数中有1 ,异或的结果中才有1)
-
自反性
A^B^B
= A^0 =A,连续和一个因子做异或运算,最终结果为自己
位运算符
在处理整形数值时,可以直接将组成整形数值的各个位进行操作。这意味着可以用屏蔽技术获得整数中的各个位。
&(与) |(或) ^(异或) ~(非、取反)
>>和>>>运算符将二进制位进行右移或者左移操作。
>>>运算符将0填充高位,>>运算符用符号位填充高位,没有<<<运算符
对于int型,1<<35和1<3的结果是相同的,而左边的操作数是long型时候需要对右侧的操作数做模64
因为int型只有32位,35会对32取模,所以35和3的结果相同
与:都为1结果为1,或:有一个为1结果为1,异或:二者不同时为1
题解
找出唯一成对的数
不重复的数给他消去
利用连续异或消除重复
思路:
利用两组【1~1000】异或,正好两组之间相同的异或消去最后剩下三个,俩个异或消去,最终剩余
import java.util.Random;
import com.sun.corba.se.impl.javax.rmi.CORBA.Util;
public class 唯一成对的数 {
public static void main(String[] args) {
int N = 11;
int arr[] = new int[N];
int index = new Random().nextInt();
for (int i = 0; i < arr.length - 1; i++) {
arr[i] = i + 1;
}
// 最后一个数是随机数 产生是从0到10
arr[arr.length - 1] = new Random().nextInt(N);
// 随机下标
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]);
}
int x1 = 0;
//将其补为两个相同的
for (int i = 1; i <= N - 1; i++) {
x1 = (x1 ^ i);
}
for (int i = 0; i < N; i++) {
x1 = x1 ^ arr[i];
}
System.out.println(x1);
System.out.println("============");
}
}
//开一个空间存出现次数
另外开辟辅助空间
int [] helper = new int[N];
for (int i = 0; i < N; i++) {
helper[arr[i]]++;
// arr[i]中存的是一到一千这些数,我们把这些数变成下标
// 下标放到helper中去,对计数进行helper+
}
for (int i = 0; i < N; i++) {
if(helper[i] == 2) {
System.out.println(i);
}
}
找出落单的数
思路:
连续的进行异或,成对出现的就会消除,单个的就会保留。保留的即为答案。
二进制中1的个数
思路一:
if((x&(1<<i))==(1<<i)) {
count++;
}
代码:
import java.util.Scanner;
public class 一的个数 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc = new Scanner(System.in);
int x = sc.nextInt();
//将int转换为二进制
System.out.println(Integer.toString(x, 2));
int count = 0;
for(int i = 0;i<32;i++) {
if((x&(1<<i))==(1<<i)) {
count++;
}
}
System.out.println(count);
}
}
Tips:
Integer.toString(int par1,int par2),par1表示要转成字符串的数字,par2表示要转成的进制表示,如:
Integer.toString(22,2),表示把22转成2进制表示的字符串,
思路2:
不带符号将原来的数字往右边挪动,挪动高位就补0
代码:
int count2 = 0;
for (int i = 0; i < 32; i++) {
if(((x>>>i)&1)==1) {
count2++;
}
}
System.out.println(count2);
思路3:
想办法干掉1,1被干掉了几次就说明有几个1。
int count3 = 0;
//因为此时不知道具体次数,所以使用while循环
while(x!=0) {
x = ((x-1)&x);
count3++;
}
System.out.println(count3);
是不是2的整数次方
要求:只要一条语句
思路:
一个数是2的整数次方,说明这个数的二进制数的表示中只有一个1。
并且要考虑1。无需考虑负数,因为2的负数次方都是浮点数,比如2^(1/2)是浮点数。
代码:
import java.util.Scanner;
public class 是不是2的整数次方 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc = new Scanner(System.in);
int x = sc.nextInt();
System.out.println(Integer.toString(x,2));
// (x-1)&x)==0起到的作用就是消掉最低位的1
if(((x-1)&x)==0) {
System.out.println(x+"是2的整数次方 ");
}else {
System.out.println(x+"不是2的整数次方 ");
}
}
}
将整数的奇偶位互换
思路:
代码:
public class 交换奇偶位 {
public static void main(String[] args) {
// TODO Auto-generated method stub
int a = 6;
int b =m(a);
System.out.println(Integer.toString(a,2));
System.out.println(Integer.toString(b,2));
System.out.println(b);
}
public static int m(int x) {
int ou = x&0xaaaaaaaa;//和1010 1010 。。。。做与运算取出偶数位
int ji = x&0x55555555;//和0101 0101.。。。。做与运算取出奇数位
return (ou>>1)^(ji<<1);
}
}
0~1之间的浮点数和二进制之间的实数
整数部分的十进制转二进制是除以2。取余留商。
小数部分:乘以2,然后取出整数部分,将剩下的小数部分继续乘以2,然后再取整数部分,一直取到小数部分为零为止。就是乘以二然后判断小数点前面,扣掉这个整数部分再次乘二。
public class 二进制小数 {
public static void main(String[] args) {
// TODO Auto-generated method stub
double num = 0.5;
StringBuffer sb = new StringBuffer("0.");
while(num>0) {
// 乘2:挪整
double r = num*2;
// 判断整数部分
if(r>=1) {
// 消掉整数部分
num = r-1;
sb.append("1");
}else {
sb.append("0");
num = r;
}
if(sb.length()>34) {
System.out.println("Error");
return;
}
}
System.out.println(sb.toString());
}
}
出现k次与出现1次
题干:
代码:
第一种解法:一个键值对数组储存,存储出现的数字以及其对应出现的次数。
public class 出现k次与出现1次 {
public static void main(String[] args) {
// TODO Auto-generated method stub
int arr[] = {3,3,3,4,4,4,5,5,5,6};
//暴力破解
int count[] = new int [arr.length];
for (int i = 0; i < arr.length; i++) {
++count[arr[i]];
}
for (int i = 0; i < count.length; i++) {
if(count[arr[i]]==1) {
System.out.println(arr[i]);
}
}
}
}
第二种解法:做不进位加法
首先要知道一个结论,2个相同的二进制数,做不进位加法,结果为0;
10个相同的十进制数,做不进位加法,结果为0;
k个相同的k进制数,做不进位加法,结果为0;
思路就是:全部转为k进制的数,相加,最后剩下的就是只出现1次的数。
十进制转k进制,java的api提供的有现成的方法,即Integer.toString(i,radix); i 为十进制的数,radix为进制。或着自己手动取余也是一种方法,只不过比较麻烦。
import java.util.*;
public class 出现k次和出现1次 {
public static void main(String[] args) {
Scanner cin = new Scanner(System.in);
int[] arr = { 3, 3, 3, 5, 7, 7, 7, 8, 8, 8, 6, 6, 6 };
int len = arr.length;
char[][] kRadix = new char[len][];
int k = 3;
int maxlen = 0;
//转换成k进制字符数组
//对于每个数字
for (int i = 0; i < len; i++) {
kRadix[i] = new StringBuilder(Integer.toString(arr[i], k)).reverse().toString().toCharArray();
if (maxlen < kRadix[i].length)
maxlen = kRadix[i].length;
}
int[] resArr = new int[maxlen];
for (int i = 0; i < len; i++) {
//不进位加法
for (int j = 0; j < maxlen; j++) {
if (j > kRadix[i].length)
resArr[j] += 0;
else
resArr[j] += (kRadix[i][j] - '0');
}
}
int res = 0;
for (int i = 0; i < maxlen; i++)
res += (resArr[i] % k) * (int) (Math.pow(k, i));
System.out.println(res);
}
}
思路:
所有数先转成k进制,然后做k进制的不进位加法,做完之后剩下就是我们答案的k进制,然后将其还原想要的结果
1.任意进制互转
手工取余法
Integer.toString(i,radix);
位运算实现加减乘除
思路1:
首先观察十进制加法的规律
类比得到二进制加法
如果想得出每一位上的值,需要将这一位和1进行与运算
得到这一位是0还是1
long flag = 0x1;
long bita = a & flag;
long bitb = b & flag;
不断移动flag 的位置:
flag <<= 1;
// 等价于flag=flag<<1,flag<<1表示b左移1位(二进制),等价于flag乘以2的1次方, flag<<=1的含义就是,flag等于flag乘以2的1次方
设置每一位上的求和得到的值
long sum = 0;
//如果这两位都是1
如果之前没有进位sum = 0,carry = true。
之前有进位,那么sum此时为1+1(本次的两个1)+1(之前两位的进位)
sum|=flag;sum为1,carry也为1
//如果这两位其中一个为1
如果之前有进位,那么sum = 0(本次的一个1和之前进位的一个1),产生一个进位carry。
如果之前没有进位那么sum = 1,不产生进位。
//如果当前都是0
如果之前有进位那么sum = 1,把进位给取消掉
代码(有错):
private static long add(int a, int b) {
// TODO Auto-generated method stub
if (a == b) {// 特殊情况
return a << 1;
}
// 用一个标志表示当前在多少位
long flag = 0x1;
// 当前位的值
boolean carry = false;
long sum = 0;
// 重复做,所有的位都比较一下
while (flag <= a || flag <= b) {// 比较flag是否变为1111...11当flag位数不够的时候肯定会比a和b小
long bita = a & flag;
long bitb = b & flag;
if ((bita & bitb) == 1) {// 如果这两位上都是1那么一定会产生进位
// 如果之前有进位,1+1+1三个一相加
if (carry == true) {
sum |= flag;
}
carry = true;
} else if ((bita | bitb) == 1) {
if (carry) {// 如果之前有进位
carry = true;
sum |= flag;
} else {
sum |= flag;// 0(sum)和1(flag)相或赋给sum
}
} else {// 当前都是为0
// 如果有进位
if (carry) {
// 进位给它加进去
sum |= flag;
// 然后取消掉
carry = false;
}
}
flag <<= 1;
// 等价于flag=flag<<1,flag<<1表示b左移1位(二进制),等价于flag乘以2的1次方,flag<<=1的含义就是,flag等于flag乘以2的1次方
}
// 如果到最后有一个进位的话,再把最后的标志位给设置上
if (carry) {
sum |= flag;
}
return sum;
}
思路2:
使用位运算实现加法时,基本思路与计算机组成原理中的加法器类似,使用位运算a ^ b来实现没有进位的本位和,用a & b来获得需要进位的位置,则(a & b << 1)则表示进位加1后的数,再用或运算求本位和,直至(a & b) == 0即没有进位为止。
递归版本:
private static int add(int a, int b) {
if(b==0||a==0) {//无进位的时候完成运算
if(a==0) {
return b;
}else {
return a;
}
}
int sum = 0,carry = 0;
sum = a^b;//完成第一步没有进位的加法运算
carry = (a&b)<<1;//完成第二步的进位运算并左移运算
System.out.println("sum = "+sum+" carry = "+carry);
return add(sum,carry);//进行递归,相加
}
先不计进位相加,然后再与进位相加,随着递归,进位会变为0,递归结束。
public static int Add(int a,int b)
{
return b!=0 ? Add(a^b,(a&b)<<1) : a;
/*if(b)
return Add(a^b,(a&b)<<1);
else
return a;*/
}
非递归版本:
int Add(int a, int b)
{
int ans;
while(b)
{ //直到没有进位
ans = a^b; //不带进位加法
b = ((a&b)<<1); //进位
a = ans;
}
return a;
}
乘法
//正数乘法运算
public static int pos_Multiply(int a,int b)
{
int ans = 0;
while(b!=0)
{
if((b&1)==1)
ans = Add2(ans, a);
a = (a<<1);
b = (b>>1);
}
return ans;
}
除法
除法就是由乘法的过程逆推,依次减掉(如果x够减的)y(231),y(230),...y8,y4,y2,y1。减掉相应数量的y就在结果加上相应的数量。
public static int pos_Div(int x,int y)
{
int ans=0;
for(int i=31;i>=0;i--)
{
//比较x是否大于y的(1<<i)次方,避免将x与(y<<i)比较,因为不确定y的(1<<i)次方是否溢出
if((x>>i)>=y)
{
ans+=(1<<i);
x-=(y<<i);
}
}
return ans;
}