【算法】位运算基础知识及应用

C++基本数据类型

类型范围
char1 个字节-128 到 127 或者 0 到 255
unsigned char1 个字节0 到 255
signed char1 个字节-128 到 127
int4 个字节-2147483648 到 2147483647
unsigned int4 个字节0 到 4294967295
signed int4 个字节-2147483648 到 2147483647
short int2 个字节-32768 到 32767
unsigned short int2 个字节0 到 65,535
signed short int2 个字节-32768 到 32767
long int8 个字节-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
signed long int8 个字节-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
unsigned long int8 个字节0 到 18,446,744,073,709,551,615
float4 个字节精度型占4个字节(32位)内存空间,+/- 3.4e +/- 38 (~7 个数字)
double8 个字节双精度型占8 个字节(64位)内存空间,+/- 1.7e +/- 308 (~15 个数字)
long double16 个字节长双精度型 16 个字节(128位)内存空间,可提供18-19位有效数字。
wchar_t2 或 4 个字节1 个宽字符
16位编译器
类型范围
char1 个字节-128 到 127 或者 0 到 255
char(即指针变量)*2
short int2
int2
unsigned int2
float4
double8
long4
long long8
unsigned long4
32位编译器
类型范围
char1 个字节-128 到 127 或者 0 到 255
char(即指针变量)*4
short int2
int4
unsigned int4
float4
double8
long4
long long8
unsigned long4
64位编译器
类型范围
char1 个字节-128 到 127 或者 0 到 255
char(即指针变量)*8
short int2
int4
unsigned int4
float4
double8
long8
long long8
unsigned long8

位运算基础知识

基本运算符有与&、或|、非~、异或^、左移运算<<、右移运算>>。

左移运算

高位丢弃低位补零。

右移运算

对于无符号数,高位补0低位丢弃;对于有符号数,高位补符号位,低位丢弃。

3条基本定理
  1. 任何数&1都是任何数。
1 & 1 = 1
0 & 1 = 0
  1. 任何数|0都是任何数。
1 | 0 = 1
0 | 0 = 0
  1. 任何数^0都是任何数。
1 ^ 0 = 1
0 ^ 0 = 0
常见位运算应用
乘除法
a >> 1 表示除以2
a << 1 表示乘以2
交换两数
a ^= b;
b ^= a;
a ^= b;
判断奇偶数
(a & 1) == 0 表示偶数
(a & 1) == 1 表示奇数
交换符号
(~a) + 1

正数取反+1变成对应的负数,负数取反加一变为其原码,变成对应正数。

+9 0000 1001 
1111 0110
1111 0111 ==> -9
+11 0000 1011
1 111 0100
1111 0101 -11
0000 1010 + 1 = 0000 1011 ==> +11
取绝对值
  1. 方法1
该方法主要是针对32为整型数,将该数向右移动31位,若此时得到的位为1,则表示是负数,否则是正数。
int abs(int a){
    int bit = (a >> 31);
    return bit == 0 ? a : (~a + 1);
}
  1. 方法2(优化之后)

原理就是任何数与0异或都是任何数,任何数与-1异或相当于对原数取反

-1 == 0xffffffff

int abs(int a) {
	int bit = (a >> 31);
    // 上述表达式中32位整型数向右移动31位,正数则为0 负数则为-1  0xffffffff
    return ((a ^ i) - i);
}

405. 数字转换为十六进制数

