From afc8a4b0d7b09a8a1c612b1365384c5a4c6bda1d Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Mon, 8 Sep 2025 14:00:13 +0100 Subject: [PATCH] single number algorithm def --- .../src/content/algorithms/single-number.ts | 628 ++++++++++++++++++ 1 file changed, 628 insertions(+) create mode 100644 frontend/src/content/algorithms/single-number.ts diff --git a/frontend/src/content/algorithms/single-number.ts b/frontend/src/content/algorithms/single-number.ts new file mode 100644 index 0000000..71dedd1 --- /dev/null +++ b/frontend/src/content/algorithms/single-number.ts @@ -0,0 +1,628 @@ +import type { + AlgorithmDefinition, + ArrayElementState, + BitState, + BitManipulationState, +} from '@/lib/visualizations/types'; + +/** + * Single Number - LeetCode 136 + * + * Given a non-empty array where every element appears twice except for one, + * find that single element. + * + * Example: nums = [2, 2, 1] + * Output: 1 + * + * Key insight: XOR all numbers together. Pairs cancel out (a ^ a = 0), + * leaving only the single number. + */ + +// Helper to convert number to binary string +function toBinary(n: number, width: number = 4): string { + if (n < 0) { + // Handle negative numbers with two's complement representation + return (n >>> 0).toString(2).slice(-width); + } + return n.toString(2).padStart(width, '0'); +} + +// Helper to create array elements +function createArrayElements( + nums: number[], + highlightIndex?: number, + processedIndices: number[] = [] +): ArrayElementState[] { + return nums.map((value, index) => ({ + value, + index, + state: + index === highlightIndex + ? 'highlighted' + : processedIndices.includes(index) + ? 'dimmed' + : 'normal', + })); +} + +// Helper to create bit state +function createBit( + id: string, + value: number, + state: BitState['state'], + label?: string +): BitState { + return { + id, + value, + bits: toBinary(value), + state, + label, + }; +} + +// Helper to create bit manipulation state +function createBitManip( + id: string, + operands: BitState[], + operation?: BitManipulationState['operation'], + result?: BitState, + label?: string +): BitManipulationState { + return { id, operands, operation, result, label }; +} + +// Input array +const NUMS = [2, 2, 1]; + +export const singleNumberAlgorithm: AlgorithmDefinition = { + id: 'single-number', + title: 'Single Number', + slug: 'single-number', + pattern: { + name: 'Bit Manipulation', + description: + 'Techniques using binary operations (AND, OR, XOR, NOT, shifts) to solve problems efficiently, often achieving O(1) space where other approaches need O(n).', + }, + problemStatement: + 'Given a non-empty array of integers where every element appears twice except for one, find that single element. You must use O(n) time and O(1) space.', + intuition: + 'XOR has two magical properties: a ^ a = 0 (any number XORed with itself is zero) and a ^ 0 = a (XORing with zero preserves the value). When you XOR all numbers together, every pair cancels out to zero, leaving only the single number.', + code: { + language: 'python', + code: `def single_number(nums: list[int]) -> int: + result = 0 + + for num in nums: + result ^= num # XOR each number + + return result`, + }, + initialExample: { + input: { nums: [2, 2, 1] }, + expected: 1, + }, + steps: [ + // ========================================== + // Phase 1: Problem (2 steps) + // ========================================== + { + id: 'problem-1', + phase: 'problem', + explanation: + 'We have an array where every element appears exactly twice, except for one element that appears only once. Our goal is to find this unique element.', + dataState: { + arrays: [ + { + id: 'nums', + label: 'nums', + elements: createArrayElements(NUMS), + }, + ], + pointers: [], + variables: [ + { id: 'nums', name: 'nums', value: '[2, 2, 1]' }, + ], + calculations: [], + }, + }, + { + id: 'problem-2', + phase: 'problem', + explanation: + 'The constraint is key: we must solve this in O(n) time with O(1) extra space. This rules out hash maps and sorting. We need a clever approach.', + dataState: { + arrays: [ + { + id: 'nums', + label: 'nums', + elements: createArrayElements(NUMS), + }, + ], + pointers: [], + variables: [ + { id: 'time', name: 'Time', value: 'O(n)' }, + { id: 'space', name: 'Space', value: 'O(1)' }, + ], + calculations: [], + }, + }, + + // ========================================== + // Phase 2: Intuition - XOR Properties (4 steps) + // ========================================== + { + id: 'intuition-1', + phase: 'intuition', + explanation: + 'Let\'s explore XOR (exclusive or). At the bit level: 0⊕0=0, 1⊕1=0, 0⊕1=1, 1⊕0=1. Same bits give 0, different bits give 1.', + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'xor-rule', name: 'XOR Rule', value: 'same=0, different=1' }, + ], + calculations: [], + bitManipulation: [ + createBitManip( + 'xor-demo', + [ + createBit('bit1', 0, 'comparing'), + createBit('bit2', 0, 'comparing'), + ], + 'XOR', + createBit('res', 0, 'result'), + 'Bit XOR: 0 ⊕ 0 = 0' + ), + ], + }, + }, + { + id: 'intuition-2', + phase: 'intuition', + explanation: + 'Key property #1: a ⊕ a = 0. Any number XORed with itself equals zero. All the same bits cancel out!', + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'property1', name: 'Property 1', value: 'a ⊕ a = 0' }, + ], + calculations: [], + bitManipulation: [ + createBitManip( + 'self-xor', + [ + createBit('a1', 5, 'comparing', 'a'), + createBit('a2', 5, 'comparing', 'a'), + ], + 'XOR', + createBit('zero', 0, 'cancelled', '0'), + '5 ⊕ 5 = 0' + ), + ], + }, + }, + { + id: 'intuition-3', + phase: 'intuition', + explanation: + 'Key property #2: a ⊕ 0 = a. XORing any number with zero preserves it. Zero is the identity element for XOR.', + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'property2', name: 'Property 2', value: 'a ⊕ 0 = a' }, + ], + calculations: [], + bitManipulation: [ + createBitManip( + 'zero-xor', + [ + createBit('a', 5, 'highlighted', 'a'), + createBit('zero', 0, 'normal', '0'), + ], + 'XOR', + createBit('result', 5, 'result', 'a'), + '5 ⊕ 0 = 5' + ), + ], + }, + }, + { + id: 'intuition-4', + phase: 'intuition', + explanation: + 'The insight: if we XOR all numbers together, every pair (a ⊕ a) cancels to 0, leaving only the single number (single ⊕ 0 = single)!', + dataState: { + arrays: [ + { + id: 'nums', + label: 'nums', + elements: createArrayElements(NUMS), + }, + ], + pointers: [], + variables: [ + { id: 'insight', name: 'Insight', value: '2⊕2⊕1 = 0⊕1 = 1' }, + ], + calculations: [], + }, + }, + + // ========================================== + // Phase 3: Pattern (2 steps) + // ========================================== + { + id: 'pattern-1', + phase: 'pattern', + explanation: + 'The XOR accumulator pattern: start with result=0, then XOR each element. Pairs vanish, the unique element remains.', + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'pattern', name: 'Pattern', value: 'XOR Accumulator' }, + { id: 'formula', name: 'Formula', value: 'result ^= num' }, + ], + calculations: [], + bitManipulation: [ + createBitManip( + 'pattern-demo', + [createBit('result', 0, 'highlighted', 'result')], + undefined, + undefined, + 'Initialize result = 0' + ), + ], + }, + }, + { + id: 'pattern-2', + phase: 'pattern', + explanation: + 'This pattern works because XOR is commutative (a⊕b = b⊕a) and associative ((a⊕b)⊕c = a⊕(b⊕c)). The order doesn\'t matter!', + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'commutative', name: 'Commutative', value: 'a⊕b = b⊕a' }, + { id: 'associative', name: 'Associative', value: '(a⊕b)⊕c = a⊕(b⊕c)' }, + ], + calculations: [], + }, + }, + + // ========================================== + // Phase 4: Code (2 steps) + // ========================================== + { + id: 'code-1', + phase: 'code', + explanation: + 'Initialize result to 0. Since a ⊕ 0 = a, starting with 0 means the first XOR just sets result to the first number.', + codeLine: 2, + codeHighlightLines: [1, 2], + dataState: { + arrays: [ + { + id: 'nums', + label: 'nums', + elements: createArrayElements(NUMS), + }, + ], + pointers: [], + variables: [{ id: 'result', name: 'result', value: 0 }], + calculations: [], + }, + }, + { + id: 'code-2', + phase: 'code', + explanation: + 'Loop through each number and XOR it with result. The ^= operator is shorthand for result = result ^ num.', + codeLine: 5, + codeHighlightLines: [4, 5], + dataState: { + arrays: [ + { + id: 'nums', + label: 'nums', + elements: createArrayElements(NUMS), + }, + ], + pointers: [], + variables: [ + { id: 'result', name: 'result', value: 0 }, + { id: 'loop', name: 'loop', value: 'for num in nums' }, + ], + calculations: [], + }, + }, + + // ========================================== + // Phase 5: Execution - Process [2, 2, 1] (10 steps) + // ========================================== + { + id: 'exec-init', + phase: 'execution', + explanation: + 'Initialize result = 0. In binary: 0000. We\'ll XOR each number from the array with this result.', + codeLine: 2, + dataState: { + arrays: [ + { + id: 'nums', + label: 'nums', + elements: createArrayElements(NUMS), + }, + ], + pointers: [], + variables: [{ id: 'result', name: 'result', value: '0 (0000)' }], + calculations: [], + bitManipulation: [ + createBitManip( + 'init', + [createBit('result', 0, 'highlighted', 'result')], + undefined, + undefined, + 'result = 0' + ), + ], + }, + }, + { + id: 'exec-1-setup', + phase: 'execution', + explanation: + 'Process first element: num = 2. In binary: 0010. We\'ll compute result ⊕ num = 0 ⊕ 2.', + codeLine: 4, + dataState: { + arrays: [ + { + id: 'nums', + label: 'nums', + elements: createArrayElements(NUMS, 0), + }, + ], + pointers: [{ id: 'i', name: 'i', index: 0, color: 'default' }], + variables: [ + { id: 'result', name: 'result', value: '0 (0000)' }, + { id: 'num', name: 'num', value: '2 (0010)' }, + ], + calculations: [], + bitManipulation: [ + createBitManip( + 'xor-1', + [ + createBit('result', 0, 'comparing', 'result'), + createBit('num', 2, 'comparing', 'num'), + ], + 'XOR', + undefined, + '0 ⊕ 2 = ?' + ), + ], + }, + }, + { + id: 'exec-1-result', + phase: 'execution', + explanation: + 'result = 0 ⊕ 2 = 2. In binary: 0000 ⊕ 0010 = 0010. Each bit: 0⊕0=0, 0⊕0=0, 0⊕1=1, 0⊕0=0.', + codeLine: 5, + dataState: { + arrays: [ + { + id: 'nums', + label: 'nums', + elements: createArrayElements(NUMS, 0, []), + }, + ], + pointers: [{ id: 'i', name: 'i', index: 0, color: 'default' }], + variables: [ + { id: 'result', name: 'result', value: '2 (0010)', previousValue: '0' }, + ], + calculations: [], + bitManipulation: [ + createBitManip( + 'xor-1', + [ + createBit('prev', 0, 'normal', 'prev'), + createBit('num', 2, 'comparing', 'num'), + ], + 'XOR', + createBit('result', 2, 'result', 'result'), + '0 ⊕ 2 = 2' + ), + ], + }, + }, + { + id: 'exec-2-setup', + phase: 'execution', + explanation: + 'Process second element: num = 2 again. We\'ll compute result ⊕ num = 2 ⊕ 2. Same number!', + codeLine: 4, + dataState: { + arrays: [ + { + id: 'nums', + label: 'nums', + elements: createArrayElements(NUMS, 1, [0]), + }, + ], + pointers: [{ id: 'i', name: 'i', index: 1, color: 'default' }], + variables: [ + { id: 'result', name: 'result', value: '2 (0010)' }, + { id: 'num', name: 'num', value: '2 (0010)' }, + ], + calculations: [], + bitManipulation: [ + createBitManip( + 'xor-2', + [ + createBit('result', 2, 'comparing', 'result'), + createBit('num', 2, 'comparing', 'num'), + ], + 'XOR', + undefined, + '2 ⊕ 2 = ?' + ), + ], + }, + }, + { + id: 'exec-2-result', + phase: 'execution', + explanation: + 'result = 2 ⊕ 2 = 0! The pair cancels out! In binary: 0010 ⊕ 0010 = 0000. Every bit is the same, so XOR gives all zeros.', + codeLine: 5, + dataState: { + arrays: [ + { + id: 'nums', + label: 'nums', + elements: createArrayElements(NUMS, 1, [0]), + }, + ], + pointers: [{ id: 'i', name: 'i', index: 1, color: 'default' }], + variables: [ + { id: 'result', name: 'result', value: '0 (0000)', previousValue: '2' }, + { id: 'insight', name: 'insight', value: 'Pair cancelled!' }, + ], + calculations: [], + bitManipulation: [ + createBitManip( + 'xor-2', + [ + createBit('prev', 2, 'cancelled', '2'), + createBit('num', 2, 'cancelled', '2'), + ], + 'XOR', + createBit('result', 0, 'cancelled', '0'), + '2 ⊕ 2 = 0 (cancelled!)' + ), + ], + }, + }, + { + id: 'exec-3-setup', + phase: 'execution', + explanation: + 'Process third element: num = 1. This is the single number! We\'ll compute result ⊕ num = 0 ⊕ 1.', + codeLine: 4, + dataState: { + arrays: [ + { + id: 'nums', + label: 'nums', + elements: createArrayElements(NUMS, 2, [0, 1]), + }, + ], + pointers: [{ id: 'i', name: 'i', index: 2, color: 'default' }], + variables: [ + { id: 'result', name: 'result', value: '0 (0000)' }, + { id: 'num', name: 'num', value: '1 (0001)' }, + ], + calculations: [], + bitManipulation: [ + createBitManip( + 'xor-3', + [ + createBit('result', 0, 'comparing', 'result'), + createBit('num', 1, 'highlighted', 'num'), + ], + 'XOR', + undefined, + '0 ⊕ 1 = ?' + ), + ], + }, + }, + { + id: 'exec-3-result', + phase: 'execution', + explanation: + 'result = 0 ⊕ 1 = 1! In binary: 0000 ⊕ 0001 = 0001. XORing with 0 preserves the value, so the single number emerges!', + codeLine: 5, + dataState: { + arrays: [ + { + id: 'nums', + label: 'nums', + elements: createArrayElements(NUMS, 2, [0, 1]), + }, + ], + pointers: [{ id: 'i', name: 'i', index: 2, color: 'default' }], + variables: [ + { id: 'result', name: 'result', value: '1 (0001)', previousValue: '0' }, + ], + calculations: [], + bitManipulation: [ + createBitManip( + 'xor-3', + [ + createBit('prev', 0, 'normal', '0'), + createBit('num', 1, 'highlighted', '1'), + ], + 'XOR', + createBit('result', 1, 'result', 'result'), + '0 ⊕ 1 = 1' + ), + ], + }, + }, + { + id: 'exec-done', + phase: 'execution', + explanation: + 'Loop complete! Return result = 1. The pair (2, 2) cancelled out, leaving only the single number: 1.', + codeLine: 7, + codeHighlightLines: [7], + dataState: { + arrays: [ + { + id: 'nums', + label: 'nums', + elements: NUMS.map((value, index) => ({ + value, + index, + state: value === 1 ? 'success' : 'dimmed', + } as ArrayElementState)), + }, + ], + pointers: [], + variables: [ + { id: 'result', name: 'return', value: 1 }, + ], + calculations: [], + bitManipulation: [ + createBitManip( + 'final', + [createBit('result', 1, 'result', 'result')], + undefined, + undefined, + 'Single Number: 1' + ), + ], + }, + }, + { + id: 'exec-summary', + phase: 'execution', + explanation: + 'Summary: 0 ⊕ 2 = 2, then 2 ⊕ 2 = 0 (pair cancelled!), then 0 ⊕ 1 = 1. The single number remains after all pairs cancel.', + dataState: { + arrays: [], + pointers: [], + variables: [ + { id: 'answer', name: 'Answer', value: 1 }, + { id: 'time', name: 'Time', value: 'O(n)' }, + { id: 'space', name: 'Space', value: 'O(1)' }, + ], + calculations: [], + }, + }, + ], +};