位运算,学了忘,刷题又学,记不住,可能是没有学到精髓,继续吧!
原码/反码/补码
反码
:
- 如果是正数,原码 = 反码;
- 如果是负数, 将0变为1,1变为0,符号位固定(这里的符号固定意味不能将符号位的1变为0)
补码
:- 如果是正数,原码 = 反码 = 补码 ;
- 如果是负数, 对反码进行加 1 操作,符号位固定(这里的符号位固定意味着从反码变为补码时,符号位不参与进位操作,从补码变为反码时不参与借位操作)
8的源码: 0000000000000000000000000000 1000
-8的原码:1000000000000000000000000000 1000
-8的反码:11111111111111111111111111110111
-8的补码:11111111111111111111111111111000 * [+3] = [00000000 00000000 00000000 000000011]原 * = [00000000 00000000 00000000 000000011]反 * = [00000000 00000000 00000000 000000011]补 * [-3] = [10000000 00000000 00000000 000000011]原 * = [11111111 11111111 11111111 111111100]反 * = [11111111 11111111 11111111 111111101]补 源码、反码、补码、位运算(及应用) 根据运算法则减去一个正数等于加上一个负数, 即: 1-1 = 1 + (-1) = 0 , 所以机器可以只有加法而没有减法, 这样计算机运算的设计就更简单了. * * 于是人们开始探索 将符号位参与运算, 并且只保留加法的方法. 首先来看原码: * * 计算十进制的表达式: 1-1=0 * * 为了解决原码做减法的问题, 出现了反码: * * 1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0 * * 发现用反码计算减法, 结果的真值部分是正确的. 而唯一的问题其实就出现在"0"这个特殊的数值上. 虽然人们理解上+0和-0是一样的, * 但是0带符号是没有任何意义的. 而且会有[0000 0000]原和[1000 0000]原两个编码表示0. * * 于是补码的出现, 解决了0的符号以及两个编码的问题: * * 8位:1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 * = [0000 0001]补 + [1111 1111]补 // 符号位也参与计算的 * = [0000 0000]补 * =[0000 0000]原 * * 这样0用[0000 0000]表示, 而以前出现问题的-0则不存在了.而且可以用[1000 0000]表示-128: * * (-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]补 + [1000 0001]补 = [1000 0000]补 * * -1-127的结果应该是-128, 在用补码运算的结果中, [1000 0000]补 就是-128. 但是注意因为实际上是使用以前的-0的补码来表示-128, * 所以-128并没有原码和反码表示.(对-128的补码表示[1000 0000]补算出来的原码是[0000 0000]原, 这是不正确的) * * 使用补码, 不仅仅修复了0的符号以及存在两个编码的问题, 而且还能够多表示一个最低数. 这就是为什么8位二进制, * 使用原码或反码表示的范围为[-127, +127], 而使用补码表示的范围为[-128, 127]. * * 因为机器使用补码, 所以对于编程中常用到的32位int类型, 可以表示范围是: [-231, 231-1] 因为第一位表示的是符号位. * 而使用补码表示时又可以多保存一个最小值. * 首先int型在当前的计算机中大多数是占了32位,这里我们为了简便,就当作8位来处理(注意最高位是符号位,也就是8位的数据表示范围是:-128~127), * 只需要满足比其大几位即可。因为计算机都是以补码计算的,计算的最后结果也是补码,计算机以补码保存。但是给用户还得转化为 * 原码(原码->反码->补码->反码->原码)。所以8位、6位、10位、32位.... 应该也是可以的。 * 8位: 1-1 见上面 * 12位:1-1 = 1 + (-1) = [0000 0000 0001]原 + [1000 0000 0001]原 * = [0000 0000 0001]补 + [1111 1111 1111]补 // 符号位也参与计算的 * = [0000 0000 0000]补 * = [0000 0000 0000]原 * 32位:计算也一样 */ /** * 取反~运算符 与 反码是不同的概念。 * 取反:针对补码的所有位取反操作,得到结果补码(计算机存储),最终转换为原码给用户。 * 反码:1. 针对原码的高位不变,其他位取反 * 2. 补码转反码,补码-1就是反码。 * * 计算机计算都是按补码计算的,存储也是补码结果。展示给用户就得转换成原码了。 * 补码转原码:补码高位为0 则补码=反码=原码 * 补码高位为1 则反码=补码-1,原码=反码除高位外,都取反 * 1 -> 2 -> 3 -> 4 -> 5 * 表达式 补码表达式 结果(补码) 反码 原码 原码(10) * 1 & 3 0001 & 0011 0001(高位0正) 0001 0001 1 * 1 & -3 0001 & 1101 0001(高位0正) 0001 0001 1 * 1 ^ 3 0001 ^ 0011 0010(高位0正) 0010 0010 2 * 1 | 3 0001 | 0011 0011(高位0正) 0011 0011 3 * ~1 0001先按位取反得到补码 1110(高位1负) 1101 1010 -2 * 针对计算结果(补码),如果为正数,只需要计算到第二步即可。因为正数 原码==反码==补码 * 如果为负数,则需要计算到第4步。 *
这样的话 1 和 -1 就如下表:
<< >>
移位运算: 逻辑运算 算术运算
- 逻辑移位:移出去的位丢弃,空缺位用"0"填充。
- 算术移位:移出去的位丢弃,空缺位用"符号位"来填充
左移: 将a的二进制整体向左移动N位,超出32位的戒掉,右边不足的用0 来补充。
int A = 12
int C = A << B // C = A * 2^B(如果不溢出)
eg:溢出
INT_MAX // 01111111 11111111 11111111 11111111(补码) = 2147483647
INT_MAX << 1 // 11111111 11111111 11111111 11111110(补码) = -2 (逻辑移位)
UINT_MAX // 11111111 11111111 11111111 11111111 = 4294967295
UINT_MAX << 1 // 11111111 11111111 11111111 11111110 = 4294967294 (逻辑移位)
右移操作
C++ 里面,右移操作和数据类型相关,无符号数是逻辑移位,有符号数是算术移位。
https://leetcode-cn.com/problems/subsets/submissions/https://leetcode-cn.com/problems/subsets/submissions/https://leetcode-cn.com/problems/number-of-1-bits/submissions/
eg:不用加减乘除运算符计算a = b * 9(不考虑溢出)
// 1 分析: a=b*9=b*8+b
8=<<3
=b<<3 +b
// 2 测试: a=4*9=4<<3+4=32+4=36
a=4*6=4<<2+4+4=24
int plusWithBit(int num1, int num2) {
int xor_result = 0;
int and_result = 0;
while (num2) {
xor_result = num1 ^ num2; //不同为1 ,相同为0;
and_result = num1 & num2; // 1 1 1否则为0
num1 = xor_result;
num2 = and_result << 1;
}
return xor_result;
}
int getNumber(int num1) {
return plusWithBit(num1 << 3, num1);
}
int A = 12
int C = A << B // C = A / 2^B
eg:
INT_MIN // 10000000 00000000 00000000 00000001(补码) = -2147483648
INT_MIN >> 1 // 11000000 00000000 00000000 00000000(补码) = -1073741824 (算术移位)
UINT_MAX // 11111111 11111111 11111111 11111111 = 4294967295
UINT_MAX >> 1 // 01111111 11111111 11111111 11111111 = 2147483647 (逻辑移位)
& 都为1 才是1,1 1 =1
0 0 1 0 1 0
& 1 0 1 1 0 0
-------------------
0 0 1 0 0 0
lowbit 方法
11000 ===24
最低位的1 1000=8;
n -n 每一位都取反 加1 ,24 -24
10:01010 (补码,最高位为符号位)
-10:10101(反码,正数的反码就是原码,负数的反码是符号位不变,其余位取反)
-10:10110(补码,正数的补码就是原码,负数的补码是反码+1)
n &= -n; // n中最后一位为1的位为1,其余位为0
0 1 0 1 0
& 1 0 1 1 0
----------------
0 0 0 1 0
11000 & 01000==1
n&(-n)=lowbit; n-lowbit 去除一个1 n==0 的时候停止
| 只要有1 就是1
0 0 1 0 1 0
| 1 0 1 1 0 0
-------------------
1 0 1 1 1 0
eg:消除二进制中最右侧的那个1
//二进制的数据
x=1100;
x-1=1011;
x&(x-1)=1000; // & ==1 1 1
bool cutRithtof1(int n)
{
return n > 0 && (n & (n-1)) == 0;
}
~ a 按位非 1 0互换
0 0 1 0 1 0
~
-------------------
0 1 0 1 0 1
^ 异或-考察最多的
不同为1 否则为0;
0 0 1 0 1 0
^ 1 0 1 1 0 0
-------------------
1 0 0 1 1 0
- 异或运算:
x ^ 0 = x
,x ^ 1 = ~x
- 与运算:
x & 0 = 0
,x & 1 = x
a ^ b ^ b = a b ^ b = 0
不用额外空间使用异或交换2个元素
异或运算的特质:
如果a ^ b = c
,那么a ^ c = b
与b ^ c = a
同时成立,利用这一条,于是交换2个变量的值有以下方法:a = a ^ b b = a ^ b a = a ^ b
a = a + b // new_a = old_a + old_b b = a - b // new_b = new_a - old_b = old_a + old_b - old_b = old_a a = a - b // new_a = new_a - new_b = old_a + old_b - old_a
eg 1
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为汉明重量)。
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。
示例 2:
输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
int hammingWeight(uint32_t n)
{
int cnt=0;
for(int i=0;i<32;i++ )
{
if((n>>i)&1) // 提取最地位
{
cnt++;
}
} // 时间复制度是 32
// return cnt;
// lowbit
int count=0;
while(n){
int lowbit=n&(-n);
count++;
n-=lowbit;
} // 时间复制度是 O(logn)
// return count;
while (n != 0)
{
n = n & (n - 1);
count++;
}
return count;
}
eg2:力扣
给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
示例 1:
输入:nums = [2,2,3,2]
输出:3
int singleNumber(vector<int>& nums)
{
// 第一种方法 哈希
// unordered_map<int,int> map;
// for(int n:nums) map[n]++;
// for(const auto& [num,count]:map)
// {
// if(count==1) return num;
// }
// return -1;
// 第二 中 位运算
int ones = 0, twos = 0;
for(int num : nums){
ones = ones ^ num & ~twos;
twos = twos ^ num & ~ones;
}
return ones;
}
eg3
数组中 只有一个数字出现一次,其他的都出现了两次,找出这个数
vector<int> ans;
unordered_map<int, int> freq;
for (int num: nums)
{
++freq[num];
}
for (const auto& [num, occ]: freq) {
if (occ == 1) {
ans.push_back(num);
}
}
return ans;
给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。
unordered_map<int,int> map;
for(int n:nums) map[n]++;
for(const auto& [num,count]:map)
{
if(count==1) return num;
}
return -1;
eg;力扣https://leetcode-cn.com/problems/count-triplets-that-can-form-two-arrays-of-equal-xor/
eg5 :力扣https://leetcode-cn.com/problems/number-complement/submissions/
eg:力扣https://leetcode-cn.com/problems/single-number/力扣
https://leetcode-cn.com/problems/bitwise-and-of-numbers-range/
int rangeBitwiseAnd(int left, int right)
{
int ret=0;
for(int i=30;i>=0;i--)
{
int bl=(left>>i)&1;
int br=(right>>i)&1;
if(bl&&br) // 拿到的位数都为1
{
ret+=(1<<i);
}else if(!bl&&br)
{
break;
}
}
return ret;
}
eg:给你一个整数数组 nums
,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。力扣https://leetcode-cn.com/problems/single-number-ii/
// 用到数位的思想, 按照位 计算
int singleNumber(vector<int>& nums)
{
int ret =0;
//按照位处理
for(int i=0;i<32;i++)
{
// 统计这一位 0,1 的数量(0 的数量其实是可以不同管的)
int cnt=0;
for(int x:nums){
int b=(x>>i)&1;//拿到为位
cnt +=b;
}
if(cnt%3)
{
ret+=(1<<i);
}
}
return ret;
}
eg: 给定一个整数数组 nums
,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。力扣https://leetcode-cn.com/problems/single-number-iii/
vector<int> singleNumber(vector<int>& nums)
{
// 1 第1种 方法
// vector<int> res;
// unordered_map<int, int> freq;
// for (int num: nums)
// {
// ++freq[num];
// }
// for (const auto& [num, occ]: freq) {
// if (occ == 1) {
// res.push_back(num);
// }
// }
// return res;
//
// 第二种方法
// 1 将a b 分离开来 ,二相同的数据必须将他们放到同一组里面
vector<int> res;
// 枚举32位 找到1 的数量
int s =0;// 所有元素的异或结果
for(int n:nums)
{
s^=n;
}
// s i = 1 原序列当中,给位有 奇数个1
int pos=-1;
for(int i=0;i<32;i++)
{
if((s>>i)&1) //拿到的是1
{
pos=i;
break;
}
}
// 分组
int x=0;
int y=0;
for(int n:nums)
{
int m=(n>>pos)&1;
if(!m){
x^=n;
}else{
y^=n;
}
}
return {x,y};
}
eg 给你一个整数数组 nums
,返回 nums[i] XOR nums[j]
的最大运算结果,其中 0 ≤ i ≤ j < n
。
class Solution {
public:
// 字典树
typedef struct Node
{
int son[2];
Node(){
son[0]=-1;
son[1]=-1;
}
}Node;
// int b2=1^b ====1-b;
vector<Node> nodes;
void insert(int x)
{
int id=0;
for(int i=31;i>=0;i--)
{
int b=(x>>i)&1;
if(nodes[id].son[b]==-1)
{
nodes.push_back({});
nodes[id].son[b] = nodes.size()-1;
}
id=nodes[id].son[b];
}
}
int query(int x)
{
int ret=0;
int id=0;
for(int i=31;i>=0;i--)
{
int b=(x>>i)&1;
int b2=1^b;// 1-b
if(nodes[id].son[b2]!=-1){
id=nodes[id].son[b2];
ret+=(1<<i);
}else{
id=nodes[id].son[b];
}
}
return ret;
}
int findMaximumXOR(vector<int>& nums)
{
// 从高位到地位考虑
if(nums.empty()) return 0;
nodes.push_back({});
insert(nums.back());
int ret=0;
for(int i=nums.size()-2;i>=0;i--){
int ans=query(nums[i]);
ret =max(ret,ans);
insert(nums[i]);
}
return ret;
}
};
技巧4:循环队列的buffer size 为什么需要保证为2的幂?
为什么需要保证 buffer size 为2的幂?
因为通常循环队列的入队和出队操作要不断的对size进行求余, 为了提高效率,将 buffer size 扩展为2的幂,就可以使用位运算。kfifo->in % kfiifo->size 等同于 kfifo->in & (kfifo->size – 1)。
假设现在size 为16:
8 & (size - 1) = 01000 & 01111 = 01000 = 8
15 & (size -1) = 01111 & 01111 = 01111 = 15
16 & (size - 1) = 10000 & 01111 = 00000 = 0
26 & (size - 1 ) = 11010 & 01111 = 01010 = 10
所以保证size是2的幂的前提下,可以通过位运算的方式求余,在频繁操作对列的情况下可以大大提高效率。