数的二进制表示
首先二进制表示分为两种,一种是无符号的数,一种是有符号的。区别在于二进制的第一位是否保存符号数。
比如说无符号数,那就是二进制的每一位都表示一个真正的数值,如1011就等于 1+2+8 = 11。而有符号数的二进制第一位并不表示数值,而是表示符号位,如1011 第一位是1所以是负数。剩下的表示真正的值(但是值也不是2+1 = 3,马上就会讲到为什么)。
然后,有符号数的正数和负数在计算机中的存储是不一样的,正数的话正常运算,比如5转化为二进制,首先是正数第一位得补0,所以是 0101 ,而负数要更加麻烦些,我们先要求出他的值,比如-5转化为二进制,首先第一位应该是符号位也就是1,所以正常应该是1101,不过负数的存储要存他的补码,也就是在这个基础上除了符号位所有位取反(变成1010)再加1,所以正常存在计算机里的是1011。
至于为什么如此存储,本文不再深究(也是计算机基础想必大家都会),如果不会的话可以去网上搜索原码补码相关内容。
位运算符号
& | ~
& 表示按位与,他会把两个二进制数逐位的比较。如果两个数的同一位上都是1,那么结果为1,如果只要有一个不是1,那就为0。凭此可以判断奇偶,如果一个数&1 = 1是奇数,反之是偶数
| 表示按位或,他会把两个二进制数逐位的比较。如果两个数的同一位上只要有一个是1,那么结果为1,如果都是0,那么结果为0。
~ 表示按位取反,他会把两个二进制数逐位的取反(即0变1,1变0)
位移运算符
>>有符号右移
a>>1
即相当于把二进制数最后一位删去,左边补上符号位
>>>无符号右移
和有符号右移差不多,不过这个药在左边补上0,而不是符号位
<<左移
a<<1 左移不区分有符号无符号,左边移除一位,只要在最后补0即可。
异或运算
^符号

