Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

16.4 Single Number III

Source: src/main/kotlin/bitset/SingleNumber3.kt (the repo’s file has a bug on the lowbit line; the corrected version is below) Pattern: XOR + lowbit split · Core page

The Problem

Every element appears twice except two elements, which appear once. Find both.

  • Constraints: $2 \le n \le 3 \times 10^4$; linear time, constant space.

Examples

Input:  nums = [1,2,1,3,2,5]   -> Output: [3,5]  (either order)

Intuition — XOR finds the XOR of the two; the lowbit separates them

XOR everything: the pairs cancel, leaving xorSum = a ⊕ b where a, b are the two singletons. Now a ≠ b, so xorSum ≠ 0they differ in at least one bit. Pick one differing bit (the rightmost set bit of xorSum):

$$ \text{lowbit} = xorSum ,&, -xorSum $$

Every number is now in exactly one of two groups: those with that bit set, those without. Crucially, a and b land in different groups (they differ at that bit) while every pair stays together (both copies have the same bits). XOR each group separately:

a = 0; b = 0
for num in nums:
    if num and lowbit != 0: a = a xor num
    else:                   b = b xor num

Group 1’s XOR = a (pairs cancel, b is not in the group), group 2’s XOR = b. Two passes, $O(n)$, $O(1)$ space.

The repo bug (noted): the file writes val rightmostSetBit = xorSum and xorSum — that’s just xorSum, not the lowbit — which would mis-split whenever a ⊕ b has more than one set bit. The correct two’s-complement idiom is xorSum and -xorSum (or xorSum and (xorSum.inv() + 1)). The book shows the corrected line; a > **Repo note:** style flag is in order.

Why is the lowbit the right pick? Any bit where a and b differ works; the lowest one is a one-line computation and is guaranteed nonzero (since xorSum ≠ 0). “Pick any differing bit” is the reasoning; the lowbit is the implementation.

Approach 1 — Hash set (O(n) space)

Add/remove each element; two remain. Correct, but not constant space.

Approach 2 — XOR + lowbit partition (the repo’s version, corrected, optimal)

class SingleNumber3 {
    /**
     * @param nums every element appears twice except two
     * @return     the two elements that appear once
     */
    fun singleNumber(nums: IntArray): IntArray {
        var xorSum = 0
        for (num in nums) {
            xorSum = xorSum.xor(num)               // a xor b (pairs cancel)
        }

        // Rightmost set bit of a xor b — where a and b differ.
        // (The repo file writes `xorSum and xorSum`, which is a bug: it must be `-xorSum`.)
        val rightmostSetBit = xorSum and -xorSum

        var (a, b) = listOf(0, 0)
        for (num in nums) {
            if (num and rightmostSetBit != 0) {
                a = a.xor(num)                     // group with the bit set
            } else {
                b = b.xor(num)                     // group without it
            }
        }
        return intArrayOf(a, b)
    }
}
public class SingleNumberIII {
    /**
     * @param nums every element appears twice except two
     * @return     the two elements that appear once
     */
    public int[] singleNumber(int[] nums) {
        int xor = 0;
        for (int num : nums) xor ^= num;           // a xor b

        int lowbit = xor & -xor;                   // rightmost set bit — where a and b differ
        int a = 0, b = 0;
        for (int num : nums) {
            if ((num & lowbit) != 0) a ^= num;     // group with the bit set
            else b ^= num;                         // group without it
        }
        return new int[]{a, b};
    }
}
#include <vector>

class SingleNumberIII {
public:
    /**
     * @param nums every element appears twice except two
     * @return     the two elements that appear once
     */
    std::vector<int> singleNumber(std::vector<int>& nums) {
        int xorSum = 0;
        for (int num : nums) xorSum ^= num;        // a xor b

        int lowbit = xorSum & -xorSum;             // rightmost set bit — where a and b differ
        int a = 0, b = 0;
        for (int num : nums) {
            if (num & lowbit) a ^= num;            // group with the bit set
            else b ^= num;                         // group without it
        }
        return {a, b};
    }
};
def single_number(nums: list[int]) -> list[int]:
    """
    @param nums: every element appears twice except two
    @return:     the two elements that appear once
    """
    xor = 0
    for num in nums:
        xor ^= num                          # a xor b

    lowbit = xor & -xor                     # rightmost set bit — where a and b differ
    a = b = 0
    for num in nums:
        if num & lowbit:
            a ^= num                        # group with the bit set
        else:
            b ^= num                        # group without it
    return [a, b]
#![allow(unused)]
fn main() {
impl Solution {
    /// @param nums every element appears twice except two
    /// @return     the two elements that appear once
    pub fn single_number(nums: Vec<i32>) -> Vec<i32> {
        let xor = nums.iter().fold(0, |acc, x| acc ^ x);   // a xor b

        let lowbit = xor & -xor;                           // rightmost set bit
        let mut a = 0;
        let mut b = 0;
        for &num in &nums {
            if num & lowbit != 0 { a ^= num; }             // group with the bit set
            else { b ^= num; }                             // group without it
        }
        vec![a, b]
    }
}
}

Dry run

Input: nums = [1,2,1,3,2,5].

xor pass: 1^2^1^3^2^5 = 3^5 = 6 (binary 110).
lowbit = 6 & -6 = 6 & 2 = 2 (binary 010)   # bit 1 is where 3 (011) and 5 (101) differ

partition by bit 1 set:
  with bit 1 set:  2 (010), 3 (011), 2 (010)  -> xor = 2^3^2 = 3
  without bit 1:   1 (001), 1 (001), 5 (101)  -> xor = 1^1^5 = 5

Output: [3, 5] ✓

The magic is visible in the partition: the two copies of 2 both land in the “bit set” group and cancel, while 3 and 5 — the two singletons — are separated by the very bit where they differ. Each group’s XOR ends up being exactly one singleton.

Complexity

Time. Two passes:

$$ T(n) = O(n) $$

Space. Three variables:

$$ S(n) = O(1) $$

Variants & follow-ups

  • Single Number (16.3) — one singleton; the first pass of this algorithm without the split.
  • Single Number II — the mod-3 counting generalization.
  • Two Missing Numbers / find-the-differing-bit family — the lowbit split is the universal “separate by a distinguishing bit” tool.
  • Interview follow-up: “Why does the lowbit split keep every pair together?” Both copies of a number have identical bits, so they always land in the same group and cancel; the two singletons differ at the chosen bit, so they land in different groups and each survives alone. The correctness is entirely in that one observation.