LeetCode The Hard Way
Math

Bit Manipulation

Bit Manipulation utilises different bitwise operations to manipulate bits

Author@wkw

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 00 and 11 to represent a number in a base-2 number system. The series of 0 and 1 are called bits. If the bit is 11, then this bit is set. We read binary number from right to left. For example, the binary representation of number 99 is 100121001_2 which can be calculated by summing up all the set bit: 23+20=9102^3 + 2^0 = 9_{10}. Bit Manipulation utilises different bitwise operations to manipulate bits.

Bitwise Operators

XYX & YX | YX ^ Y~ X
000001
010111
100110
111100

AND (&) Operator

& takes two bit integers to compare. If the bits are both 11, then the resulting bit is 11, else 00.

For example, 001020010_2 & 00112=001020011_2 = 0010_2 because only the second bits from the right are both 11.

Usage #1: Check if the rightmost bit is set

Let's say nn is 5105_{10} which is 010120101_2. If we execute nn & 11, i.e. 010120101_2 & 000120001_2, the result is 000120001_2 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 nn is 5105_{10} which is 010120101_2. How to check if the 2-nd, 3-rd, or 4-th bit is set? Using the same idea, the mask would be 001020010_2, 010020100_2, 100021000_2 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. nn & (1<<i)(1 << i) where nn is the i-th bit to be checked.

Usage #3: Remove the rightmost set bit

We can use nn & (n1)(n - 1) 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 nn & (n1)(n - 1) to see the result is 0 or not to determine if the target value is a power of 2 or not.

In short, nn & (n1)(n - 1) 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 nn & (n1)(n - 1) 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

\vert takes two bit integers to compare. If either bits are 11, then the resulting bit is 11, else 00.

For example, 0010200112=001120010_2 | 0011_2 = 0011_2 because only the first and the second bits from the right are 11 in either value.

Usage #1: Set the rightmost bit

Let's say nn is 4104_{10} which is 010020100_2. If we execute n1n \vert 1, i.e. 01002000120100_2 \vert 0001_2, the result is 010120101_2 because 000120001_2 would turn the rightmost bit of 010020100_2 to 11 since that bit is set.

Usage #2: Set the i-th bit

Let's say nn is 4104_{10} which is 010020100_2. How to set other bits? Similar to above example, we can use left shift operator (which will be discussed below), i.e. n(1<<i)n \vert (1 << i) where nn 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 11, else 00.

For example, 001020010_2 ^ 00112=000120011_2 = 0001_2 because the first bit got 00 and 11 while the second bit got both 11.

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 nn distinct numbers in the range [0,n][0, n], we can find the missing number using the above XOR properties.

For example, let's say the input is [0,1,3][0, 1, 3] and we know the the missing number is 22. We can compare the index (0, 1, 2) and the value (0, 1, 3) and write 33 ^ (0(0 ^ 0)0) ^ (1(1 ^ 1)1) ^ (2(2 ^ 3)3).

Based on property #2, we know 00 ^ 00 and (1(1 ^ 1)1) would be 00.

Based on property #1, we know that 00 ^ 11 would be 11. Therefore, we got 33 ^ (2(2 ^ 3)3).

Based on property #4, we can rewrite as 22 ^ (3(3 ^ 3)3) and use property #2 again to get 22 ^ 00.

Based on property #1, we have our final answer which is 22.

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 00. At the end, there would be 00 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, a=4a = 4 (01002)(0100_2), b=6b = 6 (01102)(0110_2), we want a=6a = 6 and b=4b = 4 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 11 would become 00 and vice versa. If we apply it on a positive integer xx, then it is simply x1-x-1.

For example, if we apply NOT on  00102~0010_2, then the resulting value is 110121101_2.

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, 1<<1=21 << 1 = 2 because we shift the 11 (00012)(0001_2) to the left to become 22 (00102)(0010_2).

Similarily, 1<<2=41 << 2 = 4 because we shift the 11 (00012)(0001_2) to the left twice to become 44 (01002)(0100_2).

And you may find that 1<<n1 << n is actually 2n2 ^ n. Also n<<mn << m means multiplying n by 2 power m. i.e, n=n(2m)n = n * (2^m).

In simple, n<<mn << m shifting each bit of n to left m times. Let's say, n=8n = 8 and m=2m = 2. nn can be represented as 100021000_2 in binary, Therefore, 8<<28 << 2 = 10002<<21000_2 << 2 = 1000002(32)100000_2 (32), which is same as (822)(8 * 2^2) = 3232

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 3103_{10} (00112)(0011_2) >>1>> 1 would become 11 (00102)(0010_2).

4>>14 >> 1 dividing 44 by 22. Also n>>mn >> m means dividing nn by 22 power m. i.e, n=n/(2m)n = n / (2^m)

In simple, n>>mn >> m shifting each bit of n to right m times. Let's say, n=8n = 8 and m=2m = 2. nn can be represented as 100021000_2 in binary, Therefore, 8>>28 >> 2 = 100021000_2 >> 2$$ = 0010_2(4),whichissameas(4), which is same as(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 3-3 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 232^3, which is 88.

The complement of 33 (the absolute value of 3-3) with respect to 88 is 55. Binary value of 55 is 010120101_2. Therefore, 3-3 in binary as a 4-bit number is 110121101_2, with the first bit being the sign bit.

In other words, the binary representation of K-K (negative K) as a N-bit number is concat(1,2N1K)concat(1, 2^N-1 - K) otherwise prepend (prefix) the sign bit (1)(1) 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 11. 33 is 001120011_2 in binary.

Flip the bits to get 100, add 11 to get 1012101_2, then prepend the sign bit 11 to get 110121101_2.

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 11 over by i bits, creating a value that looks like 010020100_2 (2i)(2^i). By performing and AND (&) with num, we clear all bits other than the bit at bit i.

Finally, compare that value to 00, if that new value is not zero, then bit i must have a 11, otherwise, bit i is a 00.

int getBit(int num, int i) {
    return num & (1 << i);
}

Set Bit

It shits 11 over by i bits, creating a value that looks like 010020100_2 ( 22 to the power of ii ). By performing an OROR 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);
}

On this page