位运算刷题总结
4628
2021.12.03
2021.12.09
发布于 未知归属地

前言

位运算就是和加减乘除等算术运算一样基础的逻辑运算,包括与或非,异或和移位。虽然非常基础,不过在日常的变成中使用的场景却不多,主要是技巧性太强,而且可读性差。即使在汇编编程中,位运算使用场景比较广的也是接口编程等比较专业的地方。但是,在底层编程,标准库等场景中,位运算还是有着不小的应用场景。另外在状态压缩,大数处理,甚至面试中,也少不了它的声音。故位运算还是值得我们去学习的,这篇文章就是我学习位运算以及刷题的一些记录。

常见应用

浮点数不能位运算

基本

基本操作原则

等价:

a=b^c

b=a^c

c=a^b

0^0=0

0^1=1

1^0=1

1^1=0

x^y=y^x

(x^y)^z=x^(y^z)

0^x=x

1^x=~x

x^x=0

位查询与修改

获取某位

// 获取 x 的第 pos 位
int getBit(int x,int pos) {
	return (x>>pos)&1;
}

// 获取从 pos 开始的 cnt 位
// 原理也是先右移 pos 位,找到起始点,然后取 cnt 位,就是把和一个数相与,即 cnt 个 1,
// 32-cnt 个数的0,这个数用 (1<<cnt)-1 得出,即先左移 cnt 位,如 1000000,再减一就是
// 0111111
int getBits(int x,int pos,int cnt) {
	return (x>>pos)&((1<<cnt) -1));
}

可以看出来,与的作用主要就是清零,然后取某些位的作用。而与的数就是 mask 掩码。

某位置 1

// 将 x 的第 pos 位设置为 1
int setBit(int x,int pox) {
	return x | (1<<pos);
}

设置 1 也很简单,就是对应位与 1 相与即可。可以看出来,或运算的主要功能就是置 1.

某位清零

// 将 x 的第 pos 位设置为 0
int unsetBit(int x,int pos) {
	return x & ~(1<<pos);
}

设置 0 和设置 1相反,置1用或运算,而清零则用与运算,并且掩码也不一样。置1是 000100 这样只有一个1,而清零则是 11110111 这样只有一个 0.

掩码直接移位取反即可。

某位取反

// 将 x 的第 pox 位取反
int flapBit(int x,int pox) {
	return x ^ (1 << pox);
}

取反特定位则与清零和置一不同,用到的是异或.由于 x^0=x,x^1=~x。故我们只要掩码设为 00001000,和置 1 类似,只是变成相异或,就能设置 1 的对应位取反。

找到最低位的 1

x&(-x)=x&(~x+1). 1会和最低的 1 开始一直进位,知道为 0.最后结果就是最低位 1 的位置。而其他则会被清零。

int lowbit(int x){
	return x&(-x);
}

消除最后一个 1

和找到最低位 1 类似

int clear(int x){
	return x&(x-1);
}

数学性质

判断两数是否异号

即判断两个是是否都是正数或是负数,或者一负一正。原理很简单,最高位异或,然后看最高位是不是 1,即结果是不是小于 0 即可。

bool isOppositeSign(int x,int y){
	return (x^y)<0;
}

判断奇偶

最后一位是 0 还是 1,因此就是取某一位判断。

bool isEven(int x){
	return (x&1)==0;
}

交换两数

异或的基本原理,面试的时候比较常见,不过真正使用频率并不高。

void swap(int x,int y){
	x^=y;
	y^=x;
	x^=y;
}

自增、自减

原理就是 - 的底层操作,- 的原理就是取反加一,故 -(~a) 就是 ~(~a)+1=a+1. (-a)=(~a+1)=a-1.

int inc(int x){
	return -(~x);
}
    
int dec(int x){
	return ~(-y);
}

判断是否是 2 的指数

如果是 2 的指数,如 2,4,8 ,那么就必然只有一个 1.如 0001,0010,0100.那么我们清空最低位 1,再判断是否为 0 即可。

bool isTowExponent(int n) {
	return (n&(n-1))==0;
}

求相反数

求补,取反+1

int reverseSign(int n) {
	return (~n)+1;
}

求绝对值

判断正负

int abs(int n) {
	int s=(n>>31);
	return (s==0)?n:(~n+1);		
}

// 优化
int abs(int n) {
	int s=(n>>31);
	return (n^s)-s;
}

位数逆转

循环移位

c++ 没有循环移位功能,不过可以使用移位和或运算模拟,如向右循环移位 n 位

int ror(int x,int n) {
	x=(x>>n)|(x<<(32-n));
}

int rol(int x,int n) {
	x=(x<<n)|(x>>(32-n));
}

高低位交换

int 32 位,高低 16 位交换位置,就是左移 16 位,右移 16 位,然后两者相与

int reverseLH(int n) {
	return (n << 16) | (n >> 16);
}

二进制逆转

取 a 的奇数位并用 0 进行填充可以表示为:a & 0xAAAA 取 a 的偶数为并用 0 进行填充可以表示为:a & 0x5555 因此,上面的第一步可以表示为:a = ((a & 0xAAAA) >> 1) | ((a & 0x5555) << 1)

int reverseInt(int n) {
	n = ((n & 0xAAAA) >> 1) | ((n & 0x5555) << 1);
	n = ((n & 0xCCCC) >> 2) | ((n & 0x3333) << 2);
	n = ((n & 0xF0F0) >> 4) | ((n & 0x0F0F) << 4);
	n = ((n & 0xFF00) >> 8) | ((n & 0x00FF) << 8);
	return n;
}

集合操作

模拟集合操作

每一位表示一个数字,1 表示在集合中,0 表示不在集合中。

image.png

其它

大小写转换

根据字符 ASCII 码之间的关系

// 转换为大写
char toUpper(char c){
	return c&'_';
}

// 转换为小写
char toLower(char c){
	return c|' ';
}

// 大小写相互转换
char exchage(char c){
	return c^'c';
}

求 1 的个数

求 1 的个数就是求各位的和

int bitCount_3(u32 x) {
    x-= ((x & 0xAAAAAAAAu) >> 1);
    x = ((x & 0xCCCCCCCCu) >> 2) + (x & 0x33333333u);
    x = ((x >> 4) + x) & 0x0F0F0F0Fu;
    x = (x * 0x01010101u) >> 24;
    return x;
}

求前缀、后缀 1 的个数

int countLeadingZeros_1(u32 x) {
    int ans = 0;
    if (x >> 16) x >>= 16; else ans |= 16;
    if (x >> 8) x >>= 8; else ans |= 8;
    if (x >> 4) x >>= 4; else ans |= 4;
    if (x >> 2) x >>= 2; else ans |= 2;
    if (x >> 1) x >>= 1; else ans |= 1;
    return ans + !x;;
}

int countTrailingZeros_1(u32 x) {
    int ans = 0;
    if (!(x & 65535u)) x >>= 16, ans |= 16;
    if (!(x & 255u )) x >>= 8, ans |= 8;
    if (!(x & 15u )) x >>= 4, ans |= 4;
    if (!(x & 3u )) x >>= 2, ans |= 2;
    if (!(x & 1u )) x >>= 1, ans |= 1;
    return ans + !x;
}

实现三目表达式

我们可以利用表达式 y ^ ((x ^ y) & -cond) 来实现选择的功能:

若 cond = 1,其结果为 x

若 cond = 0,其结果为 y

int branch(bool cond,int x,int y){
	return y ^ ((x ^ y) & -cond);
}

0 和 1 转换

0 即 00000000,1 即 000000001,

0^1=000000000^000000001=000000001=1

1^1=000000001^000000001=000000000=0

int exchage(int n){
	return n^1;
}

构造低位全 1 的掩码

如 5 即 0101 → 000000ffh

int getMask(int x){
	x|=x>>1;
	x|=x>>2;
	x|=x>>4;
	x|=x>>8;
	x|=x>>16;	
	return x;
}

题型

编码性质

编码性质是指使用位运算处理一些编码上的问题,比如数的表示,循环码,字符编码等等。

  • list

    476. 数字的补数

    题意

    将数低位取反

    分析

    最简单的自然是从左边遍历,找到第一个 1,然后开始取反,就是个简单的获取设置位操作。

    优化

    要取反的话,其实和 1 异或就是,0^1=1,1^1=0.故我们只要构造掩码,即低位全 1,然后异或即可。

    效率

    时间复杂度:,掩码是

    空间复杂度:

    代码

    遍历模拟

    class Solution {
    public:
        int findComplement(int num) {
            int cnt=0;
            bool flag=false;
            
            for (int i=31;i>-1;i--){
                int b=getBit(num,i);
                    
                if (b==0 && !flag) {
                    continue;
                }
                if (b==1 && !flag){
                    flag=true;
                }
                
                num=flapBit(num,i);
            }
            
            return num;
        }
        
        int setBit(int x,int pos){
            return x|(1<<pos);
        }
        
        int unsetBit(int x,int pos){
            return x&(~(1<<pos));
        }
        
        int getBit(int x,int pos) {
            return (x>>pos)&1;
        }
        
        int flapBit(int x,int pos){
            return x^(1<<pos);
        }
    };

    掩码异或(取反)

    class Solution {
    public:
        int findComplement(int num) {
            return getMask(num)^num;
        }
        
        int getMask(int x){
    	    x|=x>>1;
    	    x|=x>>2;
    	    x|=x>>4;
    	    x|=x>>8;
    	    x|=x>>16;	
    	    return x;
        }
    };

    1009. 十进制整数的反码

    同 476.

    打印补码的最值

    题意

    打印补码的最值(范围)

    分析

    这是 csapp 的一个 lab,有助于我们理解补码的性质。

    10000000 00000000 00000000 00000000 → INT_MIN 即

    00000000 00000000 00000000 00000000 → 0

    01111111 11111111 11111111 11111111 → INT_MAX 即

    void show range() {
    	cout << (~(1<<31))<<endl;
    	out << 0 <<endl;
    	cout << (1<<31)<<endl;
    }
    
    void showAll() {
    	cout << bitset<32> (INT_MIN) << endl;
    	cout << bitset<32> (-1) << endl;
    	cout << bitset<32> (0) << endl;
    	out << bitset<32> (1) << endl;
      cout << bitset<32> (INT_MAX) << endl;
    }
    
    10000000000000000000000000000000
    11111111111111111111111111111111
    00000000000000000000000000000000
    00000000000000000000000000000001
    01111111111111111111111111111111

    89. 格雷编码

    题意

    生成格雷编码

    分析

    使用数电中生成格雷编码的算法,n 位到 n+1 的过程是:

    1. 先镜像对称增加原数组
    2. 再把新增加的数高一位置 1

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int unsetBit(int x, int pos) {
            return x & (~(1 << pos));
        }
    
        int setBit(int x, int pos) {
            return x | (1 << pos);
        }
    
        int flipBit(int x, int pos) {
            return x ^ (1 << pos);
        }
    
        vector<int> grayCode(int n) {
            if (n == 1) {
                return {0, 1};
            }
    
            vector<int> nums = grayCode(n - 1);
            int m = nums.size();
            int t;
    
            for (int i = 0; i < m; i++) {
                t = nums[m - i - 1];
                t = setBit(t, n - 1);
                nums.push_back(t);
            }
    
            return nums;
        }
    };

    1238. 循环码排列

    题意

    生成格雷码再反转

    分析

    生成格雷码,低位镜像对称,高位全置 1 即可。然后找到 start 位置,进行局部反转后整体反转即可。

    生成格雷码的过程可参考 89 题。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
    
        int unsetBit(int x, int pos) {
            return x & (~(1 << pos));
        }
    
        int setBit(int x, int pos) {
            return x | (1 << pos);
        }
    
        int flipBit(int x, int pos) {
            return x ^ (1 << pos);
        }
    
        vector<int> getGayCode(int n) {
            if (n == 1) {
                return {0, 1};
            }
    
            vector<int> nums = getGayCode(n - 1);
            int m = nums.size();
            int t;
    
            for (int i = 0; i < m; i++) {
                t = nums[m - i - 1];
                t = setBit(t, n - 1);
                nums.push_back(t);
            }
    
            return nums;
        }
    
        vector<int> circularPermutation(int n, int start) {
            vector<int> nums = getGayCode(n);
            int index;
    
            for (int i = 0; i < nums.size(); i++) {
                if (nums[i] == start) {
                    index = i;
                }
            }
    
            reverse(nums.begin(), nums.begin() + index);
            reverse(nums.begin() + index, nums.end());
            reverse(nums.begin(), nums.end());
    
            return nums;
        }
    
    };

    393. UTF-8 编码验证

    题意

    给定的是一连串 byte 数组,验证是不是 UTF-8 编码的字符集(可能有多个字符)

    分析

    其实这题是一个典型的 DFA 题型,每个字节输入有不同的状态,即首字节和非首字节,非首字节又有第几字节几种情况。

    不过这题状态较少,直接模拟也能写出来,就是 DFA 的跳转表写法。

    由于输入字节主要有两个状态,故我们需要一个计数器表示当前输入字节是哪个状态:

    1. cnt 为 0 表示输入的是第一个字节
    2. cnt 不为 0 表示输入的是后面的字节

    如果输入的不是第一个字节,直接判断当前字节是不是 10 开头的即可。并且 cnt - -

    而如果是第一个字节,则又要分多种情况。

    1. 开头 1 的个数多于 4 个,假
    2. 开头 1 的个数是 0,2,3,4,则 cnt 直接设为 1 的个数即可
    3. 其它情况都是假

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        bool validUtf8(vector<int>& nums) {
            int cnt=0;
            
            for (int i=0;i<nums.size();i++){
                if(cnt==0){
                    if((nums[i]&0b11111000)==0b11111000){
                        return false;               
                    } else if((nums[i]&0b11110000)==0b11110000){
                        cnt=3;                
                    } else if((nums[i]&0b11100000)==0b11100000){
                        cnt=2;
                    } else if((nums[i]&0b11000000)==0b11000000){
                        cnt=1;
                    } else if((nums[i]&0b10000000)==0){
                        cnt=0;
                    } else {
                        return false;
                    }
                } else {
                    if((nums[i]&0b10000000)!=0b10000000){
                        return false;
                    }
                    cnt--;
                }
            }
            return cnt==0;
        }
    };

