【LeetCode】位运算精选9题

目录

01. 按位取反操作符 ~

02. 移位操作符

02.1 左移操作符 <<

1 << n = 2的n次幂

02.2 右移操作符 >>

n >> 1 和 n / 2    非负数和负偶数时二者等价

03. 位操作符

03.1 按位与操作符 &

n >> i & 1    获取二进制的i位

n & 1 和 n % 2    非负数时二者等价

n & ~ (1 << i)    将二进制的i位变为0

n & n - 1    将二进制最右边的1变为0

n & -n    提取二进制最右边的1

03.2 按位或操作符 |

n | 1 << i    将二进制的i位变为1

03.3 按位异或操作符 ^

a ^ a = 0    0 ^ a = a

04. 位图

基础位运算:

1. 比特位计数(简单)

2. 丢失的数字(简单)

3. 只出现一次的数字(简单)

4. 只出现一次的数字 II(中等)

5. 只出现一次的数字 III(中等)

6. 消失的两个数字(困难)

7. 两整数之和(中等)

位图:

1. 判断字符是否唯一(简单)

2. 最大单词长度乘积(中等)


整数的二进制表示有3种:原码、反码、补码。正整数的原码、反码、补码相同。负整数的反码是原码符号位不变,其他位按位取反,补码是反码+1。整数在内存中存储的是补码。

+7:
原码:00000000000000000000000000000111
反码:00000000000000000000000000000111
补码:00000000000000000000000000000111
-7:
原码:10000000000000000000000000000111
反码:11111111111111111111111111111000
补码:11111111111111111111111111111001

操作符的优先级:

() = [] = 结构成员访问操作符 > 单目操作符 > 算术操作符 > 移位操作符 > 关系操作符 > 位操作符 > 逻辑操作符 > ?: > 赋值操作符 > ,(逗号)

如果记不住,写表达式的时候,能加括号就加括号。

01. 按位取反操作符 ~

~是一个单目操作符,表示对一个数的二进制的每一位按位取反,即0变成1,1变成0。

int a = 7;
int b = ~a;
// a的补码:00000000000000000000000000000111
// b的补码:11111111111111111111111111111000
int a = -7;
int b = ~a;
// a的补码:11111111111111111111111111111001
// b的补码:00000000000000000000000000000110

02. 移位操作符

02.1 左移操作符 <<

移位规则:左边抛弃,右边补0

int a = 7;
int b = a << 1;

0[00000000000000000000000000001110]

int a = -7;
int b = a << 1;

1[11111111111111111111111111110010]

1 << n = 2的n次幂

int a = 1;
int b = a << 1;

0[00000000000000000000000000000010] = 2的1次幂

int b = a << 2;

00[00000000000000000000000000000100] = 2的2次幂

int b = a << 3;

000[00000000000000000000000000001000] = 2的3次幂

02.2 右移操作符 >>

移位规则:

  • 逻辑移位:左边补0,右边抛弃
  • 算术移位:左边补原符号位,右边抛弃(常见编译器都是算术移位)

算术移位:

int a = 7;
int b = a >> 1;

[00000000000000000000000000000011]1

int a = -7;
int b = a >> 1;

[11111111111111111111111111111100]1

n >> 1 和 n / 2    非负数和负偶数时二者等价

n为非负数时,n >> 1 等价于 n / 2。

10 >> 1 = 5        10 / 2 = 5        0 >> 1 = 0        0 / 2 = 0

n为负偶数时,n >> 1 等价于 n / 2。

-10 >> 1 = -5        -10 / 2 = -5

n为负奇数时,n >> 1 = n / 2 - 1(n >> 1是n除以2向下取整,n / 2直接丢弃小数部分)。

-5 >> 1 = -3        -5 / 2 = -2

03. 位操作符

&按位与,|按位或,^按位异或