1^1=0;
1^0=1;
0^1=1;
0^0=0;
也就说0和1异或的时候相同的异或结果为0,不同的异或结果为1,根据上面的规律我们得到
a^a=0;自己和自己异或等于0
a^0=a;任何数字和0异或还等于他自己
a^b^c=a^c^b;异或运算具有交换律
位运算常用小技巧
1.乘2
a*2 = a<<1 正负数通用
或许有点难理解为什么正负通用,因为负数左移之后符号位会消失。但是呢,符号位的下一位仍然是负数(除非越界),所以不用担心负数不适用的问题。
可与看出他在溢出之前都工作的很好
2.乘2加1
a*2 + 1 = a<<2|1,原因是左移后最后一位必为0,按位或运算符和1运算可与把末位0置为1.
正负数通用
3.除以2
a/2 = a>>2 正负数通用
如果无法整除,那么向上取整合向下取整有个问题:
如果是正数的话会向下取整:11>>2 =5
如果是负数的话也会向下取整取整:-11>>2 = -6
而正常的除运算:正数也会向下取整,但是负数会向上取整:-11/2 = -5,所以这是个需要注意的点
异或:交换两个数
a ^= b;
b ^= a;
a ^= b;
原理: 异或运算相当于两个数的差异位设置为1,其他设置为0,
a ^= b 时,a 的值为 a ^ b;
b ^= a 等于 b = b ^ (a ^ b) 等于 a;
a ^= b 等于 a = (a ^ b) ^ a 等于 b;
这或许会导致一个问题,因为a^a = 0。所以如果说在中途中出现了两个数是同一个变量让你交换,这个不会像你希望的那样变量还是原来的值,而会把变量值设为0。
取相反数
-a = ~a+1
因为补码存储,很好理解。正负数都可以
获取最低位 1(lowbit)
a&-a
负数的补码是正数补码加一。正数低位的 0 取反后为 1,再加 1 则会进位直到将遇到的第一个 0 转为 1。因此负数补码的最低位 0 就是正数补码的最低位 1。因此 a 与 -a 的最低位 1 是相同的,其他位 1 均不相同。因此 a & (-a) 可以获取最低位 1,此操作也被称为 lowbit 操作。
异或:一堆数中找到唯一孤独的那一个
因为 a^a = 0, 0^a = a,异或有交换律,所以假如说一个数组中除了某数只有一个之外之外剩下的全是成对的,例:[1,1,2,2,3]
那么想找到唯一一个孤独的数的话,就把他们全异或一下,结果就是了。
n&(n-1):把二进制数的最后一位1换成0
这个算式可以获得n把最后一位1换成0之后的二进制数,其实很好理解,n-1就相当于把n的最后一位1换成0然后后面的0换成1,然后&运算之后自然就把最后一位1和之后所有数位都设为0了
这个方法也可以来判断数是否为2的幂,因为2的倍数二进制位上应该只有一个1、
第一题
剑指 Offer 15. 二进制中1的个数
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为 汉明重量).)。
提示:
- 请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
- 在 Java 中,编译器使用 二进制补码 记法来表示有符号整数。
解法1
看到这道题,我们首先应该潇洒的写上一行代码然后离去,直接就能通过。
public class Solution {
public int hammingWeight(int n) {
return Integer.bitCount(n);
}
}
但是如果面试这么做的话大概面试官会不太高兴吧....不过无论怎么说,库函数的学习还是有用的。
解法2
这应该是最好想到的方法了,就是每次把n无符号右移一位。然后再&1,也就是判断最后一位是不是1。如果是的话就把result加1,然后最后return result。
需要注意的是n的条件不能判断不能写n>0,因为无符号右移会有前导0,会被认为是负数。
public class Solution {
public int hammingWeight(int n) {
int result=0;
while(n != 0){
//注意这里不加括号会报错,因为位运算的优先级问题。
if((n & 1) == 1){
result++;
}
n =n >>> 1;
}
return result;
}
}
这个的时间复杂度就是o(n),n是二进制的数位。如果认为参数是n的话,复杂度就是o(log2n)
我们可以看到,已经击败100%了。但是,是否有一种方法能更快的解决这个问题呢?
解法3
之前我们其实有一个地方浪费了时间,就是我们既判断了1位,也判断了0位,但是我们真的需要判断0位吗,是否有一种方法只判断1的位置就足够解决这个问题了呢?
答案自然是有的,那就是用之前说过的小技巧n &(n-1),这个的用法之前已经说过了,我们直接来上代码就好。
public class Solution {
public int hammingWeight(int n) {
int result=0;
while(n != 0){
n &= (n-1);
result++;
}
return result;
}
}
这样复杂度就又降低了。
第二题
剑指 Offer 53 - II. 0~n-1中缺失的数字
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
解法1
既然提到了有序数组,那自然就要想到二分法。
class Solution {
public int missingNumber(int[] nums) {
//二分法难以处理没有最后一个数的情况,需要单独处理
if(nums[nums.length-1] == nums.length-1){
return nums.length;
}
int low = 0;
int high = nums.length - 1;
while(low < high){
//这样计算中值是因为防止high+low可能导致int溢出
int mid = low + (high - low)/2;
if(mid != nums[mid]){
high = mid;
}
else{
low = mid + 1;
}
}
return low;
}
}
大致思路就是二分看看索引是否和索引所在的元素相等,如果相等说明缺失的数在右半部分,如果不相等说明缺失的数在左半部分
而下图这种形式(缺失8)二分法很难解决,所以要特殊判断。
最终也是100%了。
解法2
毕竟本文是讲位运算的,所以要讲一下位运算的解法。
不过大家要注意一下:位运算的时间复杂度是o(n),而二分是o(logn),所以还是二分快一点的。
class Solution {
public int missingNumber(int[] nums) {
int res = 0;
for(int i = 0;i<nums.length;i++){
res = res ^ i ^ nums[i];
}
//这样循环会少异或一个最大的数,所以补上。
res ^= nums.length;
return res;
}
}
原理:
假如说我把0-n所有元素都加入数组,那么数组里应该只有一个元素是单个的,其他都是两个两个的,而相同数的异或运算结果为0,即a^a = 0,而且0^a = a ,而且异或还有交换律。通过这些,我们就能知道,如果我们把所有的数都异或一遍,那得出的答案就是最孤单的那个数了,也就是答案
这里顺便说一下,看到有序数组还是要优先考虑二分,位运算在这里除了炫技之外也没啥用处了。
第三题
剑指 Offer 56 - I. 数组中数字出现的次数
一个整型数组 nums
里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
解法
既然要求了时间复杂度o(n),这就阻止了我们暴力循环解,还要求了空间o(1),这也阻止了我们用map写。所以也就只能用我们这一种方法了,也就是位运算。
这题的位运算还是很好想的,和上一题差不多,区别就是这次是两个不一样的数了。
如果说除了一个数字之外都出现两次,那么把他们都异或运算一下就能得到剩下的那个。那么既然这题是两个孤独的数字,那就很好处理了:我们把数组分成两个数组,每个数组只有一个孤独的数字。这样两边都异或一下不就得到结果了?所以难点就在于怎么确保两个孤独的数分别在两个数组里面。
这就要用到异或的知识了,设第一个孤独的数是a,第二个是b,首先我们知道,我们把整个数组的元素全都异或一遍会得到一个a^b,而异或的性质确保了这个a^b的结果的二进制位如果有一个1,说明这个位就是a和b的差别所在:即a和b的这个数位上,一个是1,一个是0。
那么我们就可以如此区分出两个数组,解决问题:
class Solution {
public int[] singleNumbers(int[] nums) {
//用来保存结果
int res = 0;
//循环数组算出a^b
for(int i = 0;i<nums.length;i++){
res ^= nums[i];
}
//保存某个数位,具体下个循环里讲解
int mask = 1;
//
while((res & mask) == 0){
//刚开始mask是1 也就是0001
//如果res的末位是1,也就得到我们所需要的数位了,就是倒数第一位,如果这样的话(res & mask) == 0)会成立,也就不会进行循环。
//如果不是的话,我们把mask左移一位,也就是 0010
//这样如果res的倒数第二位是1,就得到了我们需要的数位.....依此循环
mask <<= 1;
}
//用来保存两个孤独的数
int a=0,b=0;
for(int i = 0;i<nums.length;i++){
//用我们之前算出的那个数位来按位与一下每个值,并且依次分堆分别异或运算,因为这样分堆之后a和b一定在两个堆中,所以两个答案就是a和b。
if((nums[i] & mask) == 0){
a ^= nums[i];
}
else{
b ^=nums[i];
}
}
return new int[]{a,b};
}
}
第四题
剑指 Offer 65. 不用加减乘除做加法
写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。
思路
如何不通过加法写出加法的效果?这道题如果大家学过计组的话一定会对这题有点印象,cpu的加法器实现原理就是这样的:
我们来看下二进制的加法。在一个数位上,无非有这几种情况:1+1 0+0 1+0 0+1 ,他们在本数位的结果应该是0,0,1,1 这几个数组是不是看上去很眼熟?没错这和异或算法一样,也就是如果求本位的两数加和结果,仅仅用异或运算符就足够,但我们还要考虑进位,那我们再来看看进位的情况1+1 0+0 1+0 0+1 ,只有第一种情况有个进位是1,这和按位与运算符还一样,所以说用&以及^就能做出本题:
思路就是异或算出本位,然后逐渐循环把每次进位的加上去。
class Solution {
public int add(int a, int b) {
while(b!=0){
int c = (a&b) << 1;
a ^= b;
b = c;
}
return a;
}
}