class Solution {
public:
    string toHex(int num) {
        if(num == 0) return "0";
        string hex_ = "0123456789abcdef";
        int b = (num >> 31);
        unsigned int abs_num = (unsigned)(num ^ b) - b;  // 先求绝对值 负数转换为正数
        unsigned int num_ = num > 0 ? abs_num : ~abs_num + 1; 
        // 要是num是负数 就取反+1 变成一个无符号正数 实际意义上它还是负数
        // 变成无符号正数方便用于计算对应负数的十六进制表示
        // 要是num是正数  就直接用绝对值即可
        cout << num_ <<endl;
        string temp = "";
        while(num_ != 0) {
            temp += hex_[num_ % 16];
            num_ /= 16;
        }
        string ans = "";
        for (int i = temp.size() - 1; i >= 0; i--) {
            ans += temp[i];
        }
        return ans;
    }
};
无符号整数的高低位交换
原始整数为a = 1011 0010 1111 0101
交换前后八位b = 1111 0101 1011 0010
a >> 8 == 0000 0000 1011 0010
a << 8 == 1111 0101 0000 0000
b = (a >> 8) | (a << 8)
二进制逆序
原整数为 a = 1011 0010 1111 0101
逆序之后的二进制整数 b = 1010 1111 0100 1101
二进制逆序的过程中,可以对二进制数进行分组,然后交换顺序,更加方便处理。
  1. 2bit为一组组内高低位交换
01 11 00 01 11 11 10 10
  1. 4bit为一组组内高低位交换
1101 0100 1111 1010
  1. 8bit为一组组内高低位交换
01001101 10101111
  1. 16bit为一组组内高低位交换
1010111101001101

注意,在上述第一步过程中,两两交换高低位比较繁琐,因此可以取奇数位和偶数位出来,其余位用0填充,

奇数位 a_ji = 1010 0010 1010 0000
偶数位 a_ou = 0001 0000 0101 0101
a_ji 右移一位 a_ji >> 1 = 0 101 0001 0101 0000 0(舍弃) # 高位填0 低位舍弃
a_ou 左移一位 a_ou << 1 =0(舍弃) 0010 0000 1010 101 0  # 高位舍弃 低位填0
a_ji | a_ou = 0111 0001 1111 1010
a的奇偶位交换成功

代码表示:

unsigned short a = 0b1011001011110101;
// 取奇数位, a & 0b1010101010101010 ==> a & 0xAAAA;
// 取偶数位, a & 0b0101010101010101 ==> a & 0x5555;
a = ((a & 0xAAAA) >> 1) | ((a & 0x5555) << 1);
a = ((a & 0xCCCC) >> 2) | ((a & 0x3333) << 2);
a = ((a & 0xF0F0) >> 4) | ((a & 0x0F0F) << 4);
a = ((a & 0xFF00) >> 8) | ((a & 0x00FF) << 8);
return a;  // 逆序成功
统计二进制中1的个数
统计二进制中1的个数,最简单的方法就是利用位运算,每次将该数与0x1进行与操作,若结果不为0,说明该位为1,计数器+1,否则该位为0,然后该数右移一位,直到该数为0。但是这样的操作效率并不算高,还有一种就是利用n&(n-1)操作来找到二进制中最右边的一个1,比如n = 1010 0100, 那么n-1 = 1010 0011,那么进行与操作之后 n = 1010 0000,可以看出n最右边的1被消除了,这也是利用了 10...00 - 1 = 01...11的特性,这两个数进行与操作必然为0;因此要想判断二进制中有几个1,只需要看n & (n-1)能进行几次使得n = 0.
int cnt = 0;
while (n) {
    n = n & (n - 1);
    cnt++;
}
return cnt;
扩展应用

整数a和b,将a修改为b需要修改多少个bit,通过异或操作我们知道,两个位相同异或结果为0,否则为1。 因此只需要将a^b的结果传入上述循环即可,求出异或结果中1的个数。

两数之和

异或运算的别名叫不进位加法,那么a、b相异或之后,其结果就是a+b不进位的结果,之后再考虑需要进位的地方,也就是a和b里面都是1的位需要进位,即a & b之后需要进位的地方为1,不需要进位的地方为1,不需要的地方为0, (a & b) << 1即为进位之后的结果,再加上之前异或(不进位)的结果,就得到了最终结果 a + b = (a ^ b) + ((a & b) << 1)。模拟实现代码如下:

