bit-manipulation

Bit manipulation has many tricks. There is a website called “Bit Twiddling Hacks” that collects almost all bit tricks:

http://graphics.stanford.edu/~seander/bithacks.html

But most of these tricks are too hard to read. I think you can use that site like a dictionary, no need to study every line. What we really need to learn are the bit tricks that are interesting and useful.

So in this article, we will start from the basics, explain some simple ideas of bit operations, then summarize some common bit tricks used in algorithm problems and real-world development.

Basics of Bit Operations

Binary representation

In a computer, all data is finally stored in binary form. Binary has only two digits: 0 and 1. Each digit is called a bit.

For example, the decimal number 13 is 1101 in binary:

1101 = 1×2³ + 1×2² + 0×2¹ + 1×2⁰ = 8 + 4 + 0 + 1 = 13

In Java and some other languages, we can use the 0b prefix to write binary numbers:

int a = 0b1101;  // same as decimal 13
int b = 0b1010;  // same as decimal 10

Shift operations

Left shift <<: move the bits to the left, and fill 0s on the right. Shifting left by n bits is the same as multiplying by 2^n.

int a = 5;           // binary: 0b0101
int b = a << 1;      // binary: 0b1010, decimal: 10
int c = a << 2;      // binary: 0b10100, decimal: 20

// Left shift n bits is like multiply by 2^n
// 5 << 1 = 5 * 2¹ = 10
// 5 << 2 = 5 * 2² = 20

Right shift >>: move the bits to the right, and fill the left side with the sign bit. Shifting right by n bits is like dividing by 2^n and taking the floor.

AND operation

AND &: the result bit is 1 only when both bits are 1, otherwise it is 0.

Common uses:

  • Check odd or even: n & 1, result 1 means odd, 0 means even

  • Get a specific bit: n & (1 << k) can get the k-th bit of n

OR operation

OR |: the result bit is 1 if at least one of the bits is 1.

Common uses:

  • Set a specific bit to 1: n | (1 << k) sets the k-th bit of n to 1

XOR operation

XOR ^: the result bit is 1 only when the two bits are different; if they are the same, the result is 0.

Important properties of XOR:

  1. a ^ a = 0: any number XOR itself is 0

  2. a ^ 0 = a: any number XOR 0 is itself

  3. XOR is commutative and associative: a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c

Common uses:

  • Flip a specific bit: n ^ (1 << k) flips the k-th bit of n (0 -> 1, 1 -> 0)

  • Swap two numbers (will be explained later)

Once we understand these basic bit operations, we can move on to some interesting bit tricks.

Some Interesting Bit Operations

The first 6 tricks are not very useful in real code. But the 7th trick is quite practical. It uses the sign bit in two’s complement.

In integer encoding, the highest bit is the sign bit. For negative numbers, the sign bit is 1. For non-negative numbers, the sign bit is 0. With XOR, you can know whether two numbers have different signs.

If you do not use bit operations, you must use if-else to check the sign, which is more trouble. You may think of using the product to check the sign, but that can easily cause integer overflow and give wrong results.

Modulo with Power of 2

For modulus (remainder), we usually use the % operator. But in some code (for example, in the source code of HashMap), you may see & instead. This is an optimization.

When the modulus m is a power of 2, x % m is the same as x & (m - 1).

The bit operation & is much faster than %, so this trick is very useful where performance matters.

A common use is in a circular array. The normal way is to use % so that the index loops in [0, arr.length - 1]:

If the array length is a power of 2, we can use & to speed up the modulus:

::: important Important

This trick only works when the array length is a power of 2, like 2, 4, 8, 16, 32, and so on. There are clever bit hacks that can round a number up to a power of 2. You can check https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2

:::

Simply put, & (arr.length - 1) can replace % arr.length and gives better performance.

Now another question: we keep doing index++, and this gives us a circular effect. But if we keep doing index--, can we still get a circular array?

If you use % for modulus, once index becomes negative, the result of % can also be negative, and you must handle this case specially. But with &, index will not become negative and everything still works:

You may not use this trick often in your own code, but you will see it a lot when reading other code bases. Just keep it in mind so you are not confused when you see it.

The Usage of n & (n-1)

The operation n & (n-1) is common in algorithms, and its effect is to remove the last 1 in the binary form of n.

Look at the picture and it becomes clear:

