Integer Representation: Bits, Two's Complement and Type Limits
Every bit trick works on the stored bit pattern of an integer, not on its decimal value. Before reading x & (x - 1) or x >> 1, the reader needs two facts: which bit pattern a given number has, and how many bits the type holds. Both change between languages, and both decide what a bit trick returns on negative numbers.
Bit positions and place values
An integer of width n is a row of n bits. The bits are numbered from the right, starting at position 0:
position: 7 6 5 4 3 2 1 0
value: 128 64 32 16 8 4 2 1
Position d is worth 2^d. The number stored is the sum of the values of the positions holding a 1:
bits: 0 0 1 0 1 1 0 1
position: 7 6 5 4 3 2 1 0
32 + 8 + 4 + 1 = 45
The rightmost bit, position 0, is called the least significant bit (LSB). Flipping it changes the number by 1. The leftmost bit is the most significant bit (MSB). In an 8-bit number, flipping it changes the number by 128.
Hexadecimal is used for bit patterns because one hex digit covers four bits:
binary: 0010 1101
hex: 2 D → 0x2D = 45
0xFF is eight ones, 0xFFFFFFFF is thirty-two ones, and 0x80000000 is a single 1 at position 31.
Unsigned integers
An unsigned integer of width n uses all n bits for the value. Its range is 0 to 2^n - 1:
| Width | Smallest | Largest | Largest in hex |
|---|---|---|---|
| 8 bits | 0 | 255 | 0xFF |
| 16 bits | 0 | 65 535 | 0xFFFF |
| 32 bits | 0 | 4 294 967 295 | 0xFFFFFFFF |
| 64 bits | 0 | 18 446 744 073 709 551 615 | 0xFFFFFFFFFFFFFFFF |
The largest value is all ones. Adding 1 to it carries out of the top bit, and that carry has nowhere to go. The stored result is 0. This wraparound is the defined behaviour for unsigned types in C, C++, Go and Rust's wrapping_* methods (overflow covers each language).
Two's complement
Signed integers need a way to store negative numbers. Every language used in this repo stores them in two's complement.
The rule
In two's complement with n bits, the top bit (position n - 1) is worth -2^(n-1) instead of +2^(n-1). Every other position keeps its usual positive value.
For 8 bits, the place values become:
position: 7 6 5 4 3 2 1 0
value: -128 64 32 16 8 4 2 1
Reading a pattern is the same sum as before, with the top bit counted as negative:
1111 1011 = -128 + 64 + 32 + 16 + 8 + 2 + 1 = -5
1000 0000 = -128
0111 1111 = 64 + 32 + 16 + 8 + 4 + 2 + 1 = 127
1111 1111 = -128 + 127 = -1
A pattern with the top bit set is negative, and one with the top bit clear is zero or positive. That is why the top bit is called the sign bit, although it is an ordinary place value with a negative weight, not a separate flag.
All sixteen 4-bit values
Four bits are small enough to list every pattern. The same 16 patterns read as unsigned on the left and as two's complement on the right:
pattern unsigned signed pattern unsigned signed
0000 0 0 1000 8 -8
0001 1 1 1001 9 -7
0010 2 2 1010 10 -6
0011 3 3 1011 11 -5
0100 4 4 1100 12 -4
0101 5 5 1101 13 -3
0110 6 6 1110 14 -2
0111 7 7 1111 15 -1
Three points follow from the table:
- The left half is identical. Patterns with the top bit clear mean the same number in both readings. Small non-negative values can move between signed and unsigned types unchanged.
- The right half differs by 16. Each pattern with the top bit set is
2^4 = 16less as a signed value than as an unsigned one:1011is 11 unsigned and11 - 16 = -5signed. - There is one more negative value than positive. The signed range is
-8to7. Zero takes one of the eight patterns with the top bit clear, so only seven are left for positive numbers.
Adding 1, pattern by pattern
Adding 1 always moves to the next pattern in the list. After 1111, the carry leaves the four bits and the pattern starts again at 0000:
pattern: 0000 0001 ... 0110 0111 1000 1001 ... 1110 1111 │ 0000
unsigned: 0 1 ... 6 7 8 9 ... 14 15 │ 0
signed: 0 1 ... 6 7 -8 -7 ... -2 -1 │ 0
↑ ↑
signed jump: 7 + 1 = -8 unsigned jump: 15 + 1 = 0
The patterns themselves never jump. Only the reading does. The unsigned reading jumps from 15 to 0 at the end of the list. The signed reading jumps from 7 to -8 in the middle, where the top bit turns on. An integer overflow is a step across one of these two jumps.
Negation: invert and add one
To negate a two's complement number, flip every bit and add 1:
5 0000 0101
~5 1111 1010 flip every bit
~5 + 1 1111 1011 add one → -5
The rule works because x + ~x is all ones, which is -1. So ~x = -1 - x, and ~x + 1 = -x.
Two edge cases come straight from the circle:
-0is0.~0000 0000 + 1is1 0000 0000. The ninth bit is dropped and0000 0000remains.- The smallest value is its own negative. In 8 bits,
-(-128)gives~1000 0000 + 1 = 0111 1111 + 1 = 1000 0000, which is-128again.+128does not fit in 8 bits. In 32 bits the same happens with-2^31, which is the source of theabs(INT_MIN)bug in overflow.
The range of a signed type
A signed type of width n holds -2^(n-1) to 2^(n-1) - 1:
| Width | Smallest | Largest |
|---|---|---|
| 8 bits | -128 | 127 |
| 16 bits | -32 768 | 32 767 |
| 32 bits | -2 147 483 648 | 2 147 483 647 |
| 64 bits | -9 223 372 036 854 775 808 | 9 223 372 036 854 775 807 |
In hex, the 32-bit limits are 0x7FFFFFFF (largest, a zero followed by 31 ones) and 0x80000000 (smallest, a one followed by 31 zeros).
Integer types in each language
The same name means different widths in different languages. The table lists the types the solutions in this repo use. C and C++ widths were measured with sizeof on x86-64 Linux with gcc. The C standard fixes only minimum widths for int and long, so int32_t, uint32_t and int64_t from <stdint.h> are the types to use when the width matters.
| Language | 32-bit signed | 32-bit unsigned | 64-bit signed | Notes |
|---|---|---|---|---|
| C / C++ | int, int32_t |
unsigned, uint32_t |
long long, int64_t |
long was 8 bytes on the test machine |
| Java | int |
none | long |
no unsigned types at all |
| Go | int32 |
uint32 |
int64, int |
int was 64 bits on the test machine |
| Rust | i32 |
u32 |
i64 |
usize is the index type, unsigned |
| JavaScript | (see below) | (see below) | none | numbers are 64-bit floats |
| Python | none | none | none | int has no fixed width |
JavaScript stores every number as a 64-bit floating-point value. The bitwise operators (& | ^ ~ << >> >>>) first convert their operands to 32-bit integers, run the operation, and return a 32-bit result. >>> returns it as unsigned and the others return it as signed. Outside the bitwise operators, integers are exact only up to 2^53:
2 ** 53 // 9007199254740992
2 ** 53 + 1 // 9007199254740992 the +1 is lost
For exact integers above 2^53, JavaScript has BigInt (9007199254740993n).
Python integers grow as needed. 1 << 100 is an exact 31-digit number. Python still uses two's complement for the bitwise operators, but treats each number as if it had infinitely many copies of its sign bit to the left. So ~5 is -6, the two's complement answer, and not 250 as it would be for an 8-bit unsigned value.
Reading the same bits as a different type
Converting between a signed and an unsigned type of the same width keeps the bits and changes only the reading. The values below were printed by each language:
| Expression | Bits kept | Result |
|---|---|---|
C: (unsigned)-1 |
32 ones | 4294967295 |
C: (int)0xFFFFFFFFu |
32 ones | -1 |
Rust: (-1i32) as u32 |
32 ones | 4294967295 |
Go: int(int32(w)) with w := uint32(0xFFFFFFFF) |
32 ones | -1 |
JavaScript: -1 >>> 0 |
32 ones | 4294967295 |
Python: -1 & 0xFFFFFFFF |
32 ones | 4294967295 |
The C, Rust and Go rows are casts. JavaScript's >>> 0 shifts by zero places, which leaves the bits alone and returns the unsigned reading. Python has no 32-bit type, so the mask keeps the low 32 bits and the result is the unsigned reading of them. The reverse direction in Python (unsigned back to signed) is in masking.
These conversions are how solutions that need unsigned arithmetic accept signed input. Sum of Two Integers (371) in C, Go and Rust converts both inputs to unsigned 32-bit, does the work there, and converts the result back to signed at the end.