基础

  • list

    交换变量

    题意

    交换两数

    临时变量

    最简单的就是引入临时变量,没什么好说的

    class Solution {
    public:
        vector<int> swapNumbers(vector<int>& nums) {
            int t=nums[0];
            nums[0]=nums[1];
            nums[1]=t;
            return nums;
        }
    };

    异或

    第二种就是使用位运算异或,交换变量也是基本用法之一了。

    基本运算:x^x=0,x^0=x.

    class Solution {
    public:
        vector<int> swapNumbers(vector<int>& nums) {
    				// nums[0]:nums[0]^nums[1], nums[1]:nums[1]
            nums[0]^=nums[1]; 
    				// nums[0]:nums[0]^nums[1], nums[1]:nums[1]^nums[0]^nums[1]=nums[0]
            nums[1]^=nums[0];
    				// nums[0]:nums[0]^nums[1]^nums[0]=nums[1], nums[1]:nums[0]
            nums[0]^=nums[1];
            return nums;
        }
    };

    加减法

    加减法是另一个技巧,就是可能会溢出

    class Solution {
    public:
        vector<int> swapNumbers(vector<int>& nums) {
    				// nums[0]:nums[0]-nums[1], nums[1]:nums[1]
            nums[0]=nums[0]-nums[1];
    				// nums[0]:nums[0]-nums[1], nums[1]:nums[1]+nums[0]-nums[1]=nums[0]
            nums[1]=nums[1]+nums[0];
    				// nums[0]:nums[0]-(nums[0]-nums[1])=nums[1], nums[1]:nums[0]
            nums[0]=nums[1]-nums[0];
            return nums;
        }
    };

    效率

    三个方法复杂度都是一样的

    时间复杂度:

    空间复杂度:

    693. 交替位二进制数

    题意

    检查数字二进制的低位是否 0、1 交替,高位全零不管

    分析

    直接每次取最右边的位数,然后和当前应该是什么数比较即可,注意 flag 在 0 和 1 直接交替,可以用 x^1 实现。

    优化

    实际上,二进制的题很多都可以找规律解决掉。这题也是一样的。由于 01 交替,比如

    000000101010101,那么我们将其移位

    000000010101010,低位必然是一个 0 一个 1,所以我们可以异或

    000000111111111,再加上个 1,就必然变成

    000001000000000 这种只有一个 1 的数,所以我们可以用 x&(x-1) 清楚掉 1,结果必然等于 0.

    效率

    时间复杂度:模拟 ,优化后

    空间复杂度:

    代码

    模拟

    class Solution {
    public:
        bool hasAlternatingBits(int n) {
            int flag=n&1;        
            
            while(n!=0){
                n>>=1;
                
                if ((n&1)==flag){
                    return false;
                }
                
                flag^=1;
            }
                    
            return true;
        }
    };

    找规律

    class Solution {
    public:
        bool hasAlternatingBits(int n) {        
            int m=((n>>1)^n);
            if (m==INT_MAX){
                m=0;
            }else{
                m++;
            }        
            return (m&(m-1))==0;
        }
    };

    201. 数字范围按位与

    题意

    [left, right] 所有数相与的结果

    分析

    最直接的自然是遍历一遍了,但是会超时。所以我们得找规律。由于相与只有 1 与 1 相与为 1,其它为 0,所以我们要找位数相同的部分。

    假设 left=5,right=7,则

    5→0101

    7→0111

    要相与的话,显然 01x1 都相等,但是还有中间的数呢,即 6

    5→0101

    6→0110

    7→0111

    相同的部分为 01xx,所以结果为 0100,没看出来?继续

    假设 left=2,right=12,则

    2 → 00000010

    12→00001100

    相同部分为 0000xxx1,猜测一下,中间那些相与起来会是什么?显然我们只需考虑最低位和高 4 位就行了,因为其它位已经为 0 了。

    高 4 位会是什么?最低位又会是什么?

    2→0000xxx0

    3→0000xxx1

    4→0000xxx0

    5→0000xxx1

    6→0000xxx0

    7→0000xxx1

    8→0000xxx0

    9→0000xxx1

    10→0000xxx0

    11→0000xxx1

    12→0000xxx0

    我们发现高 4 位和左右边界一样都是 0000,而最低位则一直在 0,1 间交替,所以结果相同的部分应该是 0000xxxx,最后结果即为 0.

    根据上面的分析,其实我们可以得出结论了:

    1. 中间数字的高位和 left,right 的高位相同前缀一样
    2. 相同前缀后面的数则一直交替

    所以:数相与起来,就是 left,right 的相同前缀构成的数。 因为高位不变,低位交替相与就是 0。比如 5 和 7 的公共前缀就是 01xx,2 和 12 的公共前缀就是 0000xxxx。

    那么问题就变成:怎么寻找两个数的公共前缀。

    最简单的自然是模拟了,从左至右扫描一遍即可。

    优化

    我们也可以依次消除 right 最右边的 1,知道刚刚小于 left,这时的 rigth 就是公共前缀。

    效率

    时间复杂度:

    空间复杂度:

    代码

    暴力

    class Solution {
    public:
        int rangeBitwiseAnd(int left, int right) {
            int res=0xffffffff;
            
            for (int i=left;i<=right;i++){
                res&=i;
            }
            
            return res;
        }
    };

    模拟

    class Solution {
    public:
        int rangeBitwiseAnd(int left, int right) {
            int res=0;
            int a,b;
            
            for (int i=31;i>-1;i--){
                a=getBit(left,i);
                b=getBit(right,i);
                
                if (a!=b){
                    return res;
                }
                
                if (a==1){
                    res=setBit(res,i);
                } else {
                    res=unsetBit(res,i);
                }
            }        
            return res;
        }
        
        int getBit(int x,int pos){
            return (x>>pos)&1;
        }
        
        int setBit(int x,int pos){
            return x|(1<<pos);
        }
        
        int unsetBit(int x,int pos){
            return x&(~(1<<pos));
        }
    };

    消除 1

    class Solution {
    public:
        int rangeBitwiseAnd(int left, int right) {
            while(left<right){
                right&=(right-1);
            }
            return right;
        }
    };

    1684. 统计一致字符串的数目

    题意

    检查哪些字符串是由给定字符组成的

    分析

    要检查字符是否符合要求,就是个存在性问题,所以使用哈希表。而这题只检测字符,范围只有 24 个,所以我们可以用一个 int 变量来模拟哈希表,这样更简单一点。

    实际上,一些问题上经常可以可以这样简化哈希表的作用,比如统计字符个数,数字个数等,就可以用一个 24 或 9 长度的数组来代替,或者像这题一样,只是检测有限字符的存在性问题,也可以用 bitmap 来代替,而这题则比 bitmap 还要简化一点,即一个 int 变量就行了。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int countConsistentStrings(string allowed, vector<string>& words) {
            int cnt=0;
            int res=0;
            
            for (int i=0;i<allowed.size();i++){
                cnt=setBit(cnt,allowed[i]-'a');
            }
                    
            for (int i=0;i<words.size();i++){
                res++;
                for (int j=0;j<words[i].size();j++){                
                    if (!isExist(cnt,words[i][j])) {
                        res--;
                        break;                    
                    }
                }            
            }
            
            return res;
        }
        
        bool isExist(int x,char c){
            return getBit(x,c-'a')==1;
        }
        
        int getBit(int x,int pos){
            return (x>>pos)&1;
        }
        
        int setBit(int x,int pos){
            return x|(1<<pos);
        }
        
        int unsetBit(int x,int pos){
            return x&(~(1<<pos));
        }
    };

    898. 子数组按位或操作

    题意

    求所有连续子序列或之后结果的个数

    分析

    最简单的自然是直接双重循环生成所有连续子数组,然后在生成的过程中求得相或的结果。但是要得到的是不同结果的个数,即需要有一个去重的过程,故直接用 set 来暂存即可。

    优化

    本题数量级在 10^5,故暴力 显然会超时,故我们需要考虑优化。而最简单的优化就是进行一些剪枝操作,而不用更改基本思路。

    我们是怎么生成子数组的?在前一个子数组的基础上增加元素。而我们要求的是什么?或运算。

    这里我们就需要利用到或运算的性质了:一个数与另一个数或的结果,结果必然单调不减。

    其实也很好理解,毕竟或运算,原来的 1 还是 1,而 0 要么变 1,要么不变,总的来说数自然是在递增的了。

    所以说,我们固定左边界,右边界增加元素的过程中,所得到到子集或结果,也是递增的。

    而可以剪枝的地方就在这里:由于结果递增,那么如果这个结果已经达到了最大值,后面的所有子集或结果仍然是最大值,而我们只需要知道结果的个数,所以,后面子集不运算也不会影响结果。

    故,我们可以进行如下剪枝:

    1. 找到或结果的最大值
    2. 生成子集的过程中,判断结果是否大于最大值

    那么现在问题就转变为:怎么求或的最大值。

    我们前面说了,增加元素的或结果是递增的,那么元素最多时,自然结果必然就是最大值。

    所以,我们直接求得所有元素的或结果,就是最大值。

    效率

    时间复杂度:

    空间复杂度:

    代码

    暴力

    class Solution {
    public:
        int subarrayBitwiseORs(vector<int>& nums) {
            unordered_set<int> s;
            int t=0;
            
            for (int i=0;i<nums.size();i++){        
                t=nums[i];
                s.insert(t);
                for (int j=i+1;j<nums.size();j++){                
                    t|=nums[j];
                    s.insert(t);
                }             
            }
            
            return s.size();
        }
    };

    剪枝

    class Solution {
    public:
        int subarrayBitwiseORs(vector<int>& nums) {
            unordered_set<int> s;
            int t=0;
            int maxValue=0;
            
            for (int i=0;i<nums.size();i++){
                maxValue|=nums[i];
            }
            
            for (int i=0;i<nums.size();i++){        
                t=nums[i];
                s.insert(t);
                for (int j=i+1;j<nums.size();j++){                
                    t|=nums[j];
                    s.insert(t);
                    
                    if (t==maxValue){
                        break;
                    }
                }
            }
            
            return s.size();
        }
    };

    面试题 05.03. 翻转数位

    题意

    找出包含一个 0 的最长 1 的个数

    分析

    本题就是一个简单的变长滑动窗口,只是套了一个位运算而已。

    窗口中的状态就是含有 0 的个数,当小于等于一个时扩展,等于 1 个时收缩,每次扩展扩展收缩后更新长度即可。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int reverseBits(int num) {
            int res=0;
            int cnt=0;
            int l=0;
            int r=0;
            
            while(r<32){
                if (getBit(num,r)==0){
                    cnt++;
                }
                while(cnt==2){
                    if (getBit(num,l)==0){
                        cnt--;
                    }
                    l++;
                }
                r++;
                res=res>(r-l)?res:(r-l);
            }
            
            return res;
        }
        
        int getBit(int x,int pos){
            return (x>>pos)&1;
        }
    };

    面试题 05.01. 插入

    题意

    将 M 中的位数设置到 N 中

    分析

    遍历 M,然后写入 N 中对应位即可

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int insertBits(int N, int M, int i, int j) {
            for (int k=i;k<=j;k++){
                if (getBit(M,k-i)==1){
                    N=setBit(N,k);
                } else {
                    N=unsetBit(N,k);
                }
            }
            return N;
        }
        
        int getBits(int x,int pos,int cnt){
            return (x>>pos)&((1<<cnt)-1);
        }
        
        int getBit(int x,int pos){
            return (x>>pos)&1;
        }
        
        int unsetBit(int x,int pos){
            return x&(~(1<<pos));
        }
        
        int setBit(int x,int pos){
            return x|(1<<pos);
        }
    };

    717. 1 比特与 2 比特字符

    题意

    有两种特殊字符。第一种字符可以用一比特 0 来表示。第二种字符可以用两比特 (10 或 11) 来表示。

    现给一个由若干比特组成的字符串。问最后一个字符是否必定为一个一比特字符。给定的字符串总是由 0 结束。

    分析

    最简单的就是模拟,要问最后是不是只是一个 0,那么从左边开始放位置,遇到 1 就跳过一个位置,遇到 0 就继续,看最后是不是就是一个 0 即可。

    优化

    其实这题在分布上有一点规律,我们知道,一共三种分布:

    0

    10

    11

    我们从最后开始往前扫描,显然最后一个比较为 0 才行,即

    xxx0

    如果 0 前面没有 1,即

    0000

    显然是满足的

    如果 0 前面只有一个 1,即

    xxxx010

    再往前一位就是 xxx0010 或 xxx1010

    对于 xxx0010,连续两个 0 必然会被消掉,无论是 xx x0 0 10 还是 xxx 0 0 10 这两种情况,所以最后剩下的必然是 10 两位,即不满足。

    同理,对于 xxx1010,只有 xxx x1 0 10 或 xxx 10 10 这两种情况,最后 10 还是不满足。

    如果 0 前面有两个 1,即

    xxx0110

    那么就有 xx x0 11 0 和 xxx 0 11 0 这两种消除的情况,所以满足

    对于三个 1 的,根据上面的规律,不满足。

    所以我们可以找到规律了,即对于最后 xxxx0111...1110 中 1 的个数,如果是奇数,则不满足,如果是偶数,则满足。

    效率

    时间复杂度:

    空间复杂度:

    代码

    模拟

    class Solution {
    public:
        bool isOneBitCharacter(vector<int>& nums) {
            int i;
            
            if (nums[nums.size()-1]==1){
                return false;
            }
            
            for (i=0;i<nums.size()-1;i++){
                if (nums[i]==1){
                    i++;
                }
            }
            
            return i==nums.size()-1;
        }
    };

    统计最后 1 的个数

    class Solution {
    public:
        bool isOneBitCharacter(vector<int>& nums) {
            int cnt=0;
            if (nums[nums.size()-1]==1){
                return false;
            }
            
            for (int i=nums.size()-2;i>-1;i--){
                if (nums[i]==1){
                    cnt++;
                } else {
                    return cnt%2==0;
                }
            }
            
            return cnt%2==0;
        }
    };

    397. 整数替换

    题意

    给定一个正整数 n ,你可以做如下操作:

    如果 n 是偶数,则用 n / 2 替换 n 。
    如果 n 是奇数,则可以用 n + 1 或 n - 1 替换 n 。
    n 变为 1 所需的最小替换次数是多少?

    分析

    基本操作如下:

    1. 当低位为 0 时,即为偶数时,直接右移
    2. 当低位为1 时,++ 或 —

    要把一个数变成 1,要把 n 变成 1,即 0000 0001,所以最后的结果,必然只有一个 1。

    无论这个 1 出现在什么地方,后面的操作都是移位。

    显然当低位为偶数时,移位不会影响 1 的个数,所以要使得满足 1 的个数,最后得奇数时,即低位为 1 时进行操作才行。

    问题就在于,当低位为 1 时,我们究竟是 ++,还是 — 呢?也就是说,每次当低位为 1 时,我们都需要从两个策略中选择一个,问题就在于,怎么选。

    直接看,似乎看不出来,来个例子

    假设数 n 为 0111 1110,我们该怎么做呢?

    1. 偶数移位为 0011 1111

    现在问题来了,低位是 1,该怎么选。

    假设++,变成: 0100 0000

    假设—,变成 : 0011 1110

    我们看到,++ 的时候,似乎 1 的个数一下子就从 6 个变成了 1 个,而 — 则还有 5 个。而我们的目的就是要通过 ++ 或 — 减少 1 的个数。

    那么为什么,上面 ++ 的时候能一下子减少这么多 1 的呢?其实很简单,因为最后是 xx111,一些 1 相邻,+1 自然会把这些 1 全部都消除掉。

    我们现在只来看最后数的两位:

    00 → 偶数

    01

    10 → 偶数

    11

    显然,00 和 10 结尾的都是偶数,所以不管。问题在于,以 01 结尾的奇数和 11 结尾的奇数怎么操作。

    按照我们上面的分析,以 11 结果的操作,进行 ++ 时,能一下子消除更多的 1.那么对于以 01 结尾的呢?

    01+1 = 10

    01-1 = 00

    显然,01 结尾的进行 — 时消除的 1 个数更多。

    所以现在我们能够进行如下算法:

    1. 最后两位是 00 或 10 的,右移一位
    2. 最后两位是 01 的,- -
    3. 最后两位是 11 的,++

    但是上面还有一个例外,就是 3,3 是 0011,但是如果先 ++ 变成 0100,就要再移两次,一共三次操作。而直接 - - 变成 0010,再 - - 变成 0001,就只有两次操作。

    所以根据上面的分析,我们知道了算法的思想,就是某次 ++ 或 - - 时,贪心的选择能消除 1 的个数最多的那个操作。

    但是根据上面 3 那个意外,我们也发现似乎这个贪心不一定就全部满足,所以:

    贪心选择消除 1 最多的操作 就一定等于 最后操作次数最少吗?

    这个就需要证明了。

    证明:略(我也不会)

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int integerReplacement(int t) {
            int res=0;
            long n=t;
                    
            while(n!=1){
                if ((n&1)==0){
                    n>>=1;
                } else if (((n&0b11)==0b11) && (n!=3)){                
                    n++;
                } else {
                    n--;
                }
    
                res++;
            }
            
            return res;
        }
    };