The key idea: n - 1 will remove the last 1 in n, and turn all the bits after it into 1. Then n & (n - 1) will clear only that last 1 to 0.

Hamming Weight

This is LeetCode 191: “Number of 1 Bitsarrow-up-right”:

LeetCode 191. Number of 1 Bits

Write a function that takes the binary representation of a positive integer and returns the number of set bits it has (also known as the Hamming weightarrow-up-right).

Example 1:**

Input: n = 11

Output: 3

Explanation:

The input binary string 1011 has a total of three set bits.

Example 2:**

Input: n = 128

Output: 1

Explanation:

The input binary string 10000000 has a total of one set bit.

Example 3:**

Input: n = 2147483645

Output: 30

Explanation:

The input binary string 1111111111111111111111111111101 has a total of thirty set bits.

Constraints:

  • 1 <= n <= 2^(31) - 1

Follow up: If this function is called many times, how would you optimize it?

You need to return how many 1s are in the binary form of n. Since n & (n - 1) can remove the last 1, we can keep doing this in a loop and count, until n becomes 0.

Check If a Number Is a Power of 2

This is LeetCode 231: “Power of Twoarrow-up-right”.

If a number is a power of 2, its binary form has exactly one 1:

Using the trick n & (n - 1) makes it easy (note operator precedence, you cannot remove the parentheses):

The Usage of a ^ a = 0

We must remember these properties of XOR:

A number XOR itself is 0, that is a ^ a = 0. A number XOR 0 is the number itself, that is a ^ 0 = a.

Find the Element That Appears Only Once

This is LeetCode 136: “Single Numberarrow-up-right”:

LeetCode 136. Single Number

Given a non-empty array of integers nums, every element appears twice except for one. Find that single one.

You must implement a solution with a linear runtime complexity and use only constant extra space.

Example 1:**

Example 2:**

Example 3:**

Constraints:

  • 1 <= nums.length <= 3 * 10^(4)

  • -3 * 10^(4) <= nums[i] <= 3 * 10^(4)

  • Each element in the array appears twice except for one element which appears only once.

For this problem, we just XOR all numbers together. Pairs of equal numbers will become 0. The single number XOR 0 is still itself, so the final XOR result is the element that appears only once:

Find the Missing Number

This is LeetCode Problem 268: Missing Numberarrow-up-right:

LeetCode 268. Missing Number

Given an array nums containing n distinct numbers in the range [0, n], return the only number in the range that is missing from the array.

Example 1:**

Example 2:**

Example 3:**

Constraints:

  • n == nums.length

  • 1 <= n <= 10^(4)

  • 0 <= nums[i] <= n

  • All the numbers of nums are unique.

Follow up: Could you implement a solution using only O(1) extra space complexity and O(n) runtime complexity?

You are given an array of length n. The valid indices are [0, n), but now you want to put n + 1 numbers [0, n] into it. So one number must be missing. Please find this missing number.

This problem is not hard. We can easily think of sorting the array first, then scan it once to find the missing number.

Or we can use a data structure: put all numbers from the array into a HashSet, then iterate through the numbers in [0, n] and check which one is not in the HashSet.

The sorting solution has time complexity O(NlogN). The HashSet solution has time complexity O(N), but it also needs O(N) extra space to store the HashSet.

There is a very simple solution for this problem: the sum formula of an arithmetic sequence.

We can understand the problem like this: we have an arithmetic sequence 0, 1, 2, ..., n, and one number is missing. We need to find it. Then the answer is:

sum(0, 1, ..., n) - sum(nums).

But the main topic of this article is bit operations. So we will see how to use bit tricks to solve this problem.

Recall the properties of XOR:

  • A number XOR itself is 0.

  • A number XOR 0 is still the number itself.

XOR also has commutative and associative laws. That is:

We can use these properties to find the missing number. For example, nums = [0, 3, 1, 4]:

To make it easier to understand, we first imagine extending the index by one, then match each element with the index that has the same value:

After doing this, except for the missing number, every index and element form a pair. If we can find the lonely index 2, we find the missing number.

How do we find this lonely number? Just XOR all the indices and all the elements together. All paired numbers will cancel out to 0, and only the lonely one will remain. That is our answer:

Because XOR is commutative and associative, we can always cancel all pairs and keep only the missing number.

Up to here, we have covered most common bit operations. These tricks are easy once you get them, and you do not need to memorize them. Just keep a rough idea in mind, and that is enough.

Last updated