What Is Two's Complement? The Bit Trick Behind Every Negative Integer

- Two's complement converts a positive integer to its negative by flipping every bit and adding one — producing exactly one zero, which eliminates the wasted slot that broke sign-magnitude.
- Asymmetric range means a 32-bit int spans -2,147,483,648 to 2,147,483,647:
Math.abs(Integer.MIN_VALUE)returns a negative number because 2,147,483,648 doesn't fit in 32 bits. - Integer overflow wraps silently in Java, C++, Go, and Rust; replace
(low + high) / 2withlow + (high - low) / 2to prevent the classic binary search bug. - Modulo sign follows the divisor in Python (
-7 % 3 = 2) but the dividend in Java, JS, C++, and Go (-7 % 3 = -1) — use((index % n) + n) % nfor safe circular indexing outside Python. - Arithmetic right shift preserves the sign bit so
-1 >> 1stays -1; Java and JS/TS add>>>for a zero-filling logical right shift that C and C++ lack.
You call Math.abs(Integer.MIN_VALUE) in Java and get back a negative number. You negate INT_MIN in C++ and your program silently produces garbage. You write -7 % 3 in Python, get 2, then run the same expression in Java and get -1.
Three bugs. Three languages. One root cause: a decision made in the 1960s about how computers should store negative integers. The answer is two's complement. Once you understand it, a whole class of interview surprises stops feeling like witchcraft and starts feeling like obvious consequences you could have seen coming.
The Obvious Approaches Both Fail
You have 32 bits. That is 2^32 distinct bit patterns. The question is which integers to map them to.
The first obvious idea is sign-magnitude: use the leftmost bit as a sign flag and interpret the rest as the magnitude. Positive 5 is 0000 0101. Negative 5 is 1000 0101. Tidy.
It breaks immediately. Zero now has two representations: 0000 0000 (positive zero) and 1000 0000 (negative zero). Every comparison in hardware has to special-case this. Every addition circuit needs extra logic. Chip designers hate extra logic.
The next attempt was one's complement: negate a number by flipping every bit. Negative 5 becomes 1111 1010. Still broken. One's complement also has two zeros. Positive zero is all zeroes. Negative zero is all ones. You solved nothing.
Flip Every Bit, Then Add One
To negate a number in two's complement: flip every bit, then add one. That is the entire algorithm. No asterisks.
Positive 1 in 8-bit binary: 0000 0001
Flip every bit: 1111 1110
Add 1: 1111 1111
So in 8-bit two's complement, negative 1 is 1111 1111. Or 0xFF if you prefer. Go ahead and stare at that for a second.
Now try it on zero. Positive zero: 0000 0000. Flip: 1111 1111. Add 1: 0000 0000. The carry propagates out of the byte and disappears. You land back at zero. There is exactly one zero, which means every 8-bit pattern maps to exactly one integer value with no wasted slot.
Why This Actually Works
Two's complement is not a cute trick. It is arithmetic modulo 2^n, and it is exactly correct.
In an 8-bit system you have 256 slots, numbered 0 through 255. Define -1 as the number that, when added to 1, gives you 0. In arithmetic modulo 256, that number is 255. And 255 in binary is 1111 1111, exactly what flip-and-add-one produces.
Addition in two's complement works identically for positive and negative numbers because subtraction is the same as adding the modular inverse. The hardware needs one adder circuit. Not two. That was the killer feature for chip designers in the 1960s. Two's complement collapsed addition and subtraction into a single operation, which meant simpler, cheaper, faster processors. They were not going to give that up.
To convince yourself, try 5 + (-3) in 8-bit two's complement:
5 → 0000 0101
-3 → 1111 1101 (flip 0000 0011, add 1)
0000 0101
+ 1111 1101
-----------
0000 0010 → 2 (carry out is discarded)
One addition, right answer, no sign logic. That is the whole pitch.
The Range Is Not Symmetric (And This Will Bite You)
For an n-bit two's complement integer, the range is -2^(n-1) to 2^(n-1) - 1.
For 32-bit integers: -2,147,483,648 to 2,147,483,647. For 8-bit: -128 to 127.
The negative side always holds one extra value. This follows directly from having exactly one zero. With 2^n total slots and one zero, you have 2^n - 1 non-zero values to split. You cannot split an odd number evenly, so one side gets the extra. The negative side got it.
The most negative number has no positive counterpart. Try applying flip-and-add-one to -128 in 8-bit:
1000 0000 → flip → 0111 1111 → add 1 → 1000 0000
You get -128 again. The operation wraps. This is not a bug in your mental model. It is a mathematical consequence of the representation. But it will absolutely wreck your day the first time you encounter it in production.
This is exactly why Math.abs(Integer.MIN_VALUE) returns a negative number in Java. The result of negating -2,147,483,648 would be 2,147,483,648, which does not fit in a 32-bit signed integer. The bit pattern wraps back to the same value. In C++, negating INT_MIN as a signed integer is undefined behavior, which gives the compiler permission to assume it never happens and optimize accordingly, which is its own category of horror. See the integer overflow guide for the full implications.
Overflow Wraps Silently (Usually)
When you add two large positive numbers and the result exceeds the maximum, the carry propagates into the sign bit and the result wraps into negative territory. No warning. No exception. Just a wrong number and a very confusing bug report.
// Java: 32-bit int overflows silently int x = Integer.MAX_VALUE + 1; // -2147483648 // Use Math.addExact if you want the crash instead int y = Math.addExact(Integer.MAX_VALUE, 1); // throws ArithmeticException
# Python uses arbitrary-precision integers. No overflow ever. x = 2_147_483_647 + 1 # 2147483648, just works
// JavaScript: numbers are 64-bit floats. Force 32-bit with bitwise OR. const x = (2147483647 + 1) | 0; // -2147483648
Python is the outlier here. Its integers grow to fit any value, so overflow is not a concept Python recognizes. Java, C++, Go, Rust, and JavaScript with forced 32-bit arithmetic all wrap silently.
This is where the binary search off-by-one bug bleeds into a two's complement bug. The classic midpoint calculation (low + high) / 2 overflows in Java when both values are near Integer.MAX_VALUE. The carry flips the sign bit, producing a negative mid, and your binary search loops forever or crashes.
// Dangerous in Java when low and high are large int mid = (low + high) / 2; // Safe: avoids the intermediate overflow int mid = low + (high - low) / 2;
Python is immune. In every other mainstream language, train yourself to write the safe form by default.
Python Went Its Own Way on Modulo
The sign of the remainder in modulo operations is a direct consequence of how each language defines integer division, which in turn follows from how two's complement rounding works.
| Language | -7 % 3 | Rule |
|---|---|---|
| Python | 2 | Result matches the sign of the divisor |
| JavaScript | -1 | Result matches the sign of the dividend |
| Java | -1 | Result matches the sign of the dividend |
| C++ | -1 | Result matches the sign of the dividend (since C++11) |
| Go | -1 | Result matches the sign of the dividend |
| Rust | -1 | Result matches the sign of the dividend |
Python follows mathematical modular arithmetic: the result is always in [0, divisor). Every other mainstream language uses truncated division, where the remainder has the sign of the dividend.
Python made the mathematically clean choice. Everyone else made the hardware-convenient choice. Neither is wrong. They are just different, and they will disagree exactly when you least expect it.
This bites you in circular index problems. If you compute index % n and index might be negative, Python gives you a valid non-negative index; Java gives you a negative one that will throw ArrayIndexOutOfBoundsException. The fix in Java: ((index % n) + n) % n. Annoying, but at least it works.
Right Shifts on Negative Numbers Are Not What You Think
Two's complement also determines how right shifts behave on negative numbers.
An arithmetic right shift fills the vacated high bits with the sign bit. A logical right shift fills them with zero. Most languages use arithmetic right shift for signed integers, which means shifting -1 right by any amount gives you -1.
print(-1 >> 1) # -1 in Python print(-8 >> 1) # -4 in Python
System.out.println(-1 >> 1); // -1 (arithmetic, sign-extending) System.out.println(-1 >>> 1); // 2147483647 (logical, zero-filling)
console.log(-1 >> 1); // -1 (arithmetic right shift) console.log(-1 >>> 1); // 2147483647 (unsigned right shift, JS/TS only)
JavaScript and TypeScript have >>> for unsigned right shift, which is unusual among mainstream languages. Java also has >>>. C and C++ have only >>, and its behavior on signed integers is technically implementation-defined, though every modern platform does arithmetic shift.
Left-shifting a negative number in C++ is undefined behavior. Compilers at -O2 will exploit it. Cast to unsigned before left-shifting if you need to shift bits in a signed value.
Three Interview Traps to Stop Falling Into
Understanding two's complement closes three patterns that come up regularly in bit manipulation problems:
The INT_MIN negation trap. Any function that negates or takes the absolute value of an integer needs an explicit guard for the minimum value. if n < 0: return -n is wrong at the boundary. Always.
The binary search overflow trap. Use low + (high - low) / 2 instead of (low + high) / 2 in any language with fixed-width integers. This is not optional.
The circular index trap. When computing index % size with a potentially negative index, use ((index % size) + size) % size in Java, C++, Go, and JavaScript. Python handles it automatically, which is one of the few times Python's "do the math right" philosophy saves you code instead of costing you an argument with a coworker.
Knowing these on paper and executing them under pressure while talking are two different skills. SpaceComplexity runs voice-based mock interviews where you have to narrate the sign handling and the modulo behavior as you go, which is where most candidates discover their mental model had gaps they did not know were there.