异或

异或是一个等价关系,应用非常广泛。

  • list

    421. 数组中两个数的最大异或值

    题意

    分析

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    暴力枚举

    class Solution {
    public:
        int findMaximumXOR(vector<int>& nums) {
            int res=0;
            
            for (int i=0;i<nums.size();i++){
                for (int j=i+1;j<nums.size();j++){
                    res=max(res,nums[i]^nums[j]);        
                }  
            }
            
            return res;
        }
    };

    1863. 找出所有子集的异或总和再求和

    题意

    分析

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    1720. 解码异或后的数组

    题意

    由差分异或得出前缀异或

    分析

    这题就是前缀数组和差分数组的一个变形,只是运算变成异或了而已,而已相互转换都是异或运算,而不是加减了。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        vector<int> decode(vector<int>& nums, int a) {
            nums.insert(nums.begin(),a);
            
            for (int i=1;i<nums.size();i++) {
                nums[i]^=nums[i-1];
            }
            return nums;
        }
    };

    1486. 数组异或操作

    题意

    求数组异或的结果

    分析

    找个锤子规律

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int xorOperation(int n, int start) {
            int res=0;
            for (int i=0;i<n;i++) {
                res^=(start+i*2);
            }
            return res;
        }
    };

    1734. 解码异或后的排列

    题意

    给定差分异或数组,求原数组,且原数组是 1~n 的数组排列(答案唯一)

    分析

    已知

    而已知 encoded 数组,故我们只需要知道 perm[0],就能递推求出整个 perm 数组。现在问题就是:怎么求出 perm[0]?

    我们来看一下题目中还有哪些条件没有使用到:

    1. n 是奇数
    2. perm 数组是 1~n 的排列

    直接看上面似乎看不出来,实际上,我们可以进行如下推理

    而 x 和 e 数组都是已知的,故我们先通过上面的公式得出 p[0],再通过递归公式求出整个 perm 数组即可。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        vector<int> decode(vector<int>& e) {
            vector<int> p(e.size()+1);
            
            for (int i=1;i<=p.size();i++){
                p[0]^=i;
                if ((i%2!=0) && (i<e.size())){                
                    p[0]^=e[i];
                }
            }
    
            for (int i=1;i<p.size();i++){
                p[i]=p[i-1]^e[i-1];
            }
            
            return p;
        }
    };

    1072. 按列翻转得到最大值等行数

    题意

    可以对任意列进行反转,求最后行相等的个数

    分析

    如果行元素相等这个可能有很多种,可能有的行都是 1,有的行都是 0,而同时考虑很多行比较麻烦,但是只考虑一行的话,就很简单。

    首先,由于可以对任意列进行反转,那么任意给定一行,我们总能将其变成行相等的情况。这是没有问题的。

    那么假设数组分成很多不同类型的行,比如 1101 的有 a 种,0011 的有 b 种,那么我们要进行反转的话,只需要找到类型最多的那种行进行反转就行了。

    但是现在问题就出现了:

    反转类型 a 的行,类型 b 的行会不会同时也变成行相等的情况呢?

    比如说

    001

    001

    110

    110

    现在 001 类型和 110 类型都有两个,我们对 001 进行反转的时候,假如反转成 000,但是这个时候,110 类型的行也同时变成 111 这种行相等的情况了,所以最后的结果不是 2 而是 4 了。

    那么我们该怎么解决这个问题呢?或者说,有没有什么办法,把行的类型变成反转的时候不会影响其它行的情况呢?

    我们来看一下那些看上去明明类型不同,但是反转后却同时满足行相等的行,是什么情况:

    001

    110

    11101

    00010

    11101010

    00010101

    发现了吗?没错,这些行其实就是完全取反后的,如果两个行之间取反相等,那么对它们进行列上的反转时,它们会同时达到行相等的情况。

    那么现在问题就很简单了:

    我们可以把数组分成很多不同类型行的组合:比如 a 和 a 取反表示一类,如 110,001 表示一类,111 和 000 表示一类。

    然后找到类别最多的那种行进行反转,就是最后行相等最大的行数。

    有人可能觉得行取反还是一个类型感觉有点麻烦,那我们就预处理一下嘛,先把那些表示相反的行取反,转换成同一类。比如以第一位为例,0 开头取反就是 1 开头的,那我们把所有 1 开头的行取反,就全部变成 0 开头的了,就不存在取反还是同一类的情况了。

    由于要统计不同类别的个数,所以直接用个哈希表暂存一下即可,最后遍历取最值。

    总结一下,就是开一些桶,某个时刻只有一个桶能满足行相等。映射桶的方式也很简单,某行和这个行所有元素取反的行映射到同一桶。最后找出最多的个桶即可。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int maxEqualRowsAfterFlips(vector<vector<int>>& mat) {
            unordered_map<string,int> m;
            string s;
            int res=0;
            
            for (int i=0;i<mat.size();i++){
                s.clear();
                for (int j=0;j<mat[0].size();j++){
                    if (mat[i][0]==1){
                        s+=('0'+ (mat[i][j]^1));
                    } else {
                        s+=('0'+ mat[i][j]);
                    }                            
                }
                m[s]++;
            }
            
            for (auto item:m) {
                res=max(res,item.second);
            }
            
            return res;
        }    
    };

寻找数

在寻找某数的题型中,通常和涉及到数字个数的,如某数重复,缺失,出现多少次等等题型,也是比较常见的位运算题目。

重复缺失个数确定,像偶数个奇数个确定的,通常可以用异或消除重复元素

涉及多个奇数的,通常要将它们分离开。

重复个数不确定的可以考虑用位数统计,当然确定的也能用,算是比较万能。

更复杂一些的,可以建立 DFA 再编码解决。

通常需要知道位为 1 的地方,可以用 x&-x 快速确定。

对于范围小于 32,重复次数最多 2 次的,可以用一个 int 模拟哈希表,比如 0~9,大小写字母重复一次等。

