Bit Manipulation
Overviewβ
We all know that information is stored in the form of bits in computer memory, which means directly manipulating these bits will yield faster results than performing computation on ordinary data.
Binary uses only and to represent a number in a base-2 number system. The series of 0 and 1 are called bits. If the bit is , then this bit is set. We read binary number from right to left. For example, the binary representation of number is which can be calculated by summing up all the set bit: . Bit Manipulation utilises different bitwise operations to manipulate bits.
Bitwise Operatorsβ
X | Y | X & Y | X | Y | X ^ Y | ~ X |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 1 |
0 | 1 | 0 | 1 | 1 | 1 |
1 | 0 | 0 | 1 | 1 | 0 |
1 | 1 | 1 | 1 | 0 | 0 |
AND (&) Operatorβ
& takes two bit integers to compare. If the bits are both , then the resulting bit is , else .
For example, & because only the second bits from the right are both .
Usage #1: Check if the rightmost bit is setβ
Let's say is which is . If we execute & , i.e. & , the result is because only the rightmost bits are both 1 and other bits would return 0.
Usage #2: Check if the i-th bit is setβ
Let's say is which is . How to check if the 2-nd, 3-rd, or 4-th bit is set? Using the same idea, the mask would be , , respectively. And you may notice that the mask is always a power of 2. A common way to do it is to use left shift operator (which will be discussed below), i.e. & where is the i-th bit to be checked.
Usage #3: Remove the rightmost set bitβ
We can use & to remove the rightmost set bit.
n n n - 1 n & (n - 1)
-- ---- ---- -------
0 0000 0111 0000
1 0001 0000 0000
2 0010 0001 0000
3 0011 0010 0010
4 0100 0011 0000
5 0101 0100 0100
6 0110 0101 0100
7 0111 0110 0110
8 1000 0111 0000
9 1001 1000 1000
10 1010 1001 1000
11 1011 1010 1010
12 1100 1011 1000
13 1101 1100 1100
14 1110 1101 1100
15 1111 1110 1110
Example #1: 0231 - Power of Two (Easy)β
We know that a power of 2 is a positive number and only has one bit set. We can use & to see the result is 0 or not to determine if the target value is a power of 2 or not.
In short, & never have a 1 bit in the same place.
For example, 1000 (8), and 0111(7) - never have 1 bit in the same place for Power of Two
class Solution {
public:
bool isPowerOfTwo(int n) {
// 1. check if it is a positive number
// 2. check the value is 0 after removing the rightmost bit
return n > 0 && !(n & (n - 1));
}
};
Example #2: 0191 - Number of 1 Bitsβ
Instead of checking the bits one by one, we can use & to jump to the next set bit.
class Solution {
public:
int hammingWeight(uint32_t n) {
int ans = 0;
for (; n; n = n & (n - 1)) ans++;
return ans;
}
};
OR (|) Operatorβ
takes two bit integers to compare. If either bits are , then the resulting bit is , else .
For example, because only the first and the second bits from the right are in either value.
Usage #1: Set the rightmost bitβ
Let's say is which is . If we execute , i.e. , the result is because would turn the rightmost bit of to since that bit is set.
Usage #2: Set the i-th bitβ
Let's say is which is . How to set other bits? Similar to above example, we can use left shift operator (which will be discussed below), i.e. where is the i-th bit to be set.
Example: 0421 - Maximum XOR of Two Numbers in an Arrayβ
class Solution {
public:
int findMaximumXOR(vector<int>& nums) {
int ans = 0, mask = 0;
for(int i = 31; i >= 0; i--){
unordered_set<int> s;
// set i-th in mask
mask |= (1 << i);
for (auto x : nums) s.insert(mask & x);
// set i-th in ans
int best = ans | (1 << i);
for(auto pref : s){
if(s.find(pref ^ best) != s.end()){
ans = best;
break;
}
}
}
return ans;
}
};
XOR (^)β
^ takes two bit intergers to compare. If one bit is zero and another bit is one, then the resulting bit is , else .
For example, ^ because the first bit got and while the second bit got both .
XOR Propertiesβ
- X ^ 0 = X
- X ^ X = 0
- X ^ 1 = ~X (Compliment of that number)
- X ^ Y = Y ^ X (Commutativity)
- (X ^ Y) ^ Z = X ^ (Y ^ Z) (Associativity)
Example #1: 0268 - Missing Numberβ
Given the fact that we know distinct numbers in the range , we can find the missing number using the above XOR properties.
For example, let's say the input is and we know the the missing number is . We can compare the index (0, 1, 2) and the value (0, 1, 3) and write ^ ^ ^ ^ ^ ^ .
Based on property #2, we know ^ and ^ would be .
Based on property #1, we know that ^ would be . Therefore, we got ^ ^ .
Based on property #4, we can rewrite as ^ ^ and use property #2 again to get ^ .
Based on property #1, we have our final answer which is .
class Solution {
public:
int missingNumber(vector<int>& nums) {
// index = 0 1 2
// value = 0 1 3
// n ^ (0 ^ 0) ^ (1 ^ 1) ^ (2 ^ 3)
// 3 ^ 0 ^ 0 ^ 2 ^ 3
// 0 ^ 0 ^ 2 ^ 3 ^ 3
// 2 ^ 0
// 2
int n = nums.size(), ans = n;
for(int i = 0; i < n; i++) ans ^= (i ^ nums[i]);
return ans;
}
};
Example #2: 0136 - Single Numberβ
As every element appears twice except for one. We can use property #2 to make all elements which appear twice become . At the end, there would be and that element which appears once. Then we use property #1 to get the final answer.
class Solution {
public:
int singleNumber(vector<int>& nums) {
// nums: [4,1,2,1,2]
// 0 ^ 4 ^ 1 ^ 2 ^ 1 ^ 2
// 0 ^ (1 ^ 1) ^ (2 ^ 2) ^ 4
// 0 ^ 0 ^ 0 ^ 4
// 4
int ans = 0;
for(auto x : nums) ans ^= x;
return ans;
}
};
Example #3: Swap 2 numbersβ
XOR (^) can be used to swap 2 numbers by changing the bits and reversing it.
Let's say, , , we want and as our answer.
a = a ^ b => (4 ^ 6) => (0100 ^ 0110) => 2 (0010)
b = b ^ a => (6 ^ 2) => (0110 ^ 0010) => 4 (0100)
a = a ^ b => (2 ^ 4) => (0010 ^ 0100) => 6 (0110)
NOT (~)β
~ inverts (flip) all the bits of a bit intergers, which means would become and vice versa. If we apply it on a positive integer , then it is simply .
For example, if we apply NOT on , then the resulting value is .
The below code snippets are actually equivalent
for (int i = n; i >= 0; i--) {
// i = n, n - 1, n - 2, ..., 2, 1, 0
}
for (int i = n; ~i; i--) {
// i = n, n - 1, n - 2, ..., 2, 1, 0
}
Left-Shiftβ
shifts the bits to the left. For example, because we shift the to the left to become .
Similarily, because we shift the to the left twice to become .
And you may find that is actually . Also means multiplying n by 2 power m. i.e, .
In simple, shifting each bit of n to left m times. Let's say, and . can be represented as in binary, Therefore, = = , which is same as =
Example #1: 0078 - Subsetsβ
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
int n = nums.size();
// number of subsets for n elements would be 2 ^ n
// because for each element, you can choose to take it or not
// if take = 1, don't take = 0, then we can use bit manipulation
int p = 1 << n; // 1 * 2 ^ n
vector<vector<int>> ans;
for(int i = 0; i < p; i++){
vector<int> t;
for(int j = 0; j < n; j++){
if((1 << j) & i) t.emplace_back(nums[j]);
}
ans.emplace_back(t);
}
return ans;
}
};
Right-Shiftβ
shifts the bits to the right. For example would become .
dividing by . Also means dividing by power m. i.e,
In simple, shifting each bit of n to right m times. Let's say, and . can be represented as in binary, Therefore, = >> 2$$ = 0010_2(8 / 2^2)4$
Example: Check the bits one by oneβ
while (n > 0) {
int bit = n & 1;
// do something with bit
// ...
// shift bits to the right
// which is same as n /= 2
n >>= 1;
}
Two's Compliment and Negative Numbersβ
Computers typically store integers in twoβs complement representation. A positive number is represented as itself while a negative number is represented as the twoβs complement of itβs absolute value (with a 1 in its sign bit to indicate that a negative value).
Letβs look at the 4-bit
integer as an example. If itβs a 4-bit number, we have one bit for the sign and three bits for the value. We want the complement with respect to , which is .
The complement of (the absolute value of ) with respect to is . Binary value of is . Therefore, in binary as a 4-bit number is , with the first bit being the sign bit.
In other words, the binary representation of (negative K) as a N-bit number is otherwise prepend (prefix) the sign bit with the value for the calculated complement.
Another way to look at this is that we invert the bits in the positive representation and then add . is in binary.
Flip the bits to get 100, add to get , then prepend the sign bit to get .
Positive Values Negative Values
-------- ------ -------- ------
7 0 111 -1 1 111
6 0 110 -2 1 110
5 0 101 -3 1 101
4 0 100 -4 1 100
3 0 011 -5 1 011
2 0 010 -6 1 010
1 0 001 -7 1 001
0 0 000
Common Bit Tasksβ
Get Bitβ
It shifts over by i bits, creating a value that looks like . By performing and AND (&) with num, we clear all bits other than the bit at bit i.
Finally, compare that value to , if that new value is not zero, then bit i must have a , otherwise, bit i is a .
int getBit(int num, int i) {
return num & (1 << i);
}
Set Bitβ
It shits over by i bits, creating a value that looks like ( to the power of ). By performing an with num, only the value at bit i will change.
All other bits of the mask are zero and will not affect num.
int setBit(int num, int i) {
return num | (1 << i);
}