位操作符通过逐位比较两个运算对象,生成一个新值。对于每个位:

  • &:两个操作数相应的位都为1,结果为1(有0则0,记忆法:&看起来像0)
  • | :两个操作数相应的位至少有一个为1,结果为1(有1则1,记忆法:|看起来像1)
  • ^:两个操作数相应的位相同为0,相异为1(无进位加法

03.1 按位与操作符 &

int a = 3;
int b = -5;
int c = a & b;
// a的补码:00000000000000000000000000000011
// b的补码:11111111111111111111111111111011
// c的补码:00000000000000000000000000000011

n >> i & 1    获取二进制的i位

一个数n,假设它的二进制的倒数第一位称为0位,倒数第二位称为1位……想要获取i位,就要在i位上& 1。

以75(000000000000000000000001001011)为例,

要想获取0位,就要在0位上& 1:

 000000000000000000000001001011
&000000000000000000000000000001
=000000000000000000000000000001
=1

要想获取1位,就要在1位上& 1,如果直接& 10(二进制),得到的结果是10(二进制),这个结果并不是我们想要的,我们想要的结果是1。所以把要把0位去掉,再& 1。

先>> 1,再& 1:

75 >> 1 = [000000000000000000000000100101]1

 000000000000000000000000100101
&000000000000000000000000000001
=000000000000000000000000000001
=1

同理,要想获取2位,就要先>> 2,再& 1:

75 >> 2 = [000000000000000000000000010010]11

 000000000000000000000000010010
&000000000000000000000000000001
=000000000000000000000000000000
=0

n & 1 和 n % 2    非负数时二者等价

显然,n & 1表示获取n的二进制的最后一位,奇数的二进制的最后一位一定是1,偶数的二进制的最后一位一定是0。所以,当n为非负数时,n & 1和n % 2等价;当n为负数时,由于取模运算保留符号,n % 2 = -1,而n & 1 = 1。

n为非负数时,n & 1等价于n % 2

  • n为奇数:n & 1 = 1  n % 2 = 1
  • n为偶数:n & 1 = 0  n % 2 = 0

n为负数时,

  • n为奇数:n & 1 = 1  n % 2 = -1
  • n为偶数:n & 1 = 0  n % 2 = 0

n & ~ (1 << i)    将二进制的i位变为0

一个数n,假设它的二进制的倒数第一位称为0位,倒数第二位称为1位……想要将i位修改成0,就要在i位上& 0,其他位都& 1。

以75(000000000000000000000001001011)为例,

要将3位修改成0,就要在3位上& 0,其他位都& 1:

 000000000000000000000001001011
&111111111111111111111111110111
=000000000000000000000001000011

n & n - 1    将二进制最右边的1变为0

n - 1表示将n的二进制最右边的1的右侧区域(包括1)全部按位取反。

n & (n - 1)表示将n的二进制最右边的1变为0,其余位不变。

24:        00000000000000000000000000011000

23:        00000000000000000000000000010111

24 & 23:00000000000000000000000000010000

如果n & (n - 1)为0,则n是2的幂。

2:      00000000000000000000000000000010

1:      00000000000000000000000000000001

4:      00000000000000000000000000000100

3:      00000000000000000000000000000011

2 & 1:00000000000000000000000000000000

4 & 3:00000000000000000000000000000000

n & -n    提取二进制最右边的1

-n表示将n的二进制最右边的1的左侧区域(不包括1)全部按位取反。

+20:       00000000000000000000000000010100

-20:        11111111111111111111111111101100

20 & -20:00000000000000000000000000000100

03.2 按位或操作符 |

int a = 3;
int b = -5;
int c = a | b;
// a的补码:00000000000000000000000000000011
// b的补码:11111111111111111111111111111011
// c的补码:11111111111111111111111111111011

n | 1 << i    将二进制的i位变为1

一个数n,假设它的二进制的倒数第一位称为0位,倒数第二位称为1位……想要将i位修改成1,就要在i位上| 1,其他位都| 0。

以75(000000000000000000000001001011)为例,

要将2位修改成1,就要在2位上| 1,其他位都| 0:

 000000000000000000000001001011
|000000000000000000000000000100
=000000000000000000000001001111

03.3 按位异或操作符 ^

int a = 3;
int b = -5;
int c = a ^ b;
// a的补码:00000000000000000000000000000011
// b的补码:11111111111111111111111111111011
// c的补码:11111111111111111111111111111000

异或运算支持交换律和结合律,即:

a ^ b = b ^ a        (a ^ b) ^ c = a ^ (b ^ c)

a ^ a = 0    0 ^ a = a

a ^ a = 0:

如,3 ^ 3 = 00000011 ^ 00000011 = 00000000

0 ^ a = a:

如,0 ^ 3 = 00000000 ^ 00000011 = 00000011

不创建临时变量实现两个数的交换:

#include <stdio.h>

int main()
{
	int a = 10;
	int b = 20;
	a = a ^ b; // 10^20
	b = a ^ b; // 10^20^20=10^0=10
	a = a ^ b; // 10^20^10=0^20=20
	printf("a = %d b = %d\n", a, b);
	return 0;
}

04. 位图

数据是否存在是两种状态,可以用一个比特位表示这种状态,1表示存在,0表示不存在。

位图就是哈希表直接定址法的变形。

用位图表示{ 1,3,7,4,12,27,30,13,22,18 }中的元素是否存在:

可以创建一个int类型的变量作为位图,

基础位运算:

1. 比特位计数(简单)

方法一:

class Solution {
public:
    vector<int> countBits(int n) {
        vector<int> ans(n + 1, 0);
        // 0的二进制中没有1,现在ans[0]已经为0,所以可以从1开始计算
        for (int i = 1; i <= n; i++)
        {
            int n = i;
            while (n)
            {
                // 每进行一次n&n-1操作,就能将最右边的1变为0,去掉所有1时(n为0时)结束循环
                // 二进制1的个数=循环的次数
                ans[i]++;
                n &= n - 1;
            }
        }
        return ans;
    }
};

如果一个整数共有k位,对于每个整数,while循环最多循环k次,每次循环的时间复杂度为O(1),因此上述代码的时间复杂度为O(kn)。

方法二:

class Solution {
public:
    vector<int> countBits(int n) {
        vector<int> ans(n + 1, 0);
        // 0的二进制中没有1,现在ans[0]已经为0,所以可以从1开始计算
        for (int i = 1; i <= n; i++)
        {
            // i的二进制1的个数比i&i-1的二进制1的个数多1个
            ans[i] = ans[i & i - 1] + 1;
        }
        return ans;
    }
};

动态规划的思想。上述代码的时间复杂度为O(n)。

方法三:

如果正整数i是偶数,那么i相当于将i / 2左移一位的结果,因此偶数i和i / 2的二进制形式中1的个数是相同的。

如果正整数i是奇数,那么i相当于将i / 2左移一位后,再将最右边一位设为1的结果,因此奇数i的二进制形式中1的个数比i / 2的二进制形式中1的个数多1个。

例如,

3:00000000000000000000000000000011

6:00000000000000000000000000000110

7:00000000000000000000000000000111

class Solution {
public:
    vector<int> countBits(int n) {
        vector<int> ans(n + 1, 0);
        // 0的二进制形式中没有1,现在ret[0]的值已经为0,所以可以从1开始计算
        for (int i = 1; i <= n; i++)
        {
            // ret[i] = ret[i / 2] + (i % 2);
            // 当i为非负数时,i/2=i>>1,i%2=i&1,且位运算比除法运算和取余运算效率高
            // 代码优化如下:
            ans[i] = ans[i >> 1] + (i & 1);
        }
        return ans;
    }
};

上述代码的时间复杂度为O(n)。

2. 丢失的数字(简单)

class Solution {
public:
    int missingNumber(vector<int>& nums) {
        // 0^(3^0^1)^(0^1^2^3)=2
        int n = nums.size();
        int ans = 0;
        for (auto& e : nums)
        {
            ans ^= e;
        }
        for (int i = 0; i <= n; i++)
        {
            ans ^= i;
        }
        return ans;
    }
};

3. 只出现一次的数字(简单)

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        // 0^(2^2^1)=1
        int ans = 0;
        for (auto& e : nums)
        {
            ans ^= e;
        }
        return ans;
    }
};