此外,还有和位运算无关的哈希排序遍历计数统计原地置换set 等方法。

  • list

    268. 丢失的数字

    题意

    求 n-1 长度的数组中,缺失的数字

    分析

    重复、缺失数字是典型的异或应用场景,异或的几大运算法则如下:

    x^x=0,x^0=x,

    故,我们可以用 0~n 共 n+1 个数字和原数组进行异或,那么除了缺失的那个数字,其它数字都有两个相同的数异或,为 0,故最后就变成缺失的那个数和 0 异或,结果就是缺失的那个数。

    面试题 17.04,剑指 Offer 53,268 题,三个题一模一样.

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int missingNumber(vector<int>& nums) {
            int res=nums.size();
            for (int i=0;i<nums.size();i++){
                res^=i;
                res^=nums[i];
            }
            return res;
        }
    };

    136. 只出现一次的数字

    题意

    数组中除了一个数只出现一次,其它都出现两次,找出这个出现一次的数字

    分析

    最自然的方法有排序然后遍历,也可以遍历哈希或辅助数组。当然,利用数组出现一次两次这个条件,是异或的典型应用场景。

    即 x^x=0,0^x=x,利用这两个公式,那么所有重复的数异或就为 0,最后 0 再和余下的数字异或,就是这个出现一次的数字了。

    优化

    异或时有个小技巧,我们不用开个 res 变量来保存结果,直接用 nums[0] 来存结果即可。

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int singleNumber(vector<int>& nums) {
            for (int i=1;i<nums.size();i++){
                nums[0]^=nums[i];
            }
            return nums[0];
        }
    };

    389. 找不同

    题意

    t 比 s 多一个字符,找到该字符

    分析

    这题就是 136 稍微变了一下,只是分成两个集合而已,本质上还是情侣中找单身狗,即只有一个出现一次,其它出现两次。

    最简单的自然是用异或消除重复了,两个全部异或起来,结果就是单身狗。

    稍微复杂点的就是根据抽屉原理,s 所有字符在对应位的和,一定比 t 对应和小。因为 s 中有的 t 都有,多出来的就是多的元素了。

    参考 136,287 等题即可。

    优化

    可以把结果存在 s[0] 中,不用新开变量.

    效率

    时间复杂度:

    空间复杂度:

    代码

    异或

    class Solution {
    public:
        char findTheDifference(string s, string t) {        
            if (s.empty()){
                return t[0];
            }
            
            for (int i=1;i<s.size();i++){
                s[0]^=s[i];
                s[0]^=t[i]; 
            }
            s[0]^=t[0];
            s[0]^=t[t.size()-1];
            return s[0];
        }
    };

    位数统计

    class Solution {
    public:
        char findTheDifference(string s, string t) {
            int cnt1;
            int cnt2;
            char res=0;
                    
            for (int offset=0;offset<8;offset++){
                cnt1=0;
                cnt2=0;
            
                for (int i=0;i<s.size();i++){                
                    cnt1+=((s[i]>>offset)&1);                
                }
                
                for (int i=0;i<t.size();i++){
                    cnt2+=((t[i]>>offset)&1);
                }
                
                if (cnt2>cnt1){
                    res|=(1<<offset);
                }
            }
            
            return res;
        }
    };

    287. 寻找重复数

    题意

    长度为 n+1 的数组,里面的数字在 1~n 范围内,且有一个元素至少重复出现1次,找出该元素。

    分析

    注意是至少重复1次,即至少 2 个一样的,个数 ≥2,不是等于 2.如果等于 2 的话,直接异或就行了,这是第一点。

    要找重复元素,其实是典型的哈希表应用场景,故可以用哈希表来做。不过由于要空间复杂度 ,就只能考虑用位来代替普通的哈希表了,即用 1个int变量来表示,每位表示一个数字。但是,由于只有 32 位,故最多表示 32 个数,可以用在字母,或一个数字上,但是这题范围 1 <= n <= 10^5 ,故还是不行。

    异或不行,哈希也不行,重复次数还不一定,那就只能考虑位数统计了。即类似于 137 题,根据抽屉原理,原先 1~n 数在每一位的个数是一定的,新数组在对应位如果更多,就必然是重复元素的。

    故我们统计 1~n 对应位,即数组每个数对应位,求和比较即可。由于要空间复杂度,故用变量暂存结果。

    优化

    可将统计两个数组的位数时同时进行,只是 1n 变成统计 0n 对应位而已,但是结果不变。

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int findDuplicate(vector<int>& nums) {
            int cnt1;
            int cnt2;
            int res=0;
            
            for (int offset=0;offset<31;offset++){
                cnt1=0;
                cnt2=0;
            
                for (int i=1;i<nums.size();i++){
                    cnt1+=((i>>offset)&1);
                }            
    
                for (int i=0;i<nums.size();i++){
                    cnt2+=((nums[i]>>offset)&1);
                }
                
                if (cnt2>cnt1){
                    res|=(1<<offset);
                }
            }
            
            return res;
        }
    };
    class Solution {
    public:
        int findDuplicate(vector<int>& nums) {
            int cnt1;
            int cnt2;
            int res=0;
            
            for (int offset=0;offset<31;offset++){
                cnt1=0;
                cnt2=0;
            
                for (int i=0;i<nums.size();i++){
                    cnt1+=((i>>offset)&1);
                    cnt2+=((nums[i]>>offset)&1);
                }
                
                if (cnt2>cnt1){
                    res|=(1<<offset);
                }
            }
            
            return res;
        }
    };

    137. 只出现一次的数字 II

    题意

    一个数字出现 1 次,其余出现 3 次,找出出现 1 次的数字。

    位数统计

    看上去这题似乎和 136,168 类似,应该用异或来消除重复的元素。不过很可惜,这个题并不能使用异或。

    异或的使用场景应该是 1 个 奇数或偶数个,不过很可惜,这题全是奇数个。我们应该考虑 3 和 1 这个特殊的数字。换句话说,一个数只出现 3 次能说明什么?能被 3 整除。出现一次呢?不能被 3 整除吗?显然不是,因为这个数可能就是 3 的倍数。

    虽然对整个数字来说,上面的思路似乎有些问题。不过在位运算题目中,显然我们也可以考虑每一位。那么对于任意一位,出现 3 次,显然和为 3,出现一次和就为 1。换句话说,如果把所有数的相应位加起来,就应该是 1331133 这种形式。相应位为 1 显然就是我们要求的那个数字的位数。

    所以思路很明确了,我们把所有对应位的和求出来,然后看哪些是 1,然后组成的数就是要求的数,如 1331133 显然就是 1001100。

    class Solution {
    public:
        int singleNumber(vector<int>& nums) {        
            vector<int> t(32);
            int res=0;
            
            for (int offset=0;offset<32;offset++){
                for (int i=0;i<nums.size();i++){
                    t[offset]+=((nums[i]>>offset)&1);
                }             
            }
            
            for (int i=0;i<32;i++){            
                res+=((t[i]%3)<<i);
            }
            
            return res;
        }
    };

    我们需要对每一个数统计 32 个位,故 ,空间复杂度用了一个 32 int 的数组,应该算 .

    组合逻辑电路设计

    如果说上面位数统计算是比较常见和基础的话,下面的组合逻辑就有点专业了,毕竟需要数电的基础知识,并且还有一些 DFA 的概念。我看了一个小时才看懂题解,如果没有学过数电可能就看不懂了。

    在位数统计中,我们是根据每位数的出现次数来判断最后这一位应该是什么的,我们是把数据的位数存下来了。在上面位数统计中,我们知道,一位数要么出现 0 次,要么出现 1 次,要么就出现 3 的倍数次。

    因为 3 的倍数次最后还是要对 3 取余,那么起始就等价于出现 0 次。换句话说,我们可以建立一个状态机,对于每一位数,它的状态一定在状态机中。建立状态机的思想,这是这题第一个要点。

    状态机有三种状态:a,b,c 分别表示出现 0,1,2 次。

    状态转移有两种方式:输入 0 或 1.

    如下图

    无效的图片地址

    对于每一位,它就应该满足这个 DFA 的状态转移方式(不知道 DFA 则需要先学习下状态机)。

    注意,是对于每一位满足这个 DFA。(初始状态我们都设为 a,即出现 0 次)。

    如 [2,2,3,2],即 [0010,0010,0011,0010],对于最高位,出现次序为 0,0,0,0,那么在 DFA 中,显然就是 a→a→a→a→a 这几种转移过程,对于低第二位,次序为 1,1,1,1,则转移过程为 a→b→c→a→b,最后一个次序为 0,0,1,0,转移过程为 a→a→a→b→b.

    那么最后所有位的状态应该是 aabb。

    而 a 即表示该为出现 0 次,那么最后该为 0。b 表示出现 1 次,该为 1.故 aabb 就是 0011,即 3.所以最后结果为 3.

    需要注意的是,由于出现次数要么 0,要么 1 要么 3,而 3 次最后就是 0 次,所以该 DFA 的终止状态应该是 a 或 b。

    无效的图片地址

    画出上面的 DFA 后,有两种方式来实现它:

    1. 常规方式,即写转移表或跳转表,表驱动函数等
    2. 用位运算,将其视为组合逻辑电路,得到表达式

    如果按 DFA 常规方式去处理的话,显然太麻烦了。所以我们可以按第二种方式来做。使用组合逻辑电路思维来设计,这是这题第二个要点。

    由于一共有 3 个状态,所以需要两位才能表示每一个状态。我们假设 xy 为当前状态,那么设

    xy状态
    00a
    01b
    10c

    根据数电中组合逻辑电路知识,我们知道上面这个 DFA 其实就是个组合逻辑电路,要设计电路的话,我们需要知道输入状态,输出状态,然后画出真值表,再求出表达式,最后化简,再画出电路图。而在这个题中,我们只需求出表达式即可。

    真值表其实就是上面 DFA 的表格化,就是当前状态是什么,输入状态是什么,下一个状态又是什么

    当前状态 xy输入下一个状态 xy
    00(a)000(a)
    00(a)101(b)
    01(b)001(b)
    01(b)110(c)
    10(c)010(c)
    10(c)100(a)

    求出表达式:

    注意了,上面的 x,y,x,y 都是二进制数。我们求得是对于一位 来说,它是怎么转换状态的。而我们要求的是 int 类型,有 32 位,所以,如果按照这种一位的求法,我们需要计算 32 次。

    也就是遍历每一个数字,然后对某位数进行上面这样的转换,那么问题来了:

    1. x,y 都是二进制数,我们只能用 bool 来表示该数
    2. 我们要遍历 32 次数字,复杂度比较高

    既然每一位都要进行相同的计算,而且不同位之间的计算是相互不影响的(按位运算,位运算就是不影响的),那我们为什么不一个数字的 32 位同时进行运算呢?

    这就是优化的第一点了,我们可以用两个 int 的变量之间来表示某个时刻,某个数,32 位的当前分别在对应 32 个状态机中的状态。 也就是常见的状态压缩。而使用状态压缩,这是这题第三个要点。

    和普通一位进行状态转移的表达式是一样的,只是之前是某一位,现在是 32 一起而已。

    假设最后结果状态为 x,y,按照我们上面的分析,其实就是 a 状态和 b 状态的一些组合。那么问题来了,我们怎么根据这个状态,得到我们想要的数字结果呢?

    这就有一点编程的技巧性了,由于我们之前对状态的编码是 x(0),y(1),而我们知道最后结束状态一定是 a 或 b。换句话说,假设处于 a 状态,那么 当前编码就是 xy=00。而如果当前处于 b 状态,编码 xy=01.

    还记得 a 状态和 b 状态表示什么意思吗?a 是出现 0 次,b 是出现 1 次。那么自然的,在对应位,出现 0 次就是 0,出现一次就是 1。也就是说,如果该位状态是 a,那么就应该是 0,如果是 b,就应该是 1。而这,和对应编码 xy 的 y 是一模一样的。

    换句话说:最后状态编码 xy 中的 y,对应的数字,恰好就等于我们要得到的结果这一点非常巧妙,也是本题最后一个要点。

    如果我们之前编码 a,b 时,对应 xy 处理得不好,那么显然就得不到这么巧合的结果了。

    最后我们再来总结一下这个题的要点:

    1. 将对应位的状态引入 DFA 的概念
    2. 用组合逻辑电路处理 DFA
    3. 状态压缩同时求 32 位
    4. 编码的巧妙直接得结果

    当然,上面由于我们求的表达式是直接根据真值表得到的,也可以优化一下,不过那就更是纯粹的数电知识了,就不深入了。

    最后再来分析一下复杂度:

    我们只遍历了一遍,时间复杂度就为 ,只用到几个辅助变量,故空间复杂度为 .

    class Solution {
    public:
        int singleNumber(vector<int>& nums) {        
            int x=0;
            int oldX=0;
            int newX=0;
            int y=0;  
            bool a,b;
            
            for (int z=0;z<nums.size();z++){
                oldX=x;
                newX=((~oldX)&y&nums[z])|(oldX&(~y)&(~nums[z]));
                y=((~x)&(~y)&nums[z])|((~x)&y&(~nums[z]));
                x=newX;
            }
            
            return y;
        }
    };

    645. 错误的集合

    题意

    数组中有一个缺失的,一个重复的元素,要找到这两个元素。

    分析

    这题看上去有点像 136,268 这几个题,缺失元素或重复元素,典型的异或场景。异或的作用主要是消除重复的元素,由于 x^x=0,x^0=x,且异或运算是二元运算,即满足结合,交换律。

    所以这题我们也可以用 1~n 的元素进行异或,那么就可以消除掉原本存在奇数次的元素,即那么存在的,自然就只有重复的元素,并且还有一个元素缺失也无法消除掉,故假设重复元素为 a,缺失元素为 b,那么最后异或的结果就是 a^b.

    但是显然,我们想要的是两个数,而现在得到的却是一个数,说明我们的思路出了一点问题。像 136,268 这种题都是集合里只有一个特殊元素的,而这题却又两个。所以,如果有什么办法,能把集合划分为两个集合的话,分别对它们异或就能得到结果了。

    比如 [1,2,2,4],我们将其和 1~4 集合合并,即 [1,1,2,2,2,3,4,4] 集合,将新集合全部异或得到的结果就是 2^3.而如果我们能将该集合划分为两个集合,如 [1,1,2,2,2],[3,4,4],然后分别异或就能得到结果 2 和 3 了。

    要划分的话,显然需要找到一个条件,然后一个集合满足,一个不满足,即变成二元状态,而与运算就可以做到。

    我们再来看,由于重复元素和缺失元素一定不相等,那么 a^b 一定不等于 0,换句话我,a^b 转换为二进制,那么,必然有一位为 1. 而这个 1 同样也是 a 和 b 对应位异或而来的。而异或要为 1,只能两个数一个 0 一个 1才行,即 1^0=1.

    换句话说,重复元素和缺失元素在某一位上,一定是一个为 1 一个为 0 的。而我们知道 1&1=1,1&0=0。所以如果我们将所有元素的这一位与 1 相与,那么我们就能将重复元素和缺失元素划分到两个集合中。且相同元素自然也是划分到一个集合中的。

    比如,对于 1,2,3,4 三个数,将数字转换为二进制,有

    1 → 0001

    2 → 0010

    3 → 0011

    4 → 0100

    而重复数字 ^ 缺失数字= 2^3=0010^0011=0001,显然最后一位就是 1.我们将集合 [1,1,2,2,2,3,4,4] 每个数最低位与 1 相与,就能划分为集合 [1,1,3],[2,2,2,4,4],然后分别相与,就能得到结果 3 和 2.

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        vector<int> findErrorNums(vector<int>& nums) {
            int xorTmp=0;
            int tmp=0;        
            int a=0;
            int b=0;
            int offset=0;
            
            // 和 [1,size] 的数相与
            for (int i=0;i<nums.size();i++){
                xorTmp^=(i+1);
                xorTmp^=nums[i];
            }
            
            // 这时 xorTmp = a^b        
            tmp=xorTmp;
            
            // 找到 tmp 对应位为 1 的偏移量,即 offset
            while((tmp&1)==0){
                offset++;
                tmp>>=1;
            }
                            
            // 遍历数组,然后将其划分为两部分
            for (int i=0;i<nums.size();i++){
                if (((nums[i]>>offset)&1)==0){                                
                    a^=nums[i];
                } else {
                    b^=nums[i];
                }
                
                if ((((i+1)>>offset)&1)==0) {
                    a^=(i+1);                
                } else {
                    b^=(i+1);
                }
            }
            
            // 判断谁是重复的元素
            for (int i=0;i<nums.size();i++) {
                if (nums[i]==a) {
                    return {a,b};
                }
            }
            
            return {b,a};
        }
    };

    剑指 Offer 56 - I. 数组中数字出现的次数

    题意

    一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。找出这两个数字。

    分析

    这题就是 645 的简单版,645 还要新组成集合,而这个已经弄好了。参考 645 即可。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        vector<int> singleNumbers(vector<int>& nums) {
            int tmp=0;
            int offset=0;
            int a=0;
            int b=0;
            
            for (int i=0;i<nums.size();i++){
                tmp^=nums[i];
            }
            
            
            while(((tmp>>offset)&1)==0){
                offset++;
            }
                    
            for (int i=0;i<nums.size();i++){
                if (((nums[i]>>offset)&1)==1){
                    a^=nums[i];
                }
            }
                    
            return {a,tmp^a};
        }
    };

    260. 只出现一次的数字 III

    题意

    分析

    参考 645,剑指 Offer 56 - I. ,基本一样

    面试题 17.19. 消失的两个数字

    题意

    给定一个数组,包含从 1 到 N 所有的整数,但其中缺了两个数字。你能在 O (N) 时间内只用 O (1) 的空间找到它们吗?

    分析

    参考 645,260,剑指 Offer 56 - I. ,基本一样

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        vector<int> missingTwo(vector<int>& nums) {
            int xorTmp=0;
            int tmp=0;        
            int a=0;
            int b=0;
            int offset=0;
            
            // 和 [1,size] 的数相与
            for (int i=0;i<nums.size();i++){
                xorTmp^=nums[i];
            }       
            // 和 [1,size] 的数相与
            for (int i=1;i<=nums.size()+2;i++){
                xorTmp^=i;
            }
            // cout<<xorTmp<<endl;
            // 这时 xorTmp = a^b        
            tmp=xorTmp;
            
            // 找到 tmp 对应位为 1 的偏移量,即 offset
            while((tmp&1)==0){
                offset++;
                tmp>>=1;
            }
                            
            // 遍历数组,然后将其划分为两部分
            for (int i=0;i<nums.size();i++){
                if (((nums[i]>>offset)&1)==0){
                    a^=nums[i];
                } else {
                    b^=nums[i];
                }
            }
            
            for (int i=1;i<=nums.size()+2;i++){
                if (((i>>offset)&1)==0){
                    a^=i;
                } else {
                    b^=i;
                }
            }
            
            return {b,a};
        }
    };

    剑指 Offer 03. 数组中重复的数字

    题意

    一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。且数组中有多个数是重复的,找出一组重复的数。

    分析

    最简单的自然排序然后遍历,虽然空间复杂度 ,但是时间复杂度是 ,要优化的话,可以空间换时间,用哈希表,但是这样虽然时间复杂度变成线性 了,但空间复杂度又增加了 .

    那么有没有办法再进行优化呢?

    优化

    想要时间复杂度不增加,原数组又无序,显然只能哈希了,但是如果哈希的话,又要新开数组存值,那么有办法降低这个空间吗?

    通常来说有两种方法:

    1. 用位图思想
    2. 原地哈希

    位图思想很容易理解,我们只需要知道一个数是否存在,不需要知道它是什么。于是,而是否存在这个信息只需一位就能保存,所以我们可以用位图来存,可以新开 bitset,如果数字范围比较小的话,还能用一个 int 来模拟位图。

    虽然位图也能解决,但是就没有更优化的策略了吗?其实是有的,那就是原地哈希。由于这个题给定的数字比较特殊,即数组长度为 n,数字范围却是 1~n-1。那么,我们可以借用这个数组本身,实现哈希的功能。

    哈希表最主要的是映射规则,而在这里,我们的映射规则也很简单:i→i,即 key 和 value 是一样的。即 nums[i]=i,换做哈希写法,就是 map[i]→i.

    无效的图片地址

    而由于有重复元素,所以必然最终有两个数映射到一个位置中,那么我们再遍历的过程中,只要判断对应位置中,是否已经有了元素,有的话,这个元素就是重复的。

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int findRepeatNumber(vector<int>& nums) {
            for (int i=0;i<nums.size();i++){            
                if (nums[i]==i){
                    continue;
                }
                
                if (nums[i]==nums[nums[i]]){
                    return nums[i];
                }
                
                if (nums[nums[i]]==nums[nums[nums[i]]]) {
                    return nums[nums[i]];
                }
                            
                swap(nums[i],nums[nums[i]]);
            }
            
            return 0;
        }
    };

    169. 多数元素

    题意

    找到出现次数超过一半的元素

    分析

    典型的位数统计题目,直接扫描某个数字的每一位,相加然后统计,如果超过一半,说明该位就是 1,否则为 0.

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int majorityElement(vector<int>& nums) {
            vector<int> t(32);
            int res=0;
            
            for (int i=0;i<32;i++){
                for (int j=0;j<nums.size();j++){
                    t[i]+=((nums[j]>>i)&1);
                }
            }
            
            for (int i=0;i<32;i++){
                if ((t[i]<<1)>nums.size()) {
                    res=setBit(res,i);
                } else {
                    res=unsetBit(res,i);
                }
            }
            
            return res;
        }    
        
        int setBit(int x,int pos) {
            return x | (1<<pos);
        }
        
        int unsetBit(int x,int pos){
            return x & (~(1<<pos));        
        }    
    };

个数统计

