文章目录
作为计算机专业的学生,对于各种二进制表示数的方法需要具备一定的敏感性,但是每次碰到相关的题目或者算法题,却总是不能第一时间反应出来,因此本专题对相关题目进行整理汇总,让自己的脑子具备“二进制思维”。
相关知识
1. 关于补码的一些认识
拿有符号8位的二进制表示的数值范围:-27…27-1
0111 1111是最大的正数 127
1000 0000是最小的负数(绝对值最大的负数)-128
一个需要确立的感性认知是:当我们加一个数时,顺时针旋转,减去一个数时,逆时针旋转。当数字本身很大,加上一个数两极反转,反倒变成最小;当数字本身很小,减去一个数两极反转,反倒变成最大。
我们经常会碰到位运算求数值的问题,该计算方法对补码负数同时有效,对于8位有符号数来说:
byte ans=0;
for (int i = 0; i < 8; i++) {
ans += (1 << i);
}
1000 0011
为(1<<7)+(1<<1)+(1<<0)=-128+2+1=-125
算出来也是负数,符合逻辑。原因是8位的最大数为(1<<7)-1
,所以1<<7
实际上已经溢出为负数了。
2. Java右移需要考虑有无符号位移
">>>"无符号右移
操作规则:无论正负数,前面补零。
">>"右移
操作规则:正数前面补零,负数前面补1
"<<"左移
操作规则:无论正负数,后面补零。
3. 移位运算时需要考虑到的运算符优先级
单算移比位逻三赋
相关题目
191.位1的个数
参考负雪明烛大神的题解,可以采用移位32次的方法,但也可以采用一种技巧性的位运算,n&(n-1)可以消除二进制末尾的1
,那么只需要记录n消除几次1最终变为0,即为答案。
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int res=0;
//题目给的是32位的数,考虑补码首位符号位的情况
for(int i=0;i<32;i++){
int temp=n>>>i&1;
res+=temp==1?1:0;
}
return res;
}
}
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int res=0;
while(n!=0){
n=n&n-1;
res++;
}
return res;
}
}
268.丢失的数字
本题可以采用多种方法进行求解,参考宫水三叶的题解,在数组内只丢失了唯一确定的一个数字。
异或法:任何数同0异或是其本身,任何数和其本身异或是0
,充分考察位运算。
class Solution {
public int missingNumber(int[] nums) {
int ans=0;
int len=nums.length;
//包括丢失的数以后的所有数字
for(int i=0;i<=len;i++){
ans^=i;
}
//让出现的数字异或为0,最后剩下丢失的数字
for(int i:nums){
ans^=i;
}
return ans;
}
}
136.只出现一次的数字
同样是异或操作,把出现偶数次的数字消掉,只剩下了出现一次的数字。
class Solution {
public int singleNumber(int[] nums) {
int res=0;
for(int i:nums){
res^=i;
}
return res;
}
}
137.只出现一次的数字II
位数统计,将所有数字在int
类型上的每一位上出现1的次数统计起来,消除出现三次的元素的方法是,已经累加的位数一一对3取模
,最后只剩下取模无法消去的只出现一次的元素。
class Solution {
public int singleNumber(int[] nums) {
int[]bit=new int[32];
for(int i=0;i<32;i++){
for(int n:nums){
if((n>>i&1)==1){
bit[i]++;
}
}
}
int ans=0;
for(int i=0;i<32;i++){
if(bit[i]%3!=0){
ans|=1<<i;
}
}
return ans;
}
}
458.可怜的小猪
参考宫水三叶大神的题解,自定义进制表示,并且用进制位数对应实验对象数目
,二进制表示能够在相互制约中表示更多信息,提高了笔者对于进制的理解。
class Solution {
//宫水三叶yyds,k进制来表示轮数
public int poorPigs(int buckets, int minutesToDie, int minutesToTest) {
int res=(int)Math.ceil(Math.log(buckets)/Math.log(minutesToTest/minutesToDie+1));
return res;
}
}
66.加一
从末尾开始+1,+1直到没法进位(进位则%10以后为0)为止
,如果全部数位都进位了,需要新构建一个1XXXXX的数组
class Solution {
//从末尾开始+1,+1直到没法进位为止,如果全部数位都进位了,需要新构建一个1XXXXX的数组
public int[] plusOne(int[] digits) {
int len=digits.length;
for(int i=len-1;i>=0;i--){
digits[i]++;
digits[i]%=10;
//如果不为0,说明该位没进位,如果为0,则一直往前进位
if(digits[i]!=0) return digits;
}
//如果全部都进位了
int[]res=new int[len+1];
res[0]=1;
return res;
}
}
2.二进制加法
有两种做法,其一是参考lilyunoke大神的加法模板,题解如下:
class Solution {
public String addBinary(String a, String b) {
int i=a.length()-1;
int j=b.length()-1;
int carry=0;
StringBuilder sb=new StringBuilder();
while(i>=0||j>=0){
int digitA=i>=0?a.charAt(i)-'0':0;
int digitB=j>=0?b.charAt(j)-'0':0;
int sum=digitA+digitB+carry;
carry=sum/2;
int digit=sum%2;
sb.append(digit);
i--;
j--;
}
if(carry!=0) sb.append(carry);
return sb.reverse().toString();
}
}
其中可以总结出来的加法模板是,这个模板可以解决很多逐位相加运算的问题,关注A当前位、B当前位、进位三要素:
while ( A 没完 || B 没完){
取到A 的当前位(A完了需要补位0)
取到B 的当前位(B完了需要补位0)
和 = A 的当前位 + B 的当前位 + 进位carry
当前位 = 和 % 2(10);
进位 = 和 / 2(10);
加入结果集
A左移调整
B左移调整
}
判断进位是否为0,不为0额外加上
将结果集反转
或者参考负雪明烛大佬的题解,不同的是循环条件,需要注意的是while循环
结束条件,注意需要遍历完两个「加数」,以及进位不为0。
class Solution {
public String addBinary(String a, String b) {
int i=a.length()-1;
int j=b.length()-1;
int carry=0;
StringBuilder sb=new StringBuilder();
while(i>=0||j>=0||carry!=0){
int digitA=i>=0?a.charAt(i)-'0':0;
int digitB=j>=0?b.charAt(j)-'0':0;
int sum=digitA+digitB+carry;
carry=sum>=2?1:0;
int digit=sum>=2?sum-2:sum;
sb.append(digit);
i--;
j--;
}
return sb.reverse().toString();
}
}
415.字符串相加
方法同上,只不过改成了十进制加法
class Solution {
public String addStrings(String num1, String num2) {
int i=num1.length()-1;
int j=num2.length()-1;
int carry=0;
StringBuilder sb=new StringBuilder();
while(i>=0||j>=0){
int digitA=i>=0?num1.charAt(i)-'0':0;
int digitB=j>=0?num2.charAt(j)-'0':0;
int sum=digitA+digitB+carry;
int digit=sum%10;
carry=sum/10;
sb.append(digit);
i--;
j--;
}
if(carry!=0) sb.append(carry);
return sb.reverse().toString();
}
}
989.数组形式的整数加法
class Solution {
public List<Integer> addToArrayForm(int[] num, int k) {
int i=num.length-1;
int carry=0;
LinkedList<Integer> ans=new LinkedList<>();
while(i>=0||k!=0){
int digitA=i>=0?num[i]:0;
int digitB=k!=0?k%10:0;
int sum=digitA+digitB+carry;
int digit=sum%10;
carry=sum/10;
//直接加到首位
ans.add(0,digit);
i--;
k/=10;
}
if(carry!=0) ans.add(0,carry);
return ans;
}
}
5.单词长度的最大乘积
如果遍历每一对单词,然后每次判断是否有重复元素,复杂度为O(N^2*L),约为O(10^9)
,TLE。因此尝试是否能将判断公共字母的时间复杂度降为O(1)
,所以我们需要进行一步预处理。针对每个单词words[i]
,采用位掩码记录该单词中出现过的字母,即如果那个字母出现,对应位置1, masks[i] |= 1 << (word.charAt(j) - 'a');
,时间复杂度O(N*L)
,总时间复杂度O(N*L+N^2),约为O(10^6)
。
class Solution {
public int maxProduct(String[] words) {
int len=words.length;
int[]states=new int[len];
for(int i=0;i<len;i++){
String w=words[i];
int wlen=w.length();
for(int j=0;j<wlen;j++){
//通过掩码数字来记录每个字符串出现的字母
states[i]|=1<<w.charAt(j)-'a';
}
}
int res=0;
for(int i=0;i<len;i++){
for(int j=i+1;j<len;j++){
if((states[i]&states[j])==0){
res=Math.max(res,words[i].length()*words[j].length());
}
}
}
return res;
}
}
7.整数反转
很经典的整数反转题目,return结果很妙。
class Solution {
public int reverse(int x) {
long res=0;
while(x!=0){
res=res*10+x%10;
x/=10;
}
return (int)res==res?(int)res:0;
}
}
9.回文数
用反转整数的方法,计算出反转的整数,最后判断两个整数是否相等。
class Solution {
public boolean isPalindrome(int x) {
//负数一定不是回文数
if(x<0) return false;
//计算反转整数
long res=0;
int quo=x;
while(quo!=0){
res=res*10+quo%10;
quo/=10;
}
//判断溢出
if((int)res!=res) return false;
return (int)res==x;
}
}
2401.最长优雅子数组
滑动窗口+位运算。用一个数字窗口window
维护整型32位上出现的数字,每次移动right只需要判断当前数字与窗口中的&
以后,是否为0,如果为0则代表所有数字都散落在窗口的不同位置,如果不为0则需要收缩左边界。可以保证的是,如果当前的数字与窗口&只为0才加入,那么窗口中所有的数字互相&都是0.
class Solution {
public int longestNiceSubarray(int[] nums) {
int res=0;
int len=nums.length;
int window=0;
int left=0;
int right=0;
while(right<len){
int temp=window & nums[right];
while(temp!=0){
window ^= nums[left];
temp = window & nums[right];
left++;
}
window |= nums[right];
res=Math.max(res,right-left+1);
right++;
}
return res;
}
}
805.数组的均值分割
二进制枚举+折半查找。参考ylb大神的题解,其中通过二进制来表示对应的位被枚举出来值得学习。
class Solution {
public boolean splitArraySameAverage(int[] nums) {
int n = nums.length;
if (n == 1) {
return false;
}
int s = Arrays.stream(nums).sum();
for (int i = 0; i < n; ++i) {
nums[i] = nums[i] * n - s;
}
int m = n >> 1;
Set<Integer> vis = new HashSet<>();
// i代表前面几位所有可能的二进制组合情况,j对每种情况的所有位进行检查,如果为1就将它加入和
for (int i = 1; i < 1 << m; ++i) {
int t = 0;
for (int j = 0; j < m; ++j) {
if (((i >> j) & 1) == 1) {
t += nums[j];
}
}
if (t == 0) {
return true;
}
vis.add(t);
}
for (int i = 1; i < 1 << (n - m); ++i) {
int t = 0;
for (int j = 0; j < (n - m); ++j) {
if (((i >> j) & 1) == 1) {
t += nums[m + j];
}
}
if (t == 0 || (i != (1 << (n - m)) - 1) && vis.contains(-t)) {
return true;
}
}
return false;
}
}