4. 只出现一次的数字 II(中等)

将数组中只出现一次的元素剔除,剩下的元素都出现了三次,将这些元素的各个数位的值相加,各个数位的和一定是3的倍数。

将数组中所有元素的各个数位的值相加:

  • 如果某位的和能被3整除,说明只出现1次的元素该位是0
  • 如果某位的和被3除余1,说明只出现1次的元素该位是1
class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int ans = 0; // 只出现一次的数
        // 依次修改ans的每一位
        for (int i = 0; i < 32; i++)
        {
            int sum = 0;
            // 计算所有元素的二进制i位的和
            for (auto& e : nums)
            {
                if ((e >> i & 1) == 1)
                {
                    sum++;
                }
            }
            // 如果i位的和被3除余1,说明ans的i位是1
            if (sum % 3 == 1)
            {
                ans |= 1 << i;
            }
        }
        return ans;
    }
};

5. 只出现一次的数字 III(中等)

假设只出现一次的元素是a和b,将数组所有元素异或在一起,结果是a ^ b,显然结果的二进制一定有1。假设结果的二进制的i位为1,根据这个条件将数组所有元素分为两个组:一组元素的二进制的i位为1,另一组元素的二进制的i位为0,那么a和b一定会分到不同的组中。分别将每个组的元素都异或在一起,会分别得到a和b。