个数统计主要是统计 1,0 的个数。

  • list

    191. 位 1 的个数

    题意

    计算二进制数中 1 的个数

    枚举

    最简单的自然是枚举了,遍历 32 位,是 1 的就统计即可。

    class Solution {
    public:
        int hammingWeight(uint32_t n) {
            int cnt=0;
            for (int i=0;i<32;i++){
                cnt+=getBit(n,i); // 位 1 的个数也等于所有位的和
            }
            return cnt;
        }
        
        int getBit(int x,int pos){
            return (x>>pos)&1;
        }
    };

    或移位到最低为判断

    class Solution {
    public:
        int hammingWeight(uint32_t n) {
            int cnt=0;
            while(n!=0){
                cnt+=(n&1);
                n>>=1;
            }
            return cnt;
        }
    };

    枚举的时间复杂度为

    消除位优化

    在枚举时,我们是遍历位再判断,实际上,我们可以使用 x&(x-1) 直接清楚最低位的 1,然后直到为 0 为之。

    class Solution {
    public:
        int hammingWeight(uint32_t n) {
            int cnt=0;
            
            while(n!=0){
                n=(n&(n-1));
                cnt++;
            }
            return cnt;
        }
    };

    时间复杂度为

    分治

    实际上,我们还有一种分治的做法,前面说了,求 1 的个数其实就是求各位的和。也就是,求 32 位的和。

    而类似于归并,我们可以先求 16 位的和,再归并。求 16 的又通过先求 8 位的和,求 8 位的又先求 4 位的,求 4 位的又求 2 位的。

    所以,我们反过来,先求所有相邻 2 位的和,再求相邻 4 位的话,再求相邻 8 位的和,再求相邻 16 位的和,最后求 32 位的和即可。

    无效的图片地址

    并且,我们就利用原遍历存中间结果,不用递归什么。要求几位的和,那么相应低几位就是和。比如求相邻 8 位的和,那么 4 4 右边那 4 位就是两个 4 位的和。

    假设我们要求 00001011 这一个字节中 1 的个数,那么

    我们要求相邻2位的和,显然,我们需要把两位每位取出来

    无效的图片地址

    数取出来后,要相加的话,显然要对齐才行,我们需要把偶数位取出来的右移 1 次,再相加

    无效的图片地址

    于是,原先的相邻两位,现在值就是它们 1 的个数

    无效的图片地址

    原来的数

    无效的图片地址

    同理,接下来就要求相邻 4 位 的了,取相邻 2 位的数

    无效的图片地址

    移位相加

    无效的图片地址

    而原来的数显然就是 0 和 3 个。

    无效的图片地址

    同理,求相邻 8 位时,显然就是上面这相邻 4 位相加,结果还是一样的。最后就是

    无效的图片地址

    同理,要求 32 位的话,继续求相邻 16,32 位的即可。

    而取数也有两种取法,先取低 n 位,再取高 n 位,然后高 n 位向右移位再相加。也可以再取高位之前,先对整个数移位,把高位变成低位,再取也行。

    class Solution {
    public:
        int hammingWeight(uint32_t n) {        
    				// 取奇数位        取偶数位,然后右移变成低位,再相加
            n=(n&0x55555555)+((n&0xaaaaaaaa)>>1);
            n=(n&0x33333333)+((n&0xcccccccc)>>2);
            n=(n&0x0f0f0f0f)+((n&0xf0f0f0f0)>>4);        
            n=(n&0x00ff00ff)+((n&0xff00ff00)>>8);
            n=(n&0x0000ffff)+((n&0xffff0000)>>16);
            return n;
        }
    };
    
    class Solution {
    public:
        int hammingWeight(uint32_t n) {      
    				// 取奇数位        数先右移,把偶数位变成奇数位,然后取再相加  
            n=(n&0x55555555)+((n>>1)&0x55555555);
            n=(n&0x33333333)+((n>>2)&0x33333333);
            n=(n&0x0f0f0f0f)+((n>>4)&0x0f0f0f0f);        
            n=(n&0x00ff00ff)+((n>>8)&0x00ff00ff);
            n=(n&0x0000ffff)+((n>>16)&0x0000ffff);
            return n;
        }
    };

    上面这种方法时间复杂度为 。上面这种分治的方法还有一些取数和加减上的优化,不过原理都是一样的,就不展开了,深入可以参考 回归本源一一位运算及其应用 这篇文章。

    338. 比特位计数

    题意

    计算 0~n+1 所有数,对应数二进制中 1 的个数

    分析

    用 191 题的解法直接写出汉明重量,即每数中 1 的个数,然后遍历即可。

    优化

    无,优化个锤子,dp,不存在的。

    效率

    时间复杂度:,k 为每个数中 1 的个数

    空间复杂度:

    代码

    class Solution {
    public:
        vector<int> countBits(int n) {
            vector<int> res;
            for (int i=0;i<=n;i++){
                res.push_back(count(i));
            }
            return res;
        }
        
        int count(int n){
            n=(n&0x55555555)+((n>>1)&0x55555555);
            n=(n&0x33333333)+((n>>2)&0x33333333);
            n=(n&0x0f0f0f0f)+((n>>4)&0x0f0f0f0f);
            n=(n&0x00ff00ff)+((n>>8)&0x00ff00ff);
            n=(n&0x0000ffff)+((n>>16)&0x0000ffff);
            return n;
        }
    };

    762. 二进制表示中质数个计算置位

    题意

    判断 1 的个数是质数的数

    分析

    将其拆分为两个过程:

    1. 获取一个数中 1 的个数
    2. 判断该数是否是质数

    第一步参考 191,用分治。第二步按理说应该遍历判断的,不过这题给定的范围,数最大为 ,即 1 的个数最大为 19,则我们直接打表出可能是质数的数判断即可。

    优化

    效率

    时间复杂度:,k 为 1 的个数

    空间复杂度:

    代码

    class Solution {
    public:
        int countPrimeSetBits(int left, int right) {
            int res=0;
                    
            for (int i=left;i<=right;i++){
                if(isPrime(count(i))){
                    res++;
                }
            }
                    
            return res;
        }
        
        int count(int n){
            n=(n&0x55555555)+((n>>1)&0x55555555);
            n=(n&0x33333333)+((n>>2)&0x33333333);
            n=(n&0x0f0f0f0f)+((n>>4)&0x0f0f0f0f);
            n=(n&0x00ff00ff)+((n>>8)&0x00ff00ff);
            n=(n&0x0000ffff)+((n>>16)&0x0000ffff);
            return n;
        }
        
        bool isPrime(int n){
            return n==2||n==3||n==5||n==7||n==11||n==13||n==17||n==19;
        }
    };

    1356. 根据数字二进制下 1 的数目排序

    题意

    先按数中 1 的个数排序,再按大小排序

    分析

    这题其实不难,就考察获取 1 的个数和传入的比较函数写法。获取 1 的个数参考 191 用分治即可,而重写比较函数则比较麻烦,不熟悉就会出问题。(和算法没啥关系,就是语言熟悉程度)。

    需要注意有两点:

    1. sort 的第三个变量必须是 static 或全局变量
    2. 排序要二次比较

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    // sort 的第三个变量必须是 static 或全局变量
    int count(int x){
        x=(x&0x55555555)+((x>>1)&0x55555555);
        x=(x&0x33333333)+((x>>2)&0x33333333);
        x=(x&0x0f0f0f0f)+((x>>4)&0x0f0f0f0f);
        x=(x&0x00ff00ff)+((x>>8)&0x00ff00ff);
        x=(x&0x0000ffff)+((x>>16)&0x0000ffff);            
        return x;
    }
    
    bool cmp(int a,int b){
        int ca=count(a);
        int cb=count(b);
        
    // 先比较 1 的个数是否相等
        if (ca==cb){
            return a<b; // 相等则返回数小的个
        }
        return ca<cb; // 1 的个数不等,则返回数小的
    }
    
    class Solution {
    public:        
        vector<int> sortByBits(vector<int>& arr) {
            sort(arr.begin(),arr.end(),cmp);
            return arr;
        }
    };

    461. 汉明距离

    题意

    统计不同的位数个数

    分析

    不同 → 异或为 1,故我们将两数异或,就转换为求 1 的个数。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int hammingDistance(int m, int n) {
            int x=m^n;
            x=(x&0x55555555)+((x>>1)&0x55555555);
            x=(x&0x33333333)+((x>>2)&0x33333333);
            x=(x&0x0f0f0f0f)+((x>>4)&0x0f0f0f0f);
            x=(x&0x00ff00ff)+((x>>8)&0x00ff00ff);
            x=(x&0x0000ffff)+((x>>16)&0x0000ffff);         
            return x;
        }
    };

    面试题 05.06. 整数转换

    同 461,只是注意溢出。

    477. 汉明距离总和

    题意

    计算任意两数间的汉明距离

    分析

    这题是 461 题的拓展,461 又是 191 的拓展,故最简单的自然是枚举任意两数异或,然后统计 1 的个数即可。

    优化

    不出意外的,枚举超时了,只能考虑优化。而很多优化都是展开固定的集合,比如字符串的题里面就有很多展开字符的,位运算里则有很多展开位数的。

    所以这题我们可以考虑遍历每一位下每个数的和,然后再统计,比如某位 1 的个数为 n,则 0 的个数为 nums.size()-n,则一共有 个不同的数。(这个即简单的组合数学,0 里面挑一个,1 里面挑一个)

    效率

    时间复杂度:优化前 ,优化后

    空间复杂度:

    代码

    暴力

    class Solution {
    public:
        int totalHammingDistance(vector<int>& nums) {
            int res=0;
            
            for (int i=0;i<nums.size();i++){
                for (int j=i+1;j<nums.size();j++){
                    res+=count(nums[i]^nums[j]);
                }
            }        
            return res;
        }
        
        int count(int x){
            x=(x&0x55555555)+((x>>1)&0x55555555);
            x=(x&0x33333333)+((x>>2)&0x33333333);
            x=(x&0x0f0f0f0f)+((x>>4)&0x0f0f0f0f);
            x=(x&0x00ff00ff)+((x>>8)&0x00ff00ff);
            x=(x&0x0000ffff)+((x>>16)&0x0000ffff);            
            return x;
        }
    };

    优化

    class Solution {
    public:
        int totalHammingDistance(vector<int>& nums) {
            int res=0;
            int cnt=0;
    
            for (int i=0;i<32;i++){
                cnt=0;
                for (int j=0;j<nums.size();j++){
                    cnt+=getBit(nums[j],i);
                }
                res+=(cnt*(nums.size()-cnt));
            }        
            return res;
        }
        
        int getBit(int x,int pos){
            return (x>>pos)&1;
        }
    };

进制

进制题设计不同数进制间转换问题。

  • list

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

    题意

    将十进制转换为十六进制

    分析

    当为正数时,进制的转换就和普通的进制转换没有区别,就是辗转相除法。问题在于当数为负数时。

    我们一般辗转相除法是这样的

    num%16
    num/=16

    但是,当 num 为负数时,上面的做法就行不通了,因为上面的做法都是基于十进制的,所以,为了通用,而且由于 16 是 2 的幂,所以我们都转换为二进制操作。

    而 4 位二进制就转换为一个十六进制,所以我们每次取低 4 位组成一个十六进制,然后右移 4 位即可。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        string toHex(int num) {
            string res;        
            
            if (num==0){
                return "0";
            }                
            
            if (num>0){
                while(num!=0){
                    res.insert(res.begin(),toChar(num&0xf));
                    num>>=4;                            
                }
                return res;
            } 
            
            for (int i=0;i<8;i++){
                res.insert(res.begin(),toChar(num&0xf));
                num>>=4;            
            }
            
            return res;
        }
        
        char toChar(int x){
            switch(x){
                case 10:
                    return 'a';
                case 11:
                    return 'b';
                case 12:
                    return 'c';
                case 13:
                    return 'd';
                case 14:
                    return 'e';
                case 15:
                    return 'f';                
                default:
                    return x+'0';
            }
        }
    };

    504. 七进制数

    题意

    十进制转换为七进制

    分析

    类似于 405 题转换为十六进制,同样分正负两种情况。正数直接辗转相除法即可得到答案,问题是当数是负数时该怎么处理。

    在 405 中,由于负数是用补码表示的,所以要将其转换为二进制处理。而在这个题中,正负的区别只是符号的区别,故我们提前预处理一下,将负数转换为正负,最后再考虑增加负号即可。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        string convertToBase7(int num) {
            if (num==0){
                return "0";
            }
            
            string res;
            string chars="0123456";
            bool sign=false;
            
            if (num<0){
                sign=true;
                num=-num;
            }
                    
            while(num!=0){
                res.insert(res.begin(),chars[num%7]);
                num/=7;
            }
              
            if (sign){
                res.insert(res.begin(),'-');
            }
            
            return res;
        }
    };

    面试题 05.02. 二进制数转字符串

    题意

    将十进制浮点数转换为二进制字符串

    分析

    由于给定范围就在 0~1,故无序取整转换,且由于小于 0,故只需转换小数部分即可。原理就是小数部分乘 2,然后判断是否进位即可。

    而要判断是否能够转换成功,即精度超过 32 位,那么最后判断一下转换后的结果是否超过 32 位即可。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        string printBin(double num) {
            int a;
            string res="0.";
            
            while(num!=0){
                a=0;
                num*=2;
                
                a=(int)num;
                if (a==1){
                    num--;
                }
                res+=(a+'0');          
            }
            
            return res.size()<32?res:"ERROR";
        }
    };

    1016. 子串能表示从 1 到 N 数字的二进制串

    题意

    判断 1~N 的所有元素的二进制是否是 s 的子串

    分析

    最简单的思路分成三步:

    1. 遍历数字
    2. 将数字转换成二进制字符串
    3. 判断是否是 s 的子串

    这是最暴力的做法

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        bool queryString(string s, int n) {
            for (int i=1;i<=n;i++){
                if (s.find(toString(i))==-1){
                    return false;
                }
            }
            
            return true;
        }
        
        string toString(int x){
            string res;
            string chars="0123456789";
            
            while(x!=0){
                res.insert(res.begin(),chars[x&1]);
                x>>=1;
            }
            
            return res;
        }
    };

    1980. 找出不同的二进制字符串

    题意

    找出不在数组中的同长度数字

    分析

    最简单的,自然是把字符串数组转换为数字,然后用哈希表存起来。再从 0 递增遍历判断谁不在哈希表中即可。

    需要注意的是,转换成数字需要是十进制才行,如果是二进制,会溢出。只需要编写字符串和数字之间转换的函数即可。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        string findDifferentBinaryString(vector<string>& nums) {
            unordered_map<int,bool> m;
            
            for (int i=0;i<nums.size();i++){
                m[toInt(nums[i])]=true;
            }
            
            int n=0;
            
            while(m.count(n)!=0){
                n++;
            }
                    
            return toString(n,nums[0].size());
        }
        
        int toInt(string s){
            int res=0;
                
            for (int i=0;i<s.size();i++){
                res+= (((s[s.size()-1-i]-'0')&1)<<i);
            }
    
            return res;
        }
        
        string toString(int n,int length){
            string res;
            
            while(n!=0){
                res.insert(res.begin(),'0'+(n&1));
                n>>=1;
            }
            
            while(res.size()<length){
                res.insert(res.begin(),'0');
            }
            
            return res;
        }
    };

    1837. K 进制表示下的各位数字总和

    题意

    转换为 k 进制

    分析

    最基本和基础的进制转换题,直接辗转相除法即可。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int sumBase(int n, int k) {
            int res=0;
            
            while(n!=0){
                res+=(n%k);
                n/=k;
            }
            
            return res;
        }
    };

    1018. 可被 5 整除的二进制前缀

    题意

    求一个数是否被 5 整除

    分析

    给定的数组转换为对应的数很简单,从左至右扫描就行了,问题在于怎么判断是否被 5 整除,如果是 2 的幂的话,移位比较方便,也不会溢出。但是 5 就不行了,显然只能 %5,但是由于数组长度为 30000,所以哪怕是 long long 也会超时(第一次就超时了)。

    所以,这里就涉及到了大数求余问题,从结论简单来说,每次对 5 取余再加减,对结果不会影响。也就是同余定理。

    同余定理在许多地方应用广泛,前缀和,滑动窗口,位运算,数学等等 tag 都有它的身影,甚至我做离散数学时都遇到过几次证明群。因此这里做一个总结:

    若 a%p=b%p,c%p=d%p
    
    1. (a+c)%p=(b+d)%p
    2. (a-c)%p=(b-d)%p
    3. (a*c)%p=(b*d)%p
    
    若 a%p=b%p,则
    1. a^n%p=b^n%p
    
    常见的有
    (a + b) % p = (a % p + b % p) % p
    (a - b) % p = (a % p - b % p + p) % p // 可能差是负数
    (a * b) % p = (a % p * b % p) % p
    (a^b) % p = ((a % p)^b) % p

    换言之,一个很大的数可能继续相加会溢出,那我们先 %p 再加减不会影响结果。

    两个数相乘的结果可能溢出,我们先分别%p再乘也不会影响结果。

    https://zh.wikipedia.org/zh-hans/同餘

    https://zhuanlan.zhihu.com/p/96666921

    https://jason87459473.wordpress.com/2018/09/10/同余定理/

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        vector<bool> prefixesDivBy5(vector<int>& nums) {
            vector<bool> res;
            int data=0;
            
            for (int i=0;i<nums.size();i++){
                data=data%5; // 关键点
                data+=data;
                data+=nums[i];
                res.push_back(data%5==0);
            }
              
            return res;
        }
    };

    1017. 负二进制转换

    题意

    分析

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        string baseNeg2(int n) {
            if (n==0){
                return "0";
            }
            
            string res;
            while(n!=0){
                res.push_back((n&1)+'0');            
                n=-(n>>1);
            }        
            reverse(res.begin(),res.end());
            return res;
        }
    };

    1073. 负二进制数相加

    题意

    分析

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        vector<int> addNegabinary(vector<int>& arr1, vector<int>& arr2) {
            int a=0;
            int b=0;
            int t=0;
            vector<int> res;
            
            for (int i=arr1.size()-1;i>-1;i--){            
                a+=(pow(-2,(arr1.size()-1-i))*arr1[i]);
            }
            
            for (int i=arr2.size()-1;i>-1;i--){
                b+=(pow(-2,(arr2.size()-1-i))*arr2[i]);
            }
            
            t=a+b;
            while(t!=0){
                res.push_back(t&1);            
                t=-(t>>1);
            }        
            
            if (res.empty()){
                res.push_back(0);
            }
            reverse(res.begin(),res.end());        
            return res;
        }
    };

运算模拟

