leetcode 位运算

5 篇文章 0 订阅
1 篇文章 0 订阅

力扣

力扣

位运算,学了忘,刷题又学,记不住,可能是没有学到精髓,继续吧!

原码/反码/补码

   反码

  • 如果是正数,原码 = 反码;
  • 如果是负数, 将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 = bb ^ 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的幂的前提下,可以通过位运算的方式求余,在频繁操作对列的情况下可以大大提高效率。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值