本文较长,大概需要二十分钟阅读,难度从入门基础到手撕leetcode真题
一、位运算基础
在计算机中的数据都是以二进制的形式去存储的,所以可以巧妙的去使用位运算的一些技巧去轻松的做出一些题,一般情况下,使用位运算的方法去做算法题在时间复杂度和空间复杂度上有着很大的优势。
注:补码:负数在计算机中已补码的形式存在。补码的求法是将原码除符号位每一位取反之后+1即可,在二进制的表示中,正数的符号位为0,负数的符号位为1。
1.计算机中的数字表示形式:
如正数+5(c/c++中int型数占4个字节,即32位)
原码:00000000000000000000000000000101
反码:00000000000000000000000000000101
补码:00000000000000000000000000000101
正数的补码反码原码都相同
如负数-5
原码:10000000000000000000000000000101
反码:111111111111111111111111111111111010
补码:111111111111111111111111111111111011
注意:int数由于最高位是符号位,则数据位只有31位
2.与运算 &
与运算是位运算中的一种基本的运算符,同 && 运算符不一样,&&运算符操作的结果得到的是一个布尔变量,表示True或False。与运算符&是将两操作数的每一位想与之后得到结果。如下:
与运算规则如下:
1 & 1 = 1
0 & 1 = 0
1 & 0 = 0
0 & 0 = 0
如 5 & -5 == 1
00000000000000000000000000000101(5)
&
111111111111111111111111111111111011(-5)
00000000000000000000000000000001 (5 & -5)
3.或运算 |
或运算同与运算相似,将左右两操作数的每一位进行或运算后得到 最后的结果
与运算规则如下:
1 | 1 = 1
0 | 1 = 1
1 | 0 = 1
0 | 0 = 0
如 5 | -5 == -1
00000000000000000000000000000101(5)
|
111111111111111111111111111111111011(-5)
111111111111111111111111111111111111 (5 | -5)
所求得到的结果中最高位的结果为1,即代表结果是一个负数,负数的储存形式是补码,要求得该结果的十进制结果要先将该结果化为原码。
补码:111111111111111111111111111111111111
反码:111111111111111111111111111111111110(反码等于补码+1)
原码:1000000000000000000000000000001(原码等于反码除符号位取反)
则结果为 -1
4.异或运算 ^
异或运算的使用与前两个基本相同
与运算规则如下:
1 ^ 1 = 0
0 ^ 1 = 1
1 ^ 0 = 1
0 ^ 0 = 0
异或也称不仅为加法,即在每一位之间做二进制加法,但是进位不往前进位。
异或的规则可以总结为相同为0,相异为1,得出一个比较重要的推论,相同的两个数异或的 结果为0,因为相同的数在二进制上每一位必定同为0或同为1.
如 5 ^ -5 == -2
00000000000000000000000000000101(5)
^
111111111111111111111111111111111011(-5)
111111111111111111111111111111111110 (5 ^-5)
所求得到的结果中最高位的结果为1,即代表结果是一个负数,负数的储存形式是补码,要求得该结果的十进制结果要先将该结果化为原码。
补码:111111111111111111111111111111111110
反码:111111111111111111111111111111111101(反码等于补码+1)
原码:1000000000000000000000000000010(原码等于反码除符号位取反)
则结果为 -2
5.取反运算~
取反运算与上面几个运算符不太相同,是一个单目运算符,即将操作数的每一位都取反。
运算规则:
~1 == 0
~0 == 1
如5
00000000000000000000000000000101
~5 == -6
补码:111111111111111111111111111111111010
反码:111111111111111111111111111111111001
原码:10000000000000000000000000000110
则 5取反运算结果为 -6
6.右移运算 >>
右移运算也是一个双目运算符,操作符左边是一个用来右移的数,右边是移动的长度
如1
000000000000000000000000000000001
1>>1表示数字1右移一位,左边移出的位补0,1右移一位以后最低为的1被移除数字中,
则数变为0;即1>>1 == 0
在十进制中,每次右移即丢掉最低位则结果比原数小十倍,即/10
在二进制中,右移一位表示/2
如7
000000000000000000000000000000111
7>>1
000000000000000000000000000000011
7>>2
000000000000000000000000000000001
7>>3
000000000000000000000000000000000
7.左移运算 <<
左移运算也是一个双目运算符,操作符左边是一个用来左移的数,右边是移动的长度
如1
000000000000000000000000000000001
1<<1表示数字1左移一位,右边移出的位补0在十进制中,每次左移即最低位后面加一个0,则结果比原数大十倍,即10
在二进制中,左移一位表示2
如1
000000000000000000000000000000111
7<<1
000000000000000000000000000001111
7<<2
000000000000000000000000000011111
7<<3
000000000000000000000000000111111
二、位运算的一些基本使用技巧
位运算基本的运算符就是上面这几个,有一些很巧妙的运用。
如:
1.原地交换两个数,不允许使用额外的空间
交换两个数 a, b、不允许使用额外的空间
这里便可以使用到位运算中的异或运算
第一步:a = a ^ b
第二步:b = a ^ b (a ^ b )^ b = a
第三步:a = a ^ b (a ^ b ) ^ a = b
经过a,b之间的三次相互异或之后便可以得到交换之后的结果
#include
using namespace std;
int main()
{
int a = 3 , b = 5;
a ^= b;
b ^= a;
a ^= b;
cout<<a << " "<< b<<endl;
return 0;
}
题目大意:给你 一组数据,数据中只有一个数字出现了 一次,别的数字都出现 了两次,要求找出这个只出现了一次的数,要求时间复杂度应为线性的,空间复杂度为O(1)。
如1 5 4 4 2 5 1一种数据中找出落单的数2.
会使用位运算中异或运算的另外 一个技巧:x ^ x = 0
即相同的两个数异或和为0
则对数据进行一遍遍历,其中出现了两次的数之间会相互之间变为 0,最后剩下的结果便是只出现一次的那个数
#include
using namespace std;
int main()
{
int n = 0;
cin >> n;
int *a = new int[n];
for(int i = 0;i < n;i++)
cin>>a[i];
int res = 0;
for(int i = 0; i < n ; i++)
res ^= a[i];
cout<<res<<endl;
return 0;
}
题目大意就是给出一个十进制数n,求出该数的二进制表示中位为1的个数
第一中方法:
使用 n & (n -1),每次去迭代,使n = n & ( n-1 ),当 n为 0 时截至,计数迭代的次数count,count即为n的二进制表示中1的个数
如 n = 5 ( 101)
第一次:n = n & (n-1)
101 & 100 = 100(n)
第二次:
100 & 011 = 0(n)
一共执行了两次,则5的二进制中一共有2个为1的位。
第二种方法
n依次右移去判断最低位是否位1,计数得到结果
第一种:
#include
using namespace std;
int main()
{
int n = 0;
cin >> n;
int res = 0;
while(n)
{
n &= (n-1);
res++;
}
cout<<res<<endl;
return 0;
}
#include
using namespace std;
int main()
{
int n = 0;
cin >> n;
int res = 0;
while(n)
{
if(n & 1)//判断最低为是否位1
res++;
n >>= 1; n每次右移1位
}
cout<<res<<endl;
return 0;
}
三、位运算进阶(leetcode刷题 )
上面这些都是小打小闹,会做 leetcode才能找工作 ,hhhhh
1.leetcode----136只出现一次的数字
只出现一次的数字
Category Difficulty Likes Dislikes
algorithms Easy (64.39%) 947 -
Tags
hash-table | bit-manipulation
Companies
airbnb | palantir
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,1]
输出: 1
示例 2:
输入: [4,1,2,1,2]
输出: 4
与上面例子的第一个基本相同
class Solution {
public:
int singleNumber(vector<int>& nums) {
if(nums.size() == 0)
return 0;
int res = 0;
for(auto c:nums)
res ^= c;
return res;
}
};
2.leetcode----191位1的个数
位1的个数
Category Difficulty Likes Dislikes
algorithms Easy (62.32%) 116 -
Tags
bit-manipulation
Companies
apple | microsoft
编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量)。
示例 1:
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 ‘1’。
示例 2:
输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 ‘1’。
示例 3:
输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 ‘1’。
提示:
请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数 -3。
进阶:
如果多次调用这个函数,你将如何优化你的算法?
class Solution {
public:
int hammingWeight(uint32_t n) {
int res = 0;
while(n != 0){
n = n & (n-1);
res++;
}
return res;
}
};
class Solution {
public:
int hammingWeight(uint32_t n) {
int res = 0;
for (int i = 0 ;i < 32;i++)
if(n >> i & 1)
res++;
return res;
}
};
3.leetcode----231.2的幂
2的幂
Category Difficulty Likes Dislikes
algorithms Easy (47.13%) 149 -
Tags
math | bit-manipulation
Companies
给定一个整数,编写一个函数来判断它是否是 2 的幂次方。
示例 1:
输入: 1
输出: true
解释: 20 = 1
示例 2:
输入: 16
输出: true
解释: 24 = 16
示例 3:
输入: 218
输出: false
class Solution {
public:
bool isPowerOfTwo(int n) {
/*
位运算1
return n > 0 && (n & -n) == n;
简便方法
*/
/*
return n > 0 && (1 << 30) % n == 0;
判断2的整数以内的最大幂取余n即可
*/
if(n <= 0)
return false;
int count = 0;
while(n){
n = n & (n-1);
count++;
}
if(count == 1)
return true;
return false;
}
};
4.leetcode----127.只出现一次的 数字II
只出现一次的数字 II
Category Difficulty Likes Dislikes
algorithms Medium (64.91%) 213 -
Tags
bit-manipulation
Companies
Unknown
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,3,2]
输出: 3
示例 2:
输入: [0,1,0,1,0,1,99]
输出: 99
思路:
(1)线性的时间复杂度不好搞,参考了网上一大神的解法
(2)设置变量one,two
one = (one ^ i) & ~two;
two = (two ^ i) & ~one;
对于任意数来说,假如出现了三次,按以上规则异或之后one与two都是0,出现两次的时候one为0,two为该数,出现一次的时候,one为该数,two为0;
class Solution {
public:
int singleNumber(vector<int>& nums) {
int one = 0 ,two = 0;
for(auto i : nums)
{
one = (one ^ i) & ~two;
two = (two ^ i) & ~one;
}
return one;
}
};
5.leetcode----190.颠倒二进制位
颠倒二进制位
Category Difficulty Likes Dislikes
algorithms Easy (51.92%) 108 -
Tags
bit-manipulation
Companies
airbnb | apple
颠倒给定的 32 位无符号整数的二进制位。
示例 1:
输入: 00000010100101000001111010011100
输出: 00111001011110000010100101000000
解释: 输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。
示例 2:
输入:11111111111111111111111111111101
输出:10111111111111111111111111111111
解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293,
因此返回 3221225471 其二进制表示形式为 10101111110010110010011101101001。
提示:
请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 2 中,输入表示有符号整数 -3,输出表示有符号整数 -1073741825。
进阶:
如果多次调用这个函数,你将如何优化你的算法?
class Solution {
public:
uint32_t reverseBits(uint32_t n) {
uint32_t res = 0;
for(int i =0;i < 32;i++)
{
res = res << 1 | n >> i & 1;
}
return res;
}
};
6.leetcode----260.只出现一次的数字III
只出现一次的数字 III
Category Difficulty Likes Dislikes
algorithms Medium (68.37%) 144 -
Tags
bit-manipulation
Companies
Unknown
给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。
示例 :
输入: [1,2,1,3,2,5]
输出: [3,5]
注意:
结果输出的顺序并不重要,对于上面的例子, [5, 3] 也是正确答案。
你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现?
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
int s = 0;
for(auto c : nums)
s ^= c;
int k = 0;
while(!(s >> k & 1))
k++;
int s1 = 0;
for(auto c :nums)
if(c >> k & 1)
s1 ^= c;
return vector<int>({s1,s1^s});
}
/*
int s = 0;
for(auto c:nums)
s ^= c;
int k = 0;
while(!(s >> k & 1))
k++;
int s2 = 0;
for(auto c:nums)
{
if(c >> k & 1)
s2 ^= c;
}
return vector({s2,s ^ s2});
*/
};
7.leetcode----201.数字范围按位 与
数字范围按位与
Category Difficulty Likes Dislikes
algorithms Medium (43.84%) 68 -
Tags
bit-manipulation
Companies
Unknown
给定范围 [m, n],其中 0 <= m <= n <= 2147483647,返回此范围内所有数字的按位与(包含 m, n 两端点)。
示例 1:
输入: [5,7]
输出: 4
示例 2:
输入: [0,1]
输出: 0
class Solution {
public:
int rangeBitwiseAnd(int m, int n) {
int res = 0;
for(int i = 0 ;(1ll << i) <= m; i++)
{
if(m >> i & 1)
{
if((m & ~((1<<i) -1ll)) +(1 << i) > n)
res += 1 << i;
}
}
return res;
}
/*枚举会超时
int rangeBitwiseAnd(int m, int n) {
int res = m;
for(int i =m+1; i <= n ;i++)
res &= i;
return res;
}
*/
};
8.leetcode----476.数字的补数
数字的补数
Category Difficulty Likes Dislikes
algorithms Easy (67.67%) 129 -
Tags
bit-manipulation
Companies
Unknown
给定一个正整数,输出它的补数。补数是对该数的二进制表示取反。
注意:
给定的整数保证在32位带符号整数的范围内。
你可以假定二进制数不包含前导零位。
示例 1:
输入: 5
输出: 2
解释: 5的二进制表示为101(没有前导零位),其补数为010。所以你需要输出2。
示例 2:
输入: 1
输出: 0
解释: 1的二进制表示为1(没有前导零位),其补数为0。所以你需要输出0。
class Solution {
public:
int findComplement(int num) {
int res = 0 , t = 0;
while(num)
{
res += !(num & 1) << t;
t++;
num >>= 1;
}
return res;
}
};
leetcode里面位运算一些基本的题目都放进来了,难度从easy到中等都有,以上这些题目看不明白的我的博客中有单独的面对每道题都有做题的思路和解析。
**注:**使用位运算的做法在时间复杂度和空间复杂度上一般比起别的算法都会 好很多,当然位运算 的用法绝对不止以上几种,望大佬指出错误!