用位运算模拟另一个运算,如加减乘除,还有一些用数组,字符串进行模拟加减乘除的,原理都差不多。

  • list

    371. 两整数之和

    题意

    用位运算实现加法

    分析

    这题其实就是考察加法器的实现原理,如果学过数电的话,就比较简单了,当然没学过也没关系,我们来思考一下,我们手动计算加法时,是怎么计算的。比如 123+789

    无效的图片地址

    对于每一位,我们就是对应位相加,然后再加上低位的进位。比如 3+9 没有低位进位,为 2,2+8 有低位进位,为 1,7+1有低位进位,为 9,最后就是 912.

    所以对于每一位,计算公式就是 sum=a+b+cf。cf 就是低位的进位。

    而对于计算机来说,底层的加法其实就是这个原理,稍微上层的 add 指令,或者用 32 位寄存器模拟 64 位加法的 adc 指令,以及更高层的 c 语法中的 + 运算,全都是这个原理。同理,减法 sub,sbb 就是减去借位 cf。

    同理,二进制加法也是这个原理,直接两数 a+b 相加的电路叫半加器,位运算中异或 ^ 就是一个半加器,即 a^b 就是 a 和 b 不考虑进位的加法。而加上进位的电路就叫全加器。

    原始模拟

    原理已经理解了,那我们直接来模拟吧,就是从最低位开始向高位扫描,每一位半加再加上进位,然后再保存向高位的进位即可。

    class Solution {
    public:
        int add(int a, int b) {
            int res=0;
            int cf=0; 
            int tmpSum=0;
            
            for (int i=0;i<32;i++){
    						// 异或就是半加器
                tmpSum=getBit(a,i) ^ getBit(b,i) ^ cf; // 获取当前位应该的值
                if (tmpSum==0){
                    res=unsetBit(res,i);
                } else {
                    res=setBit(res,i);
                }
                
                cf=(getBit(a,i)+getBit(b,i)+cf)>1?1:0; // 获取向高位的进位
            }
            
            return res;
        }
        
        int flapBit(int x,int pos){
            return x^(1<<pos);
        }
        
        int setBit(int x,int pos) {
            return x|(1<<pos);
        }
        
        int unsetBit(int x,int pos){
            return x&(~(1<<pos));
        }
        
        int getBit(int x,int pos){
            return (x>>pos)&1;
        }
    };

    直接模拟的话,时间复杂度扫一遍,应该是 .

    32 位一起加

    前面我们是一位一位的扫描,每次就把一位给加好了。其实在位运算中,经常会有这种方法:需要对每一位进行操作,那么可以并行同时对 32 位一起进行。

    在这里也是一样的,我们每一位都需要做两个加法,即那么我们 32 位一起进行操作。

    1. 半加:a^b
    2. 和进位半加

    但是由于每一位半加之后,低位可能对进位半加又进位,所以要一直半加,直到没有进位了为之。(好吧,这个的确没有一位位的模拟好理解。)

    class Solution {
    public:
        int add(int a, int b) {
            int sum=a;
            int cf=b;
            
            while(b!=0){            
                sum=a^b;
                cf=(uint32_t)(a&b)<<1;
                a=sum;
                b=cf;
            }
            
            return sum;
        }
    };

    时间复杂度自然也是 .

    递归实现

    在 32 位一起加的过程中,我们对每一位进行的操作其实都是一样的,那么自然也能写成递归形式。

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

    复杂度应该也是 .

    剑指 Offer 65. 不用加减乘除做加法

    同 371

    面试题 17.01. 不用加号的加法

    同 371

    67. 二进制求和

    题意

    字符串模拟实现加法器

    分析

    和 371 一个原理,只是变成字符串存储了而已,还是一样的半加+进位,再向高位进位。

    需要注意的是,由于初始字符串不对齐,所以需要提前预处理一下。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        string addBinary(string a, string b) {
            char cf='0';
            string res;
            
            // 预处理,将两字符对齐,并且增加一个高位
            if (a.size()>b.size()){
                a.insert(a.begin(),'0');                           
                int m=a.size()-b.size();
                for (int i=0;i<m;i++){
                    b.insert(b.begin(),'0');
                }
            } else if (a.size()<b.size()){
                b.insert(b.begin(),'0');
                int m=b.size()-a.size();
                for (int i=0;i<m;i++){
                    a.insert(a.begin(),'0');
                }
            } else {
                b.insert(b.begin(),'0');
                a.insert(a.begin(),'0');
            }
            
            // 遍历模拟全加器
            for (int i=a.size()-1;i>-1;i--){
                res.insert(res.begin(),add(a[i],b[i],cf));            
                cf=getCarry(a[i],b[i],cf);
            }
    
            // 最后消除高位 0
            if (res[0]=='0'){
                res.erase(res.begin());
            }
            
            return res;
        }
        
        char add(char a,char b,char cf){
            return (((a+b+cf-'0'-'0')=='0') || ((a+b+cf-'0'-'0')=='2'))?'0':'1';
        }
        
        char getCarry(char a,char b,char cf) {
            return ((a+b+cf-'0'-'0')>='2')?'1':'0';
        }
    };

    剑指 Offer II 002. 二进制加法

    同 67

    415. 字符串相加

    题意

    字符串模拟十进制加法器

    分析

    原理同 67,只是这题变成十进制了而已,那么进位和半加更改一下判断即可。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        string addStrings(string a, string b) {
            char cf='0';
            string res;
            
            // 预处理,将两字符对齐,并且增加一个高位
            if (a.size()>b.size()){
                a.insert(a.begin(),'0');                           
                int m=a.size()-b.size();
                for (int i=0;i<m;i++){
                    b.insert(b.begin(),'0');
                }
            } else if (a.size()<b.size()){
                b.insert(b.begin(),'0');
                int m=b.size()-a.size();
                for (int i=0;i<m;i++){
                    a.insert(a.begin(),'0');
                }
            } else {
                b.insert(b.begin(),'0');
                a.insert(a.begin(),'0');
            }
                    
            // 遍历模拟全加器
            for (int i=a.size()-1;i>-1;i--){
                res.insert(res.begin(),add(a[i],b[i],cf));            
                cf=getCarry(a[i],b[i],cf);
            }
    
            // 最后消除高位 0
            if (res[0]=='0'){
                res.erase(res.begin());
            }
            
            return res;
        }
        
        char add(char a,char b,char cf){            
            char c = a+b-'0';
            if (c>'9'){
                c=c-'9'+'0'-'1'+'0';
            }
            c=c+cf-'0';      
            if (c>'9'){
                c=c-'9'+'0'-'1'+'0';
            }
            return c;
        }
        
        char getCarry(char a,char b,char cf) {
            return ((a+b+cf-'0'-'0')>'9')?'1':'0';
        }
    };

    66. 加一

    题意

    实现全加器,一个操作数固定为 1

    分析

    1. 对齐
    2. 模拟全加器,即位相加再进位

    最后清除高位 0 即可,可参考 989,415,67,371 等题

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        vector<int> plusOne(vector<int>& nums) {
            int cf=1;
            nums.insert(nums.begin(),0);
            int tmp;
            
            
            for (int i=nums.size()-1;i>-1;i--){
                tmp=nums[i];
                nums[i]=getHalfSum(nums[i],cf);
                cf=getCarry(tmp,cf);
            }
            
            if (nums[0]==0){
                nums.erase(nums.begin());
            }
            
            return nums;
        }
        
        int getHalfSum(int a,int cf){
            if ((a+cf)>9){
                return a+cf-10;
            } 
            return a+cf;
        }
        
        int getCarry(int a,int cf){
            return (a+cf)>9?1:0;
        }
    };

    989. 数组形式的整数加法

    题意

    一个数组,一个整数,求和

    分析

    最简单的自然是数组转换成整数,然后求和再转换会数组。不过这样就把求和的过程交给底层去做了,这题就没意义了。为了提高难度,所以反过来,手动模拟加法过程。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        vector<int> addToArrayForm(vector<int>& a, int k) {
            vector<int> b,res;
            int cf=0;
            
            while(k!=0){
                b.insert(b.begin(),k%10);            
                k/=10;
            }
            
            // 预处理,将两字符对齐,并且增加一个高位
            if (a.size()>b.size()){
                a.insert(a.begin(),0);                           
                int m=a.size()-b.size();
                for (int i=0;i<m;i++){
                    b.insert(b.begin(),0);
                }
            } else if (a.size()<b.size()){
                b.insert(b.begin(),0);
                int m=b.size()-a.size();
                for (int i=0;i<m;i++){
                    a.insert(a.begin(),0);
                }
            } else {
                b.insert(b.begin(),0);
                a.insert(a.begin(),0);
            }
            
            // 遍历模拟全加器
            for (int i=a.size()-1;i>-1;i--){
                res.insert(res.begin(),getHalfSum(a[i],b[i],cf));            
                cf=getCarry(a[i],b[i],cf);
            }
    
            // 最后消除高位 0
            if (res[0]==0){
                res.erase(res.begin());
            }
            
            return res;
        } 
        
        
        int getHalfSum(int a,int b,int cf){
            if ((a+cf+b)>9){
                return a+b+cf-10;
            } 
            return a+cf+b;
        }
        
        int getCarry(int a,int b,int cf){
            return (a+cf+b)>9?1:0;
        }
    };

    43. 字符串相乘

    题意

    手动模拟乘法过程

    分析

    和题意一样,完全手动模拟。

    1. 写出基本的字符串加法函数
    2. 写出两个字符相乘的函数
    3. 写出一个字符和字符串相乘的函数
    4. 写出两个字符串相乘的函数

    优化

    无(?)

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        string multiply(string num1, string num2) {
            if (num1=="0"||num2=="0"){
                return "0";
            }
            
            string res="0";
            string tmp;
            
            for (int i=num1.size()-1;i>-1;i--){
                tmp=multiplySingle(num1[i],num2);
                for (int j=0;j<num1.size()-i-1;j++){
                    tmp.push_back('0');
                }
                res=addStrings(res,tmp);
            }
            
            return res;
        }
        
        string multiplySingle(char a,string b){
            string res;
            string tmp;
            
            for (int i=b.size()-1;i>-1;i--){
                tmp=mul(a,b[i]);
                for (int j=0;j<b.size()-i-1;j++){
                    tmp.push_back('0');
                }
                res=addStrings(res,tmp);
            }
            
            return res;
        }
        
        string mul(char c1,char c2){
            string res;
            int m=(c1-'0')*(c2-'0');        
            res.push_back((m/10)+'0');
            res.push_back((m%10)+'0');
            if (res[0]=='0'){
                res.erase(res.begin());
            }
            return res;
        }
        
        string addStrings(string a, string b) {
            char cf='0';
            string res;
            
            // 预处理,将两字符对齐,并且增加一个高位
            if (a.size()>b.size()){
                a.insert(a.begin(),'0');                           
                int m=a.size()-b.size();
                for (int i=0;i<m;i++){
                    b.insert(b.begin(),'0');
                }
            } else if (a.size()<b.size()){
                b.insert(b.begin(),'0');
                int m=b.size()-a.size();
                for (int i=0;i<m;i++){
                    a.insert(a.begin(),'0');
                }
            } else {
                b.insert(b.begin(),'0');
                a.insert(a.begin(),'0');
            }
                    
            // 遍历模拟全加器
            for (int i=a.size()-1;i>-1;i--){
                res.insert(res.begin(),add(a[i],b[i],cf));            
                cf=getCarry(a[i],b[i],cf);
            }
    
            // 最后消除高位 0
            if (res[0]=='0'){
                res.erase(res.begin());
            }
            
            return res;
        }
        
        char add(char a,char b,char cf){            
            char c = a+b-'0';
            if (c>'9'){
                c=c-'9'+'0'-'1'+'0';
            }
            c=c+cf-'0';      
            if (c>'9'){
                c=c-'9'+'0'-'1'+'0';
            }
            return c;
        }
        
        char getCarry(char a,char b,char cf) {
            return ((a+b+cf-'0'-'0')>'9')?'1':'0';
        }
    };

    面试题 08.05. 递归乘法

    题意

    递归实现十进制乘法

    分析

    乘法本质是加法,而且十进制和二进制方法也有一些不同。对于十进制,最简单的自然是相加,即 A*B=A 个 B 相加,无论递归还是迭代都很容易写出。

    优化

    对加法的优化基本思想类似于快速幂,都是分治的思想,而在乘法中,有一个稍微更简便的算法,即俄夫乘法

    而具体实现也有递归和迭代方式,这题要递归,而分治快速幂的话,更多是迭代利用过去求得的结果来算的。

    而乘法还有其它一些实现,必然手动模拟乘法,字符串高精度就是这么算的。参考 43 题,还有二进制乘法,也可以模拟手动或换成加法来算。

    效率

    时间复杂度:

    空间复杂度:

    代码

    简单加法

    class Solution {
    public:
        int multiply(int A, int B) {
            if (B==0){
                return 0;
            }
            return A+multiply(A,B-1);
        }
    };

    分治优化

    class Solution {
    public:
        int multiply(int A, int B) {
            if (A==1){
                return B;
            }
            if ((A&1)==0){
                return multiply(A>>1,B<<1);
            }
            return multiply((A-1)>>1,B<<1)+B;
        }
    };

    再优化为一个调用(A 为奇数时,除以二的结果是一样的,所以只需要根据 A 的奇偶判断是否加一个 B 即可)

    class Solution {
    public:
        int multiply(int A, int B) {
            int res=B;        
            (A!=1)&&(res=(((-(A&1))&B)+multiply(A>>1,B<<1)));        
            return res;
        }
    };

    29. 两数相除

    题意

    模拟除法

    减法

    除法本质上是减法,因此我们可以模拟减法的过程,而且这题只求商,商就是指 a 能分成多少个 b,即 a-kb 必然刚刚够分,故我们直接每次减去一个 b,然后看最后什么时候不够分就行。

    只是需要注意溢出,以及正负,所以可能是加可能是减,判断的条件也可能是大于 0 或小于 0.

    减法变形

    由于直接模拟减法展开两数分正负,有 4 种情况过于复杂。故我们考虑优化:提前将其变成同号再计算。

    而 int 的范围在 中,故负数的范围大稍微多一点,如果转换为正数的话,那么 转换为对应的相反数 就会溢出。

    故我们将其转换为负数。

    而由于根据是否同号,最后的商也分正负,故我们需要提前记录下是否同号,然后最后再将商转换一下即可。

    无论是减法和减法变形,其时间复杂度都非常的高,取决于商的大小。为 如果商非常大,如 ,就要循环 次,显然会超时。

    模拟减法展开

    class Solution {
    public:
        int divide(int a, int b) {
            if (a==INT_MAX && b==1){
                return INT_MAX;
            }
            if (a==INT_MAX && b==-1){
                return INT_MIN;
            }
            if (a==INT_MIN && b==1){
                return INT_MIN;
            }
            if (a==INT_MIN && b==-1){
                return INT_MAX;
            }
            if (a==INT_MAX && b==INT_MAX){
                return 1;
            }
            if (a==INT_MIN && b==INT_MIN){
                return 1;
            }
     
            if (a==0 || b==0){
                return 0;
            }
            
            int res=0;
            bool f=(a^b)<0;
            
            if (a>0 && b>0){
                while(a>=0){
                    res++;
                    a=a-b;
                } 
            }else if (a>0 && b<0){
                while(a>=0){
                    res++;
                    a=a+b;
                }            
            } else if (a<0 && b>0){
                while(a<=0){
                    res++;
                    a=a+b;
                }
            } else if (a<0 && b<0){
                 while(a<=0){
                    res++;
                    a=a-b;
                }           
            }
            res--;
            
            if (f) {
                res=(~res)+1;
            }
            
            return res;
        }
    };

    减法优化

    class Solution {
    public:
        int divide(int a, int b) {
            int res=-1;
            
            if (a==INT_MIN && b== -1){
                return INT_MAX;
            }
            if (b==0 || a==0){
                return 0;
            }
            
            // true 则同号
            bool sign = ((a^b)>=0);
    
            int abs_a=(a<0)?a:(~a+1);
            int abs_b=(b<0)?b:(~b+1);
            
            while(abs_a<=0){
                res++;
                abs_a-=abs_b;
            }
            
            return sign?res:(~res+1);
        }
    };

    减法倍增优化

    class Solution {
    public:
        int divide(int a, int b) {
            int res=0;
            
            if (a==INT_MIN && b== -1){
                return INT_MAX;
            }
            if (a==INT_MIN && b== 1){
                return INT_MIN;
            }
            if (b==0 || a==0){
                return 0;
            }
            
            // true 则同号
            bool sign = ((a^b)>=0);
    
            int abs_a=(a<0)?a:(~a+1);
            int abs_b=(b<0)?b:(~b+1);
            
            if (abs_a>abs_b){
                return 0;
            }
            res=div(abs_a,abs_b);
            return sign?res:(~res+1);
        }
        
        int div(int a,int b) {
            if (a > b) { // 当分母比分子大时,返回 0
                return 0;        
            }
            
            int count=1;
            int tmpB=b;
            
            while(a<=(tmpB+tmpB)) { // 迭代时,倍增 b,看最多能减到什么时候
                count += count;
                tmpB += tmpB;            
            }
                    
            return count + div(a-tmpB,b); // 递归时,则减 a,b 不变,余下的 a 又重新开始倍增
        }
    };

    剑指 Offer II 001. 整数除法

    同 29

    面试题 16.07. 最大数值

    题意

    用位运算找出大的数

    分析

    基本思路是:

    实现三目表达式

    首先我们来看,三目表达式的基本格式是怎么样的

    cond?x:y;

    cond 是逻辑表达式,x 是 cond 为真即 1 是的 值,y 是 cond 为 0 的值。

    显然,这是一个分支 ,但是我们不能用分支语句,所以必然的,我们要想办法实现分支语句

    基本思路有两个

    1. 用逻辑表达式的短路特性
    2. 用位运算的 | 运算

    逻辑表达式参考 offer 64 题中的思路,即利用 A&&B,当 A 为 false 时,B 不会执行,这就是个分支语句。

    实现如下

    class Solution {
    public:
        int maximum(int a, int b) {
            return branch(a>b,a,b);
        }
        
        int branch(bool cond,int x,int y){
            int res=y;
            (cond)&&(res=x);
            return res;
        }
    };

    短路特性比较巧妙,而且是个语言特性,我们来继续看位运算的方式。

    要实现分支,其实最简单的就是逻辑表达式中的或逻辑,即 |,即实现如

    x|y

    类似这种形式的表达式,就能得到结果,也就是说,我们要自己构造逻辑表达式

    而类似要得到分支的结果,我们需要一方等于 0 才行,即 0 | a 这种形式,而要得到 0,显然需要与操作才行。

    也就是说,我们需要得到

    (?&x)|(?&y)

    这种格式的一个表达式,而 ? 就是根据 cond 进行一些变化得到的。

    在或表达式中,除了一方要得到 0 之外,另一方显然应该得到正确的结果,即 x 或 y。也就是说:在 | 的两方,要是一个得到 0,一个保持不变。

    而有时与运算,显然

    1. 与 0 相与即为 0,即 x&0=0
    2. 与 -1 相与即不变,即 x&-1=x (-1 即全 1 ffffff,相与不变)

    换句话说,我们需要根据输入的 cond 是 1 或者 0,得到一个为 0,一个为 -1 的结果。

    但是 -1 显然太麻烦了,如果是得到 0 或 1 的话,就比较方便。而 0 取负数实际上还是 0,所以,我们只需要知道:

    怎么根据输入 0 或 1,得到 0,1 或 1,0 即可。

    假设我们需要得到的表达式为

    (m&x)|(n&y)

    而上面要得到的显然是一个真值表,所以我们画出真值表

    无效的图片地址

    而又要取个负数,故

    则最终的逻辑表达式,应该是

    则能写出代码如下

    class Solution {
    public:
        int maximum(int a, int b) {
            return branch(a>b,a,b);
        }
        
        int branch(bool cond,int x,int y){
            return ((-cond)&x)|((-(!cond))&y);
        }
    };

    上面我们用到了 表达式,似乎也是个表达式,干脆都换成位运算,即 0 变成 1,1 变成 0,这个操作就等于和 1 异或的结果。则

    class Solution {
    public:
        int maximum(int a, int b) {
            return branch(a>b,a,b);
        }
        
        int branch(bool cond,int x,int y){
            return ((-cond)&x)|((-(cond^1))&y);
        }
    };

    当然,除了上面这种分析方法,还有其它一些方法,比如下面这个表达式也实现了三目表达式,原理我还没看懂

    class Solution {
    public:
        int maximum(int a, int b) {
            return branch(a>b,a,b);
        }
        
        int branch(bool cond,int x,int y){
    	    return y ^ ((x ^ y) & -cond);
        }
    };

    优化

    效率

    时间复杂度:

    空间复杂度:

    剑指 Offer 64. 求 1+2+…+n

    题意

    不用分支,循环,乘除,三目表达式求和

    分析

    这题很骚,我学到不少东西,我们来一个个的分析

    要求很多数的和,自然两个思路:

    1. 用加法
    2. 用乘法

    用加法的基础,显然有循环和递归两种方式,而这里不用循环。所以:我们只能用递归代替循环,这是第一点。

    比如随手一个循环

    class Solution {
    public:
        int sumNums(int n) {
            int res=0;
            for (int i=0;i<=n;i++){
                res+=i;
            }
            return res;
        }
    };

    用于用了循环,所以我们考虑递归

    class Solution {
    public:
        int sumNums(int n) {
            if (n==0){
                return 0;
            }
            return n+sumNums(n-1);
        }
    };

    显然,递归必然要考虑递归出口的问题,也就是说,必然会用到分支,但是分支语句又不让用,怎么办呢?

    我考虑过用位运算实现三目表达式,比如这样

    class Solution {
    public:
        int sumNums(int n) {   
            return n+sumNums(n-1)^(sumNums(n-1)&(-(n==0)));        
        }
    };

    如果是普通的数的话,结果上是没问题的,但是这是函数,所以还是会一直递归调用,最后会栈溢出。所以,我们得必须解决分支的问题。

    想了半天,没有办法,只能看题解了,题解利用了逻辑表达式中的短路原理,即

    A && B,如果 A 为 false,则 B 不会执行。同理,A || B ,如果 A 为 true,B 也不会执行。这其实就是个分支语言,只不过是简单的 if 单分支而已。所以,用逻辑表达式短路原理实现分支,这就是这个题的第二个难点了。

    所以我们可以写出下面语句

    class Solution {
    public:
        int sumNums(int n) {   
            int res=0;
            (n!=0)&&(res=(n+sumNums(n-1)));
            return res;   
        }
    };

    现在当我们有了

    1. 用递归实现循环
    2. 用短路实现分支

    这两个利器之后,基本的其它方法都可以实现了。

    比如说,由于是等差公式,所以还可以用求和公式简化,即

    但是,这里出现了乘法 ,而我们又不允许使用乘法,所以,又用位运算自己实现了个乘法,原理是俄夫乘法。

    class Solution {
    public:
        int sumNums(int n) {   
            return multiply(n,n+1)>>1;
        }
    
        int multiply(int A, int B) {
            int res=B;        
            (A!=1)&&(res=(((-(A&1))&B)+multiply(A>>1,B<<1)));        
            return res;
        }
    };

    上面这个可以说基本压缩到极限了,我们用了递归,位运算,短路原理等。

    其实这个题还有一个更骚的算法,把空间换时间的思想也用到了极限,不,应该叫直接利用空间大小得出答案。是的,你已经想到了,由于 sum=(n*(n+1) )/2,而 nm 的大小和二维数组大小计算方式一样,所以,我们可以定义一个 n(n+1) 大小的数组,然后除以二(右移1位),就能得到答案。

    class Solution {
    public:
        int sumNums(int n) {
            char a[n][n+1];
            return sizeof(a)>>1;
        }
    };

    效率

    时间复杂度:加法的复杂度为 ,乘法主要是分治,为 ,而利用数组则是

    空间复杂度:

    用或、与实现异或

    题意

    用 | & 实现 ^

    分析

    这是 CSAPP 上的一个 lab,顺便做了.

    像这种题第一反应就是画真值表再看,毕竟是典型的电路设计题。

    无效的图片地址

    可以看出来,重点是中间两行的结果要为 1

    无效的图片地址

    而与都为 0,故考虑将与取反再看

    无效的图片地址

    这下就很清晰了,直接只有中间两行都是其,其它都至少有个 0,而这和相与的规则是一样的,故相与即可。即

    先 AB 相与取反,再 AB 相或,最后再与起来即可。

    而我们在这里用到了取反操作,最简单的自然是 ,不过既然都使用位运算,干脆都用位运算实现。而 0 和 1 之间的取反操作最简单的是直接和 1 异或,即 x^1,但是,我们这里就是要实现异或,所以显然不行。

    我们先来分析一下 0 和 1 的二进制,看看它们之间有什么规律:

    0 → 0000 0000 → 全 0

    1 → 0000 0001 → 只有最低位为 1

    从上面可以看出来, 0 和 1 之间的区别就是最低位相反,而这就很简单了,不用异或实现取反,还有按位取反的 ~ 操作,但是 ~ 所有位的都反,就都变成 1 了,而非最低位应该是 0 才对,所以我们将其它位清 0,而清零操作就是和 0000 00001 即 1 相与即可。

    故,最终可写出代码如下

    int getXor(int a,int b) {
    	return ((~(a&b))&1)&(a|b);
    }