int getSum(int a, int b) {
    while (b != 0) { // 不需要进位了
        int a_ = a ^ b;  // 不进位的数
        unsigned int b_ = (unsigned int)(a & b) << 1;
        // 例如 1111 1010 << 1 要是一字节有符号数 则会溢出 若是无符号数 则变成 1111 0100
        // 进位之后的数 可能超出有符号数的范围发生溢出 使用无符号数则可以避免 
        // 无符号数的话 在C++/C内部会直接进行取模操作  而有符号数则不会 视编译器而定
        // 溢出后的数会以2^(8*sizeof(type))作模运算
        a = a_;
        b = b_;
    }
    return a;
}
a = 1010
b = 1111
1. a ^ b = 0101 (a & b) << 1 = 1010 << 1 = 1 0100
2. a ^ b = 1 0001 (a & b) << 1 = 0 0100 << 1 = 0 1000
3. a ^ b = 1 1001 (a & b) << 1 = 0 0000 << 1 = 0 0000
循环结束
2的幂次

若某个数是2的幂次则该数必然大于0,且二进制表示中只有最高位是1,其他位是0.

return n >0 && (n & (n - 1) == 0)
子集枚举
给定一个含不同整数的集合,返回其所有的子集。
S = {1,2,3}

N bit Combination
0 000 {}
1 001 {1}
2 010 {2}
3 011 {1,2}
4 100 {3}
5 101 {1,3}
6 110 {2,3}
7 111 {1,2,3}
class Solution {
public:
    /**
     * @param nums: A set of numbers
     * @return: A list of lists
     */
    vector<vector<int>> subsets(vector<int> &nums) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        vector<vector<int>> ans;
        // 共2^n - 1个子集
        for (int i = 0; i < (1 << n); i++) {
            vector<int> subset;
            for(int j = 0; j < n; j++) {
                if ((i & (1 << j)) != 0) {
                    subset.push_back(nums[j]);
                }
            }
            ans.push_back(subset);
        }
        
        return ans;
    }
};
只出现一次的数字

题目链接

我的题解

T C − O ( n ) S C − O ( 1 ) TC-O(n) SC-O(1) TCO(n)SCO(1)

class Solution {
public:
    
    int singleNumber(vector<int>& nums) {
        int n = nums.size();
        //任何数和0异或不变
        int onceNum = 0;
        for(int i = 0; i < n; i++){
            onceNum ^= nums[i];
        }
        return onceNum;
    }
};
只出现一次的数字 III

题目链接

我的题解
class Solution {
public:
    //TC O(n)  SC-O(1)
    vector<int> singleNumber(vector<int>& nums) {
        int n = nums.size();
        int ans = 0;
        for(int i = 0; i < n ; i++){
            ans ^= nums[i];
        }
        
        //ans是两个不同的数字ab异或之后的结果;
        int x = 1;
        while((x & ans) == 0){
            x <<= 1;//x左移一位
        }//直到找到ans为1的位
        
        int a = 0;
        int b = 0;
        //分组 
        //nums[i]与x相与=1 表示nums[i]该位不为0 而原数组中两个不重复的数在该位一定不同 所以必定分到两个不同的组中  两个组中的元素数目不一定相同 这取决于原数组元素的分布 而相同的数必定会分到同一组中
        for(int i = 0; i < n; i++){
            if(x & nums[i])
                a ^= nums[i];
            else
                b ^= nums[i];
        }
        
        return vector<int>{a,b};
    }
};
UTF-8 编码验证

题目链接

我的题解
class Solution {
public:
    bool validUtf8(vector<int>& data) {
        int n = data.size();
        int flag = 0;
        int cnt = 0;
        for(int i = 0; i < n; i++){
            if(cnt == 0){
                if(data[i] >> 5 == 0b110) cnt = 1;
                else if(data[i] >> 4 == 0b1110) cnt = 2;
                else if(data[i] >> 3 == 0b11110) cnt = 3;//剩余需要考虑的数字
                else if(data[i] >> 7) return false;
            }
            else {
                if(data[i] >> 6 != 0b10) return false;
                --cnt;
            }
        }
        //cnt != 0说明还没考察结束 就退出循环了 说明10xxxx不够 不符合题意
        return cnt == 0;
    }
};
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值