class Solution {
public:
    vector<int> singleNumber(vector<int>& nums) {
        // 将数组所有元素异或在一起
        int tmp= 0;
        for (auto& e : nums)
        {
            tmp ^= e;
        }

        // 提取tmp的二进制的最右边的1,假设是i位
        // 如果tmp为INT_MIN,那么-tmp会溢出,所以要把它转化为无符号整型
        int lowbit = tmp & -(unsigned int)tmp;

        // 根据i位的不同,将元素分成两组
        int a = 0;
        int b = 0;
        for (auto& e : nums)
        {
            if (e & lowbit) // 这组i位为1
            {
                a ^= e;
            }
            else // 这组i位为0
            {
                b ^= e;
            }
        }
        return { a,b };
    }
};

6. 消失的两个数字(困难)

给定一个数组,包含从1到N所有的整数,但其中缺了两个数字。如果把1到N所有的整数再算进去,问题就转化成:给定一个数组,包含从1到N所有的整数,其中恰好有两个元素只出现一次,其余所有元素均出现两次。和上一题“只出现一次的数字 III”类似。

class Solution {
public:
    vector<int> missingTwo(vector<int>& nums) {
        // 将数组所有元素和1~N所有整数都异或在一起
        int tmp= 0;
        for (auto& e : nums)
        {
            tmp ^= e;
        }
        int N = nums.size() + 2;
        for (int i = 1; i <= N; i++)
        {
            tmp ^= i;
        }

        // 提取tmp的二进制的最右边的1,假设是x位
        int lowbit = tmp & -tmp;

        // 根据x位的不同,将数组所有元素和1~N所有整数都分成两组
        int a = 0;
        int b = 0;
        for (auto& e : nums)
        {
            if (e & lowbit) // 这组x位为1
            {
                a ^= e;
            }
            else // 这组x位为0
            {
                b ^= e;
            }
        }
        for (int i = 1; i <= N; i++)
        {
            if (i & lowbit) // 这组x位为1
            {
                a ^= i;
            }
            else // 这组x位为0
            {
                b ^= i;
            }
        }
        return { a,b };
    }
};

7. 两整数之和(中等)

^:两个操作数相应的位相同为0,相异为1,可以理解为无进位加法。

&:两个操作数相应的位都为1,结果为1,可以理解为获得进位。

a + b = (a ^ b) + (a & b) << 1

显然,可以用递归:

class Solution {
public:
    int getSum(int a, int b) {
        if (b == 0)
            return a;
        return getSum(a ^ b, (a & b) << 1);
    }
};

也可以用迭代:

class Solution {
public:
    int getSum(int a, int b) {
        while (b)
        {
            int carry = (a & b) << 1; // 进位结果
            a = a ^ b; // 无进位加法结果
            b = carry;
        }
        return a;
    }
};

位图:

1. 判断字符是否唯一(简单)

可以创建一个int类型的变量作为位图,假设它的二进制的倒数第一位称为0位,倒数第二位称为1位……如果出现小写字母'a',就把0位变为1,如果出现小写字母'b',就把1位变为1……

class Solution {
public:
    bool isUnique(string astr) {
        // 鸽巢原理
        if (astr.size() > 26)
            return false;

        int bitset = 0; // 位图
        for (auto& ch : astr)
        {
            int i = ch - 'a'; // 字母在位图中对应的位置
            if ((bitset >> i & 1) == 1) // 如果字母已经出现过
                return false;
            bitset |= 1 << i; // 将i位变为1
        }
        return true;
    }
};

2. 最大单词长度乘积(中等)

创建元素为int类型的bitsets数组,每个元素都是位图。bitsets[i]记录words[i]出现的字符,如果flags[i] & flags[j] == 0,那么words[i]和words[j]没有相同字符,此时可以计算它们长度的乘积,然后和之前计算过的最大值比较,取较大的。

class Solution {
public:
    int maxProduct(vector<string>& words) {
        int n = words.size();
        vector<int> bitsets(n, 0); // bitsets[i]记录words[i]出现的字符
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < words[i].size(); j++)
            {
                int x = words[i][j] - 'a'; // 字母在位图中对应的位置
                bitsets[i] |= 1 << x; // 将bitsets[i]的x位变为1
            }
        }
        // 如果bitsets[i]&bitsets[j]==0,那么words[i]和words[j]没有相同字符
        int ans = 0;
        for (int i = 0; i < n; i++)
        {
            for (int j = i + 1; j < n; j++)
            {
                if ((bitsets[i] & bitsets[j]) == 0)
                {
                    int prod = words[i].size() * words[j].size();
                    ans = max(ans, prod);
                }
            }
        }
        return ans;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值