n 的幂

问一个数是否是 n 的幂,解法比较多,有如下几种思路:

  1. 先判断该数的质因子是什么,然后最大值整除,即同余定理(n 是指数)
  2. 转换成其它幂(n 是 2 的倍数,或可开方等)
  3. 分析该数二进制规律(通用)
  4. 打表枚举(通用)
  5. 辗转相除法(通用)
  • list

    231. 2 的幂

    题意

    判断数是否是 2 的幂

    辗转相除法

    最简单和暴力的思路,如果是 n 的幂,那么说明值就是 1nnn...*n=m.那么我们反过来,m 就会被 n 整除,一直到为 1.

    这是最暴力和直接的思路,适用于所有判断幂的情况,不过时间复杂度达到了 .

    class Solution {
    public:
        bool isPowerOfTwo(int n) {
            if (n<=0){
                return false;
            }
            while(n%2 == 0){
                n>>=1;
            }
            return n==1;
        }
    };

    打表

    第二种方法也比较简单和直接,要判断是不是 n 的幂,由于 int 是有范围的,所以最终的结果其实也是固定的一些数,那么我们提前找出这些数判断即可。

    打表也是判断 n 的幂的通用解法之一,虽然看上去似乎太?,但是实际上这种方法在我们真正写项目过程中反而常用一些。

    当然,我们也可以不打表,而是直接计算出对应的值,然后判断是否相等也行。显然打表的时间复杂度位 .

    class Solution {
    public:
        bool isPowerOfTwo(int n) {
            switch (n) {
                case 1:
                case 2:
                case 4:
                case 8:
                case 16:
                case 32:
                case 64:
                case 128:
                case 256:
                case 512:
                case 1024:
                case 2048:
                case 4096:
                case 8192:
                case 16384:
                case 32768:
                case 65536:
                case 131072:
                case 262144:
                case 524288:
                case 1048576:
                case 2097152:
                case 4194304:
                case 8388608:
                case 16777216:
                case 33554432:
                case 67108864:
                case 134217728:
                case 268435456:
                case 536870912:
                case 1073741824:
                    return true;     
                default:
                    return false;
            }
        }
    };

    二进制规律

    要判断似乎是 2 的幂,我们可以找到所有幂,然后看它们有什么规律。

    1 →0001

    2 → 0010

    4 → 0100

    8 → 1000

    如上,我们发现,2 的幂其实就是只有一个 1 的二进制数,因此,我们可以利用这一点来判断,如果只有一个 1,那么就是 2 的幂。

    而判断是否只有一个 1,也有两种方法:

    1. 我们清除这个 1,如果结果是 0,那么就满足
    2. 获取 1 的位置,如果和原数相同,那么也满足

    显然找规律的时间复杂度为 .

    class Solution {
    public:
        bool isPowerOfTwo(int n) {
            return n>0 && (n&(n-1))==0; // n&(n-1) 是获取消除最低位的 1
        }
    };
    class Solution {
    public:
        bool isPowerOfTwo(int n) {
            return n>0 && (n&(-n))==n; // n&-n 是获取最低位的 1
        }
    };

    同余定理

    若 2 的幂为 n,则 n 的质因数只有 2,故 n 必然等于 1222..,换句话说,不同幂 m,n 之间必然能够整除,即如果 .而我们要判断一个数是不是 2 的幂,即判断是不是 ,那么这个数同样也能够被比它大的 2 的幂整除。由于 int 最大 ,故 2 的幂最大为 2<<29,则必有

    class Solution {
    public:
        bool isPowerOfTwo(int n) {
            return n>0 && (2<<29)%n==0;
        }
    };

    326. 3 的幂

    题意

    判断数是否是 3 的幂

    辗转相除法

    最简单和暴力的思路,如果是 n 的幂,那么说明值就是 1nnn...*n=m.那么我们反过来,m 就会被 n 整除,一直到为 1.

    这是最暴力和直接的思路,适用于所有判断幂的情况,不过时间复杂度达到了 .

    class Solution {
    public:
        bool isPowerOfThree(int n) {
            if (n<=0){
                return false;
            }
            while(n%3 == 0){
                n/=3;
            }
            return n==1;
        }
    };

    打表

    类似的,我们也可以打表

    class Solution {
    public:
        bool isPowerOfThree(int n) {
            switch (n) {
                case 1:
                case 3:
                case 9:
                case 27:
                case 81:
                case 243:
                case 729:
                case 2187:
                case 6561:
                case 19683:
                case 59049:
                case 177147:
                case 531441:
                case 1594323:
                case 4782969:
                case 14348907:
                case 43046721:
                case 129140163:
                case 387420489:
                case 1162261467:  
                    return true;     
                default:
                    return false;
            }
        }
    };

    寻找规律

    1→0001

    3→0011

    9→1001

    27→11011

    看上去似乎没有规律,我们分析一下 x=x3=x(2+1)=x*2+x=x<<1+x。也就是说先移位再加,比如 0011=0001 移位得 0010+0001=0011.

    的确有规律,不过这个规律不好判断。故无法使用这个方法。

    同余定理

    同理判断 2 的幂,由于 3 也是一个质数,所以 3 的幂必然也为 333*..,则必然 3 的幂的最大值必然会被 整除,而在 int 范围内,3 的幂的最大值为 1162261467,则必有 1162261467%m=0

    class Solution {
    public:
        bool isPowerOfThree(int n) {
            return n>0 && 1162261467%n==0;
        }
    };

    342. 4 的幂

    题意

    判断数是否是 4 的幂

    辗转相除法

    class Solution {
    public:
        bool isPowerOfFour(int n) {
            if (n<=0){
                return false;
            }
            while(n%4 == 0){
                n>>=2;
            }
            return n==1;
        }
    };

    打表

    class Solution {
    public:
        bool isPowerOfFour(int n) {
            switch (n) {
                case 1:
                case 4:
                case 16:
                case 64:
                case 256:
                case 1024:
                case 4096:
                case 16384:
                case 65536:
                case 262144:
                case 1048576:
                case 4194304:
                case 16777216:
                case 67108864:
                case 268435456:
                case 1073741824:
                    return true;     
                default:
                    return false;
            }  
        }
    };

    二进制规律

    1 → 0000 0001

    4 → 0000 0100

    16→ 0001 0000

    64→ 0100 0000

    可以看到,4 的幂的二进制就是奇数位是 1,且只有一个 1,那么我们可以根据这一点写出判断条件:

    1. 消除一个 1 后,值为 0
    2. 1 的右边的位数是奇数
    class Solution {
    public:
        bool isPowerOfFour(int n) {
            if ((n<=0) || ((n&(n-1))!=0)){
                return false;
            }
            
            int cnt=-1;
            
            while(n!=0){            
                cnt++;
                n>>=1;            
            }
      
            return cnt%2==0;
        }
    };

    当然,我们还可以从另外的角度来分析这个性质。即不是判断右边位数为奇数,而是直接判断 1 在奇数上,怎么做呢?其实很简单。

    奇数上为 1 时,最大的数为 0101 0101 0101 ...即 0x55555555.如果只有奇数上为 1 的话,那么这个数和 0x55555555 相与必然结果还是该数。

    如 0x0010 0000 & 0x5555 5555=0x0010 0000,因为 0x5555555 只有奇数上为 1,那么如果要求的数没有 1,或者偶数上为 1,和 0x5555555 相与之后结果都会变化。故如果 0x5555555&n==n,则 n 就是 4 的幂。当然,有人可能会问,如果不只一个 1 呢,比如 5 就是 0101,也满足这个条件。那我们提前过滤一遍即可。

    class Solution {
    public:
        bool isPowerOfFour(int n) {
            if (n<=0 || ((n&(n-1)) !=0)){  // 小于 0 或 1 的个数多于 1,则失败
                return false;
            }
            return (0x55555555 & n)==n;
        }
    };

    同余定理

    .

    class Solution {
    public:
        bool isPowerOfFour(int n) {        
            return n>0 && ((n&(n-1))==0) && ((n%3)==1);
        }
    };

    转换为 2 的幂

    ,故对其开方后,如果还是 2 的幂的话,那么就是 4 的幂。

    class Solution {
    public:
        bool isPowerOfFour(int n) {                    
            int m;
            
            if (n<=0 || ((n&(n-1)))!=0){
                return false;
            }
            m=sqrt(n);
            if (m*m!=n){ // 判断是否是平方数
                return false;
            }
            
            return n>0 && (2<<29)%m==0;
        }
    };

    总结

    判断任意数 n 的幂,有多种方法,其中辗转相除,打表法是通用的。而如果 n 是质数,可以找幂最大值判断是否整除,其它的则要看 n 的性质,有的可以根据其二进制数的规律,有的可以根据同余定理,有的也可以转换为其它数判断。

    1711. 大餐计数

    题意

    找到和为 2 的幂的两个数

    分析

    由于给定的范围,2 的幂个数是确定的,所以对于一个数,要找到另一个数是否和为幂,而要找的那几个数就已经固定了,所以枚举这几个幂,就变成无序数组求两数和问题了,即 1 题,直接用哈希表暂存即可。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int countPairs(vector<int>& nums) {
            unordered_map<int,int> m;
            int res=0;
            vector<int> power={1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536,131072,262144,524288,1048576,2097152};        
            
            for (int i=0;i<nums.size();i++){
                for (int j=0;j<power.size();j++){                
                    if (((power[j]-nums[i])>-1) && (m.count(power[j]-nums[i])!=0)){
                        res%=1000000007;
                        res+=m[power[j]-nums[i]];
                    }
                }
                m[nums[i]]++;
            }
            
            return res;
        }    
    };

