When representing integers using a fixed number of bits, negative numbers are typically represented using two's complement. If using n bit numbers, the two's complement of a number x with 0≤x<2n is (−x)mod2n=2n−x. But what do you do if you want to work with unbounded/multiple-precision integers? Fixing x and letting the number of bits go to infinity, you will notice that increasing n by one simply adds a 1 at the left. For instance,
- 1975=(11110110111)2
- −1975=212−1975=(100001001001)2 (with n=12)
- −1975=213−1975=(1100001001001)2 (with n=13)
- −1975=220−1975=(11111111100001001001)2 (with n=20)
- −1975=(…1111111111111100001001001)2 (with n=∞)
(This can be made more rigorous using 2-adic numbers). Conversely, every binary number with infinitely many 1s to the left corresponds to a negative integer.
Notice the important special case −1=(…1111)2. If x denotes bitwise not of x, where each bit is flipped from 0 to 1 and vice versa, we observe that
x+x=…1111=−1,
from which we have the important identity
x=−1−x.
This makes bitwise not equivalent to a simple subtraction. Notice how bitwise not turns a non-negative integer into a negative integer and vice versa.
Let us turn to general bitwise operators. Consider a function that maps two bits to a single bit. Given such a function and two non-negative integers, we can apply the function to the zeroth bit of both numbers to obtain the zeroth bit of the result, then apply the function to the first bit of both numbers to obtain the first bit of the result, and so forth. In this way, any binary bit-operator {0,1}2↦{0,1} can be extended to work on any non-negative integer (and as we shall see, any integer). There are 16 possible binary bit-operators:
|
(x,y) |
(0,0) |
(1,0) |
(0,1) |
(1,1) |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
x&y |
0 |
0 |
0 |
1 |
2 |
x&y |
0 |
0 |
1 |
0 |
3 |
y |
0 |
0 |
1 |
1 |
4 |
x&y |
0 |
1 |
0 |
0 |
5 |
x |
0 |
1 |
0 |
1 |
6 |
x⊕y |
0 |
1 |
1 |
0 |
7 |
x∣y |
0 |
1 |
1 |
1 |
8 |
x∣y |
1 |
0 |
0 |
0 |
9 |
x⊕y |
1 |
0 |
0 |
1 |
10 |
x |
1 |
0 |
1 |
0 |
11 |
x∣y |
1 |
0 |
1 |
1 |
12 |
y |
1 |
1 |
0 |
0 |
13 |
x∣y |
1 |
1 |
0 |
1 |
14 |
x&y |
1 |
1 |
1 |
0 |
15 |
1 |
1 |
1 |
1 |
1 |
The first column of the table enumerates the functions from 0 to 15 (such that the binary representation of each number corresponds to the outputs). We see that exactly the functions 0–7 map (0,0) to 0, meaning that only these functions will map two non-negative integers to a non-negative integer.
The second column shows expressions for the functions using the well-known operators bitwise and, x&y, bitwise or (inclusive or), x∣y, bitwise xor (exclusive or), x⊕y, and bitwise not, x. The table simultaneously define these operators.
We can now formulate the goal of this article: Using only the bitwise operators that map non-negative integers to non-negative integers, together with usual integer arithmetic, how can we implement all 16 functions? The approach is quite simple: Use bitwise not to transform any negative integer into a non-negative integer, apply one of the functions 0–7, and then possibly apply bitwise not again to obtain the result.
Before proceeding, we need some fundamental identities. First, symmetry:
x&y=y&x,x∣y=y∣x,x⊕y=y⊕x.
Then, De Morgan's laws:
x&y=x∣y,x∣y=x&y.
Finally some useful rules for exlusive or:
x⊕y=x⊕y,x⊕y=x⊕y=x⊕y.
All of these are easily proved since they (by definition) operate bitwise. This means that you only have to consider one-bit numbers, which means only four different cases to check.
The only non-trivial operators among the functions 0–7 are x&y, x∣y, x⊕y, and x&y. We will use the notation x&y=x&y. Note how & is not symmetric. The only non-trivial operators among the functions 8–15 are x&y, x∣y, x∣y, and x⊕y. Considering these eight cases, along with whether x and y are negative or not, we get the following table:
|
x≥0,y≥0 |
x≥0,y<0 |
x<0,y≥0 |
x<0,y<0 |
x&y |
x&y |
x&y |
y&x |
x∣y |
x∣y |
x∣y |
y&x |
x&y |
x&y |
x&y |
x&y |
x&y |
x∣y |
y&x |
x⊕y |
x⊕y |
x⊕y |
x⊕y |
x⊕y |
x&y |
x&y |
x&y |
y&x |
x∣y |
x∣y |
x∣y |
y&x |
x&y |
x&y |
x∣y |
y&x |
x∣y |
x&y |
x&y |
x⊕y |
x⊕y |
x⊕y |
x⊕y |
x⊕y |
Here, we have used only the identities shown earlier. Of course, we need to convert each bitwise not into a subtraction to complete the task. For instance, with x<0, y≥0 we have
x∣y=x&y=−1−((−1−x)&y).
This way, the bitwise and-operation is being applied to non-negative numbers and we see that the result is always negative.
We can now, with assistance from the table above, apply any of the 16 binary bitwise operators to any pair of integers, without restricting ourselves to working with a fixed number of bits.
For further reading related to the binary representation of numbers, I recommend The Art of Computer Programming, Volume 4A, Section 7.1.3: Bitwise Tricks & Techniques by Donald E. Knuth and Hacker's Delight by Henry S. Warren, Jr.