逆转

逆转位数通常涉及到局部逆转,分治等思想。

  • list

    7. 整数反转

    题意

    反转十进制整数

    分析

    刚开始我想错了,以为直接反转 4 个字节就行了,结果发现并不是 BCD 码,十进制反转不能从二进制考虑。于是就简单写了个辗转相除法,结果可能溢出。那么溢出就是这题处理的要点了。

    最简单的自然是用更大范围的数暂存,不过题目又限制不能用。那么我们就只能避免溢出了。通常,判断溢出的方式有两种:

    1. 先运算,然后看超出范围没有,比如正数变负数
    2. 先得到结果,再逆运算看结果变没变,类似于判断是否是平方数,先开方再平方就能判断。

    而避免溢出也通常有两种方式:

    1. 用更大的存储空间
    2. 更高运算法则

    第一个不用多说,第二个则比较常见,通常是移项然后变运算法则。比如加法变减法,乘法变除法,常见的如二分中 mid=(r-l)/2+l 就是加法变减法。

    这题也是一样,溢出时 n*10>INT_MAX → n>INT_MAX/10,下溢同理。

    当然,除了辗转相除法,还有转换成字符串模拟等方法处理。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    更大存储空间

    class Solution {
    public:
        int reverse(int x) {
            int res=0;
            
            while(x!=0){
                if ((long)res*10>INT_MAX || (long)res*10<INT_MIN){
                    return 0;
                }
                
                res=res*10+x%10;
                x/=10;
            }
            
            return res;
        }
    };

    变运算

    class Solution {
    public:
        int reverse(int x) {
            int res=0;
            
            while(x!=0){
                if (res>(INT_MAX/10)){
                    return 0;
                }
                if (res<(INT_MIN/10)){
                    return 0;
                }
                            
                res=res*10+x%10;
                x/=10;
            }
            
            return res;
        }
    };

    190. 颠倒二进制位

    题意

    将二进制数逆转

    分析

    一看到逆转,就想到字符串逆转。在字符串逆转中,双指针是最常见的了,不过这题显然不好双指针,但是逆转的原理是一样的。

    通常,我们可以通过局部反转达到目的,这题也是一样的,分治来做。

    无效的图片地址

    同理,整数也是一样的,只是变成 32 位了而已。

    现在第二个问题就是,我们怎么反转对应的位。其实很简单,分成三步:

    1. 取对应的数
    2. 移动到对应位置
    3. 置 1

    即 x=(x<<m)|(x>>n).

    当然,我们其实也可以模拟字符串反转的方式,不过效率不咋地,可以当作练习位运算的操作,涉及取位,设置等。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    分治

    class Solution {
    public:
        uint32_t reverseBits(uint32_t n) {
            n=((n&0xffff0000)>>16)|((n&0x0000ffff)<<16);
            n=((n&0xff00ff00)>>8)|((n&0x00ff00ff)<<8);
            n=((n&0xf0f0f0f0)>>4)|((n&0x0f0f0f0f)<<4);
            n=((n&0xcccccccc)>>2)|((n&0x33333333)<<2);
            n=((n&0xaaaaaaaa)>>1)|((n&0x55555555)<<1);
            return n;
        }
    };

    双指针

    class Solution {
    public:
        uint32_t reverseBits(uint32_t n) {
            int i=0;
            int j=31;
            
            while(i<=j){
                n=swap(n,i,j);
                i++;
                j--;
            }
            return n;
        }
        
        int swap(int n,int i,int j){
            int vi=getBit(n,i);
            int vj=getBit(n,j);
            
            if (vi==0){
                n=unsetBit(n,j);
            } else {
                n=setBit(n,j);
            }
            
            if (vj==0){
                n=unsetBit(n,i);
            } else {
                n=setBit(n,i);
            }
                    
            return n;
        }
        
        int getBit(int x,int pos){
            return (x>>pos)&1;
        }
        
        int setBit(int x,int pos){
            return x|(1<<pos);
        }
        
        int unsetBit(int x,int pos){
            return x&(~(1<<pos));
        }
    };

    面试题 05.07. 配对交换

    题意

    交换奇数位和偶数位

    分析

    1. 获取奇数位和偶数位
    2. 将其移动到正确位置
    3. 相或

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int exchangeBits(int num) {
            return ((num&0xaaaaaaaa)>>1)|((num&0x55555555)<<1);
        }
    };

集合操作

集合操作是指用一个数表示成一个集合,然后用位运算代替集合的基本操作。是竞赛中比较常见的题型之一。

  • list

    78. 子集

    题意

    生成数组子集

    分析

    对于数组中的某个元素,它只有两种状态:即出现或不出现。

    所以,我们能用一个数组长度的变量,表示某个集合的元素情况。比如 [1,2,3] 的子集 ,用 101 就可以表示子集 [1,3]。

    而我们要生成所有子集,所以,我们只需要找到所有表示状态的变量即可。即 0000~1111,我们枚举所有变量,然后对于每一个变量,即一个子集。然后遍历元素看是否需要在其中即可。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        vector<vector<int>> subsets(vector<int>& nums) {
            vector<vector<int>> res;
            vector<int> t;
            int cnt=0;
            int n=pow(2,nums.size());
            
            for (int i=0;i<n;i++){
                t.clear();
                for (int j=nums.size()-1;j>-1;j--){
                    if (getBit(i,j)){
                        t.push_back(nums[j]);
                    }
                }
                res.push_back(t);
            }
            
            return res;
        }
        
        int getBit(int x,int pos){
            return (x>>pos)&1;
        }
    };

    90. 子集 II

    题意

    生成子集,但不能有重复元素

    分析

    本题就是在 78 的基础上增加了去重的操作,问题就在于怎么去重。

    我们可以排序,如果上一个元素相同,且没有取,那么就要跳过当前元素。

    比如 [1,2,2],重复的两个即

    101

    110

    当第一次取了 101 后,这时该取 110 了,但是 110 和 101 是相同的,理论上我们应该跳过,那我们是怎么判断的呢?

    其实是这样的:

    1,2,2

    1 1 0

    当我们取 110 的 第二个 1,即 12x 的 2 时,如果后面的 x 和 2 相同,并且为 0,说明什么?说明上一次已经取了这个元素了,即上一次已经取了 1x2 了,所以我们不应该再取 12x,这时就该跳过。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        vector<vector<int>> subsetsWithDup(vector<int>& nums) {        
            vector<vector<int>> res;
            vector<int> t;
            int cnt=0;
            int n=pow(2,nums.size());
            bool flag;
            
            sort(nums.begin(),nums.end());
            
            for (int i=0;i<n;i++){
                t.clear();
                flag=true;
                
                for (int j=nums.size()-1;j>-1;j--){
                    if (getBit(i,j)){
                        if ((j-1)>-1 && getBit(i,j-1)==0 && nums[j]==nums[j-1]){
                            flag=false;
                        }
                        
                        t.push_back(nums[j]);
                    }
                }
                if (flag){
                    res.push_back(t);
                }            
            }
            
            return res;
        }
        
        int getBit(int x,int pos){
            return (x>>pos)&1;
        }
    };

    318. 最大单词长度乘积

    题意

    找出两个没有交集的数组长度之积的最大值

    分析

    要找没有交集的两集合,故我们可以考虑使用位运算将其映射到 int 变量中,每一位表示一个元素,然后使用与运算表示交集。

    优化

    效率

    时间复杂度:

    空间复杂度:

    代码

    class Solution {
    public:
        int maxProduct(vector<string>& words) {
            vector<int> sets(words.size());
            int res=0;
            
            for (int i=0;i<words.size();i++){
                for (int j=0;j<words[i].size();j++){
                    sets[i]=setBit(sets[i],words[i][j]-'a');
                }
            }
            
            for (int i=0;i<sets.size();i++){
                for (int j=i+1;j<sets.size();j++){
                    if ((sets[i]&sets[j])==0){
                        int t=words[i].size()*words[j].size();
                        res=max(res,t);
                    }
                }
            }
            
            return res;
        }
        
        int setBit(int x,int pos){
            return x|(1<<pos);
        }
    };

    1178. 猜字谜

    题意

    位运算模拟集合+子集生成+哈希统计个数

    分析

    一看要使得所有字符在其它字符中,就想到了位运算模拟集合,然后交集就等于其本身。也是状态压缩的一种方式。

    分别把 words 和 puzzles 压缩到两个 int 数组里面,而要找交集等于自己,其实就是相与等于自己。

    所以暴力的,就是对于每个 puzzles,遍历 words,但是这样复杂度有点高,O(nm) 会超时。

    优化

    优化也很简单,把 words 放在哈希表里,对于每个 puzzles,我们不遍历 words 了,而是生成 puzzles 对应的子集,然后判断这个子集在不在哈希表中即可。这样就把复杂度从遍历 puzzles 优化到了生成 puzzles 的子集。

    效率

    时间复杂度:,优化

    空间复杂度:

    代码

    压缩+暴力枚举

    class Solution {
    public:
        vector<int> findNumOfValidWords(vector<string>& words, vector<string>& puzzles) {
            vector<int> set1(words.size());
            vector<int> set2(puzzles.size());
            vector<int> res(puzzles.size());
            int tmp=0;
            
            for (int i=0;i<words.size();i++){
                for (int j=0;j<words[i].size();j++){
                    set1[i]=setBit(set1[i],words[i][j]-'a');
                }
            }
            
            for (int i=0;i<puzzles.size();i++){
                for (int j=0;j<puzzles[i].size();j++){
                    set2[i]=setBit(set2[i],puzzles[i][j]-'a');
                }
            }
            
            for (int i=0;i<set2.size();i++){
                tmp=0;
                for (int j=0;j<set1.size();j++){
                    if (((set2[i]&set1[j])==set1[j])&&(getBit(set1[j],puzzles[i][0]-'a'))){
                        tmp++;
                    }                
                }
                res[i]=tmp;
            }  
    
            return res;
        }    
        
        int setBit(int x,int pos){
            return x|(1<<pos);
        }
        
        int getBit(int x,int pos){
            return (x>>pos)&1;
        }
    };

    压缩+子集生成+哈希查询

    class Solution {
    public:
        vector<int> findNumOfValidWords(vector<string>& words, vector<string>& puzzles) {
            vector<int> set1(puzzles.size());
            vector<int> res(puzzles.size());
            unordered_map<int,int> m;
            int tmp=0;
            int t=0;
            int cnt=0;
    
            // 将 words 映射成 int,并且存于 map 中
            for (int i=0;i<words.size();i++){
                t=0;
                for (int j=0;j<words[i].size();j++){
                    t=setBit(t,words[i][j]-'a');
                }
                m[t]++;
            }
                    
            // 将 puzzles 映射成 int,且存于数组中
            for (int i=0;i<puzzles.size();i++){
                for (int j=0;j<puzzles[i].size();j++){
                    set1[i]=setBit(set1[i],puzzles[i][j]-'a');
                }
            }
            
            int n=pow(2,7);
            
            for (int i=0;i<puzzles.size();i++){ // 枚举 puzzles
                cnt=0;
                
                for (int j=0;j<n;j++){ // 根据 j 生成 puzzles 的子集
                    if (getBit(j,0)==0){
                        continue;
                    }
                    
                    tmp=0; // 暂存生成的子集
                    
                    for (int k=0;k<7;k++){ // 枚举 puzzles[i] 的字母
                        if (getBit(j,k)){
                            tmp=setBit(tmp,puzzles[i][k]-'a');
                        }                                
                    }
                                    
                    cnt+=m[tmp];
                }
                
                res[i]=cnt;
            }
            
            return res;
        }    
        
        int setBit(int x,int pos){
            return x|(1<<pos);
        }
        
        int getBit(int x,int pos){
            return (x>>pos)&1;
        }
    };

状态压缩

状态压缩就是将问题中的一些状态压缩到一个 int 变量中,然后通过位运算进行状态间的转换和判断。这种方式在许多题目中都很常见,比如 dp 中。常见的将 int 视为集合进行操作就是典型的状态压缩题型之一。

大数据处理

  • list

总结

位运算总体来说其实是比较简单的,基本操作也就与或非移位异或,但是它的使用场景其实一点都不少,无论是底层编程,还是项目中的优化,又或者在面试中,或者单纯的算法中,它的场景非常的多。要掌握好位运算的话,需要了解一些基本的底层知识,如计组,数电的一些知识,数的编码,位运算也通常在 dp,前缀树,状态压缩,回溯等较难的算法中出现,要掌握好还是需要大量的练习才行。

参考

位运算有什么奇技淫巧?

手动消除程序中的 if-else 分支结构

分享|2021 秋招算法总结 9 - 位运算

回归本源一一位运算及其应用

位运算

位运算有什么奇技淫巧?

Bit Twiddling Hacks 解析 (一)

Bit Twiddling Hacks 解析 (二)

深入理解计算机系统 —— 位操作实验

评论 (15)