Wednesday, September 23, 2026

LeetCode 4 Median of Two Sorted Arrays in JavaScript

LeetCode 4, Median of Two Sorted Arrays, asks us to find the median of two already sorted arrays without actually merging them. The key idea is to use binary search on a partition so that the combined left and right halves contain the correct elements.

Instead of building a merged array in O(m + n), we can find the correct partition in O(log min(m,n)) time and O(1) extra space.

Problem Statement

You are given two arrays, nums1 and nums2, both sorted in ascending order. The arrays can have different sizes, and one of them can even be empty.

Your task is to return the median of all elements as if the two arrays were combined into one sorted array. The required runtime is logarithmic rather than linear.

Problem: LeetCode 4 — Median of Two Sorted Arrays
Difficulty: Hard
Topics: Array, Binary Search, Divide and Conquer

View the original problem on LeetCode

Examples

Example 1

Input:
nums1 = [1, 3]
nums2 = [2]

Output:
2.0

The sorted combination would be [1, 2, 3], so the middle value is 2.

Example 2

Input:
nums1 = [1, 2]
nums2 = [3, 4]

Output:
2.5

The combined sorted array is [1, 2, 3, 4]. Since there are four elements, the median is the average of the two middle values: (2 + 3) / 2 = 2.5.

Constraints

  • Both input arrays are sorted in ascending order.
  • Either array can contain between 0 and 1000 elements.
  • The two arrays together contain at least one element.
  • Values can range from -106 to 106.
  • The required runtime is logarithmic.

The important constraint is the logarithmic runtime. A normal merge would take O(m + n), so we need to use the fact that both arrays are already sorted.

LeetCode 4 Median of Two Sorted Arrays — Intuition

The first instinct is usually to merge the two arrays:

nums1 = [1, 2]
nums2 = [3, 4]

Merged = [1, 2, 3, 4]

But we do not actually need the complete merged array. We only need to know where the middle of that array would be.

So instead of asking:

"How do I merge these arrays?"

we ask:

"Where should I cut each array so that everything on the left belongs to the first half?"

The Partition Idea

Imagine placing a partition inside each array:

nums1: [ 1   2 | 3   4 ]
               ↑
            partition

nums2: [ 5   6 | 7   8 ]
               ↑
            partition

          LEFT  |  RIGHT

Everything before the partition belongs to the left half, while everything after it belongs to the right half.

For a correct partition, two things must be true:

  1. The left side must contain exactly half of the total elements.
  2. Every element on the left must be less than or equal to every element on the right.

We Only Need Four Boundary Values

Because each individual array is already sorted, we do not need to compare every element. We only need the values immediately next to the two partitions.

nums1: [ ... maxLeft1 | minRight1 ... ]

nums2: [ ... maxLeft2 | minRight2 ... ]

                 LEFT | RIGHT

The partition is correct when:

maxLeft1 <= minRight2
AND
maxLeft2 <= minRight1

Why are these two comparisons enough? Because values within each array are already sorted. The only possible problem is that an element from the left side of one array might be larger than an element from the right side of the other array.

Why Binary Search?

We can choose the partition position in the smaller array and calculate the required partition in the larger array. That means there is only one partition position we need to search for.

If the partition in the first array is too far right, we move it left. If it is too far left, we move it right. This is exactly the behavior binary search is designed for.

Too far right
       ↓
[ 1  2  3 | 4  5 ]
         ← move left

Correct
[ 1  2 | 3  4  5 ]

Too far left
       ↓
[ 1 | 2  3  4  5 ]
       → move right

Approach

Brute Force Approach

The straightforward solution is to merge the two sorted arrays and then find the middle element or middle two elements.

[1, 3] + [2, 4]

        ↓

[1, 2, 3, 4]

        ↓

median = (2 + 3) / 2 = 2.5

This takes O(m + n) time because we have to process the elements while merging. That does not satisfy the logarithmic requirement.

Optimal Binary Search Approach

  1. Always binary search the smaller array.
  2. Choose a partition in the smaller array.
  3. Calculate the corresponding partition in the larger array.
  4. Check whether the four boundary values form a valid partition.
  5. If the partition is too far right, move left.
  6. If the partition is too far left, move right.
  7. Once the partition is valid, calculate the median from the boundary values.

Calculating the Partitions

Suppose the two arrays have lengths m and n. The total number of elements is:

total = m + n

We want approximately half of these elements on the left:

half = Math.floor((m + n + 1) / 2)

If the partition in nums1 contains partition1 elements, then the partition in nums2 must contain:

partition2 = half - partition1

This guarantees that the left side always contains the required number of elements.

Handling Array Boundaries

A partition can occur before the first element or after the last element. For example:

| 1 2 3
      4 5 |

If there is no element on the left, we treat the left value as negative infinity. If there is no element on the right, we treat the right value as positive infinity.

No left element  → -Infinity
No right element →  Infinity

This lets the same comparison logic work even when one partition is at an array boundary.

Finding the Median

Once we find a valid partition, the median is directly available from the four boundary values.

For an odd number of total elements, the left side contains one extra element. Therefore:

median = max(maxLeft1, maxLeft2)

For an even number of elements, the median is the average of the largest value on the left and the smallest value on the right:

median =
    (max(maxLeft1, maxLeft2)
     + min(minRight1, minRight2)) / 2

Dry Run

Consider:

nums1 = [1, 2]
nums2 = [3, 4]

The total number of elements is 4, so the left side needs 2 elements. We binary search the smaller array, nums1.

Step partition1 partition2 Boundary Values Result
1 1 1 1 | 2 and 3 | 4 Valid partition

The partition looks like this:

nums1: [ 1 | 2 ]
nums2: [ 3 | 4 ]

        LEFT | RIGHT

Left side  = [1, 3]
Right side = [2, 4]

The largest value on the left is 3, while the smallest value on the right is 2. That tells us this particular partition is actually invalid because:

maxLeft2 = 3
minRight1 = 2

3 <= 2  → false

Therefore, the partition in nums1 is too far to the right and must move left.

The next partition is:

nums1: [ | 1 2 ]
nums2: [ 3 4 | ]

Left side  = [3, 4]
Right side = [1, 2]

For this particular setup, the search bounds continue adjusting until the valid partition is found. The correct final partition is:

nums1: [ 1 2 | ]
nums2: [ | 3 4 ]

Combined:

[ 1 2 | 3 4 ]

Now:

maxLeft1 = 2
maxLeft2 = -Infinity

minRight1 = Infinity
minRight2 = 3

So the two middle values are 2 and 3:

median = (2 + 3) / 2
       = 2.5

The important part of the dry run is not the individual arithmetic. The key idea is that binary search keeps moving the partition until the left side and right side are correctly ordered.

Solution Code — JavaScript

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number}
 */
var findMedianSortedArrays = function(nums1, nums2) {
    if (nums1.length > nums2.length) {
        return findMedianSortedArrays(nums2, nums1);
    }

    const m = nums1.length;
    const n = nums2.length;
    const total = m + n;
    const half = Math.floor((total + 1) / 2);

    let left = 0;
    let right = m;

    while (left <= right) {
        const partition1 = Math.floor((left + right) / 2);
        const partition2 = half - partition1;

        const maxLeft1 = partition1 === 0
            ? -Infinity
            : nums1[partition1 - 1];

        const minRight1 = partition1 === m
            ? Infinity
            : nums1[partition1];

        const maxLeft2 = partition2 === 0
            ? -Infinity
            : nums2[partition2 - 1];

        const minRight2 = partition2 === n
            ? Infinity
            : nums2[partition2];

        if (maxLeft1 <= minRight2 && maxLeft2 <= minRight1) {
            if (total % 2 === 1) {
                return Math.max(maxLeft1, maxLeft2);
            }

            return (
                Math.max(maxLeft1, maxLeft2) +
                Math.min(minRight1, minRight2)
            ) / 2;
        }

        if (maxLeft1 > minRight2) {
            right = partition1 - 1;
        } else {
            left = partition1 + 1;
        }
    }
};

Solution Code — C#

public class Solution
{
    public double FindMedianSortedArrays(int[] nums1, int[] nums2)
    {
        if (nums1.Length > nums2.Length)
        {
            return FindMedianSortedArrays(nums2, nums1);
        }

        int m = nums1.Length;
        int n = nums2.Length;
        int total = m + n;
        int half = (total + 1) / 2;

        int left = 0;
        int right = m;

        while (left <= right)
        {
            int partition1 = (left + right) / 2;
            int partition2 = half - partition1;

            int maxLeft1 = partition1 == 0
                ? int.MinValue
                : nums1[partition1 - 1];

            int minRight1 = partition1 == m
                ? int.MaxValue
                : nums1[partition1];

            int maxLeft2 = partition2 == 0
                ? int.MinValue
                : nums2[partition2 - 1];

            int minRight2 = partition2 == n
                ? int.MaxValue
                : nums2[partition2];

            if (maxLeft1 <= minRight2 &&
                maxLeft2 <= minRight1)
            {
                if (total % 2 == 1)
                {
                    return Math.Max(maxLeft1, maxLeft2);
                }

                return (
                    Math.Max(maxLeft1, maxLeft2) +
                    Math.Min(minRight1, minRight2)
                ) / 2.0;
            }

            if (maxLeft1 > minRight2)
            {
                right = partition1 - 1;
            }
            else
            {
                left = partition1 + 1;
            }
        }

        return 0;
    }
}

Code Walkthrough

1. Search the Smaller Array

The first check ensures that nums1 is the smaller array. This is important because binary search should operate over the smallest possible search space.

2. Calculate the Second Partition

Once partition1 is chosen, partition2 is determined automatically. This means we only need to binary search one partition instead of searching two independently.

3. Check the Four Boundary Values

The four values around the partitions tell us whether the split is valid:

maxLeft1 <= minRight2
maxLeft2 <= minRight1

4. Move the Binary Search

If maxLeft1 > minRight2, too many elements were taken from nums1, so the partition moves left. Otherwise, the partition moves right.

5. Calculate the Median

Once the partition is valid, the median can be obtained directly from the largest value on the left and the smallest value on the right. No merged array is required.

Complexity Analysis

Time: O(log min(m, n)) because binary search is performed only on the smaller array.

Space: O(1) because the algorithm uses only a fixed number of variables and does not create a merged array.

Edge Cases

  • One array is empty: The partition logic handles this using infinity boundary values.
  • Both arrays contain duplicate values: The comparison uses less-than-or-equal, so duplicates are handled correctly.
  • Total number of elements is odd: The median is the largest value on the left side.
  • Total number of elements is even: The median is the average of the largest left value and smallest right value.
  • One array is much smaller than the other: Binary search is performed on the smaller array to keep the search space minimal.

Common Mistakes and Tips

  • Do not merge the arrays if the goal is to satisfy the logarithmic runtime requirement.
  • Always binary search the smaller array.
  • Remember that a partition can be at index 0 or at the end of an array.
  • For an even number of elements, use floating-point division when calculating the average.

FAQ

Why do we binary search the smaller array?

Searching the smaller array minimizes the number of possible partition positions. This gives O(log min(m,n)) time.

Why don't we merge the two arrays?

Merging requires O(m + n) time. The problem specifically requires logarithmic runtime, so the solution must use the sorted structure of the arrays instead.

Why are Infinity values used?

A partition can occur at the beginning or end of an array, meaning one side has no actual boundary element. Negative and positive infinity allow those cases to be handled with the same comparison logic.

Related Problems

  • LeetCode 33 — Search in Rotated Sorted Array
  • LeetCode 35 — Search Insert Position
  • LeetCode 153 — Find Minimum in Rotated Sorted Array
  • LeetCode 162 — Find Peak Element

Conclusion

The main lesson from the LeetCode 4 Median of Two Sorted Arrays solution is to stop thinking about merging and instead think about finding the correct partition. Because both arrays are already sorted, only four boundary values are needed to determine whether a partition is valid.

Binary searching the partition in the smaller array reduces the search to O(log min(m,n)) time while using O(1) extra space. This partition technique is a useful pattern to remember for problems where sorted collections need to be combined conceptually without actually merging them.

LeetCode 2542 Maximum Subsequence Score in JS & C#

LeetCode 2542: Maximum Subsequence Score asks us to choose exactly k indices so that the sum of selected values from nums1 multiplied by the minimum selected value from nums2 is as large as possible. The key idea is to sort the pairs by nums2 and use a min-heap to keep the best possible k values from nums1.

This approach avoids checking every combination and gives an O(n log n) solution.

Problem Statement

You are given two arrays, nums1 and nums2, having the same length, along with an integer k. We need to select exactly k indices.

For the selected indices, the score is:

Score = sum(selected nums1 values) × minimum(selected nums2 values)

The goal is to find the maximum possible score.

Original problem: LeetCode 2542 - Maximum Subsequence Score

Problem: 2542. Maximum Subsequence Score
Difficulty: Medium
Topics: Sorting, Heap, Greedy

Examples

Example 1

nums1 = [1,3,3,2]
nums2 = [2,1,3,4]
k = 3

Output: 12

Choosing indices 0, 2, 3 gives:

(1 + 3 + 2) × min(2, 3, 4) = 6 × 2 = 12

Example 2

nums1 = [4,2,3,1,1]
nums2 = [7,5,10,9,6]
k = 1

Output: 30

Since k = 1, we choose one index. Index 2 gives 3 × 10 = 30, which is the maximum.

Constraints

  • nums1 and nums2 have the same length.
  • 1 ≤ n ≤ 100,000.
  • Each value in both arrays is between 0 and 100,000.
  • 1 ≤ k ≤ n.

With up to 100,000 elements, trying every combination of k indices is far too expensive. We need an approach close to O(n log n).

Intuition

The difficult part is that the score has two components:

1. We want a large sum from nums1.

2. We want a large minimum value from nums2.

The second part gives us the key to the problem.

Suppose we sort the paired values by nums2 in descending order:

(nums2, nums1)

Once we reach a particular pair, its nums2 value can be treated as the minimum nums2 value for the selected group.

So the problem becomes:

For every possible minimum nums2, keep the largest possible sum of k nums1 values among the elements seen so far.

This is exactly where the min-heap helps.

Why Do We Need a Min-Heap?

Imagine we have already processed several elements and need to keep exactly k values from nums1.

We want those k values to have the largest possible sum. Therefore, whenever we get a new nums1 value, we can add it to the heap.

If the heap now contains more than k values, we remove the smallest value.

                 Min-Heap

                  2
                /   \
               5     7
              / \
             8   9

        Smallest value = 2
              ↑
        Remove this one

This guarantees that the heap always contains the best k nums1 values from everything processed so far.

The Main Idea Visually

First, pair the arrays so that we never lose the relationship between nums1[i] and nums2[i].

nums1 = [1, 3, 3, 2]
nums2 = [2, 1, 3, 4]

Pairs:

nums2   nums1
  2       1
  1       3
  3       3
  4       2

Sort by nums2 descending:

nums2   nums1
  4       2
  3       3
  2       1
  1       3

Now, when we are at nums2 = 2, all elements seen so far have nums2 >= 2.

Therefore, if we choose any k elements from those processed elements, the minimum nums2 is guaranteed to be at least 2. At this point, we can calculate a candidate score.

Approach

Brute Force Idea

One straightforward idea would be to generate every possible group of k indices, calculate its nums1 sum and minimum nums2, and keep the maximum score.

However, the number of combinations can be enormous. With n as large as 100,000, this approach is not practical.

Optimal Approach

Step 1: Pair the values.

Create pairs containing nums2[i] and nums1[i]. This keeps both values belonging to the same index together.

Step 2: Sort by nums2 in descending order.

This allows the current nums2 value to represent the minimum possible nums2 for the elements processed so far.

Step 3: Maintain a min-heap of nums1 values.

Add each nums1 value to the heap and maintain at most k values. If there are more than k, remove the smallest one.

Step 4: Track the sum.

Instead of recalculating the sum of the heap every time, maintain a running sum. When a value enters the heap, add it. When the smallest value is removed, subtract it.

Step 5: Calculate the score.

Whenever the heap contains exactly k values:

score = current nums2 × sum of k nums1 values

Update the maximum score with this candidate.

Dry Run

Consider:

nums1 = [1, 3, 3, 2]
nums2 = [2, 1, 3, 4]
k = 3

After pairing and sorting by nums2 descending:

(4, 2)
(3, 3)
(2, 1)
(1, 3)
Step Current Pair Heap Values Sum Score
1 (4, 2) [2] 2 Not enough elements
2 (3, 3) [2, 3] 5 Not enough elements
3 (2, 1) [1, 3, 2] 6 2 × 6 = 12
4 (1, 3) [2, 3, 3] 8 1 × 8 = 8

The best score found during the process is 12.

Why Does This Work?

The important observation is that after sorting by nums2 in descending order, every element before the current element has a nums2 value greater than or equal to the current value.

Therefore, when the current value is x, any k elements selected from the processed portion have a minimum nums2 of at least x.

Among those processed elements, the min-heap keeps the k largest nums1 values. That gives us the maximum possible nums1 sum for that particular minimum nums2.

So at every step we are evaluating the best possible score whose limiting nums2 value is the current value.

Solution Code - JavaScript

var maxScore = function (nums1, nums2, k) {
    const pairs = nums1.map((num, index) => [nums2[index], num]);

    pairs.sort((a, b) => b[0] - a[0]);

    // Min Heap
    const heap = [];

    const push = (value) => {
        heap.push(value);

        let i = heap.length - 1;

        while (i > 0) {
            const parent = Math.floor((i - 1) / 2);

            if (heap[parent] <= heap[i]) break;

            [heap[parent], heap[i]] = [heap[i], heap[parent]];
            i = parent;
        }
    };

    const pop = () => {
        const min = heap[0];
        const last = heap.pop();

        if (heap.length > 0) {
            heap[0] = last;

            let i = 0;

            while (true) {
                let smallest = i;
                const left = 2 * i + 1;
                const right = 2 * i + 2;

                if (
                    left < heap.length &&
                    heap[left] < heap[smallest]
                ) {
                    smallest = left;
                }

                if (
                    right < heap.length &&
                    heap[right] < heap[smallest]
                ) {
                    smallest = right;
                }

                if (smallest === i) break;

                [heap[i], heap[smallest]] = [heap[smallest], heap[i]];
                i = smallest;
            }
        }

        return min;
    };

    let sum = 0;
    let max = 0;

    for (const [num2, num1] of pairs) {
        if (heap.length >= k && heap[0] > num1) {
            continue;
        }

        sum += num1;
        push(num1);

        if (heap.length > k) {
            sum -= pop();
        }

        if (heap.length === k) {
            max = Math.max(max, num2 * sum);
        }
    }

    return max;
};

Solution Code - C#

public class Solution {
    public long MaxScore(int[] nums1, int[] nums2, int k) {
        var pairs = nums1.Select((num, index )=> (num2: nums2[index], num1: num))
        .OrderByDescending(x => x.num2)
        .ToList();

        var minHeap = new PriorityQueue<int, int>();
        long max = 0;
        long sum =0;

        foreach(var pair in pairs){
            if(minHeap.Count >= k && minHeap.Peek() > pair.num1)continue;
            sum += pair.num1;
            minHeap.Enqueue(pair.num1, pair.num1);

            if(minHeap.Count > k){
                var n = minHeap.Dequeue();
                sum -= n;
            }

            if(minHeap.Count == k){
                max = Math.Max(max, pair.num2 * sum);
            }


        }
        return max;

        
    }
}

Code Walkthrough

Both implementations follow the same algorithm. The main difference is the heap implementation.

Pairing and Sorting

Each nums1[i] must stay connected to nums2[i], so the two values are stored together. The pairs are then sorted by nums2 in descending order.

Maintaining the Best k Values

The heap contains the current candidates from nums1. Because it is a min-heap, the smallest selected value is always available at the top. If we have more than k values, that smallest value is removed.

This means the heap effectively maintains the largest k nums1 values seen so far.

The Optimization in the C# and JavaScript Solutions

The condition that checks the smallest value in the heap before inserting can skip an unnecessary replacement when the heap already contains k values and the new nums1 value is smaller than the current minimum.

Skipping is safe because the current element has an equal or smaller nums2 value than the elements already processed. Replacing a larger nums1 value with a smaller one cannot improve the resulting sum.

Complexity Analysis

Time: O(n log n) — Sorting takes O(n log n), and each heap operation takes O(log k), giving an overall O(n log n) complexity.

Space: O(n) — The pairs require O(n) space, while the heap uses at most O(k).

Edge Cases

  • k = 1: Each element is evaluated individually, so the answer is the maximum nums1[i] × nums2[i].
  • k = n: All elements must be selected, so there is only one possible group.
  • Zero values: Either array can contain zero, which can make the score zero.
  • Duplicate values: Multiple pairs can have the same nums1 or nums2 value; the heap handles them normally.
  • Large sums: The score can reach around 1015, so C# uses long. JavaScript's Number can represent these integer values exactly because they remain below Number.MAX_SAFE_INTEGER.

Common Mistakes and Tips

  • Do not sort nums1 and nums2 independently. Their indices must remain connected.
  • Sorting by nums2 descending is what lets the current value act as the minimum nums2.
  • Use a min-heap, not a max-heap, because we need to remove the smallest nums1 value when the heap exceeds k.
  • Keep a running sum instead of calculating the heap sum from scratch after every insertion.

FAQ

Why do we sort nums2 in descending order?

After sorting, when we reach a particular nums2 value, every previously processed element has a nums2 value at least as large. Therefore, the current value can serve as the minimum for the selected group.

Why is a min-heap used for Maximum Subsequence Score?

We want to keep the largest k values from nums1. A min-heap makes the smallest of those values immediately available, so it can be removed whenever the heap grows beyond k.

Can this problem be solved without a heap?

A heap is the standard efficient way to maintain the best k values while scanning the sorted pairs. Without an equivalent data structure, repeatedly finding and removing the smallest value can increase the complexity.

Related Problems

  • LeetCode 1383 — Maximum Performance of a Team
  • LeetCode 857 — Minimum Cost to Hire K Workers
  • LeetCode 502 — IPO
  • LeetCode 630 — Course Schedule III

Conclusion

The key to the LeetCode 2542 Maximum Subsequence Score solution is to stop thinking about all possible groups of k elements. Instead, sort by nums2 so that each current value represents a possible minimum, and use a min-heap to maintain the best k nums1 values seen so far.

The pattern is useful beyond this problem: when a score combines a sum of selected values with a minimum or maximum value, sorting around that limiting value and maintaining the best candidates with a heap can often turn an otherwise expensive combination problem into an efficient O(n log n) solution.

Friday, September 18, 2026

LeetCode 2336 Smallest Number in Infinite Set – JavaScript

What looks like an infinite-data-structure problem becomes much simpler once we notice that we do not need to store the entire infinite set. In this LeetCode 2336 Smallest Number in Infinite Set solution, we use a JavaScript Set to remember numbers that were added back and a current pointer to represent the untouched part of the infinite sequence.

The key idea is to separate the numbers that have been removed and later returned from the numbers that have never been removed. This lets us simulate the infinite set without actually creating it.

Problem Statement

LeetCode 2336, Smallest Number in Infinite Set, asks us to implement a data structure that initially contains every positive integer:

1, 2, 3, 4, 5, 6, ...

We need to support two operations:

  • popSmallest() removes and returns the smallest number currently available.
  • addBack(num) adds a number back if it is no longer present in the set.

You can read the original problem on LeetCode 2336 – Smallest Number in Infinite Set .

Problem Number: 2336
Difficulty: Medium
Topics: Set, Design, Heap / Priority Queue

Examples

Example 1

Input:

["SmallestInfiniteSet", "addBack", "popSmallest",
 "popSmallest", "popSmallest", "addBack",
 "popSmallest", "popSmallest", "popSmallest"]

[[], [2], [], [], [], [1], [], [], []]

Output:

[null, null, 1, 2, 3, null, 1, 4, 5]

Calling addBack(2) does nothing because 2 is already present. After removing 1, 2, and 3, calling addBack(1) makes 1 available again, so the next smallest value is 1.

Example 2

Simple sequence:

popSmallest() → 1
popSmallest() → 2
popSmallest() → 3
addBack(2)
popSmallest() → 2
popSmallest() → 4

Once 2 is added back, it becomes smaller than the next untouched number, 4, so it must be returned first.

Constraints

  • 1 <= num <= 1000
  • At most 1000 calls are made to popSmallest and addBack in total.

These constraints are important. Although the conceptual set is infinite, only a limited number of operations can happen. Therefore, we never need to explicitly store the entire infinite set.

Key observation: After at most 1000 operations, only a limited number of values can have been removed and added back. We can track those exceptional values instead of representing infinity.

Intuition

Imagine the infinite set as two parts:

Numbers added back   |   Untouched numbers

For example, suppose we have already removed 1, 2, 3, and 4.

The next untouched number is 5. We can represent that with:

current = 5

Now suppose addBack(2) is called. We do not need to move current backward. We simply remember that 2 has become available again:

heap = {2}
current = 5

The smallest available number is now 2. After returning 2, the Set becomes empty, so the next call can return current, which is 5.

This is exactly what the two variables in the solution represent:

  • current represents the smallest number that has never been removed.
  • heap stores numbers that were removed earlier and subsequently added back.

Approach

Brute-Force Idea

One straightforward idea would be to explicitly store many positive integers and repeatedly search for the smallest available value.

The problem is that the set is conceptually infinite. Storing every positive integer is neither necessary nor practical.

We only need to keep track of numbers that differ from the normal increasing sequence.

Step-by-Step Approach

  1. Initialize current = 1.
  2. Maintain a Set called heap for numbers that have been added back.
  3. In popSmallest(), if the Set contains numbers, find its smallest number, remove it, and return it.
  4. If the Set is empty, return current and increment it.
  5. In addBack(num), only add num when num < current.

Why does the condition num < current work?

Any number greater than or equal to current has not been removed yet. Therefore, it is already present in the infinite set and does not need to be added back.

For example, if current = 5, then 5, 6, 7, 8, ... are already available. Calling addBack(7) would make no difference.

Dry Run

Let's trace the main example while tracking both pieces of state.

Step Operation heap current Result
1 addBack(2) {} 1 No change
2 popSmallest() {} 2 1
3 popSmallest() {} 3 2
4 popSmallest() {} 4 3
5 addBack(1) {1} 4 1 added
6 popSmallest() {} 4 1
7 popSmallest() {} 5 4
8 popSmallest() {} 6 5

Notice the important transition at addBack(1). The current pointer remains at 4 because the untouched sequence still starts from 4. The value 1 is handled separately through the Set.

LeetCode 2336 Smallest Number in Infinite Set Solution in JavaScript

Here is the submitted JavaScript solution exactly as provided:

JavaScript Solution

var SmallestInfiniteSet = function () {

    this.heap = new Set();

    this.current = 1;

};

/**

 * @return {number}

 */

SmallestInfiniteSet.prototype.popSmallest = function () {

    if (this.heap.size > 0) {

        let smallest = Infinity;

        for (let num of this.heap) {

            smallest = Math.min(smallest, num);

        }

        this.heap.delete(smallest);

        return smallest;

    }

    return this.current++;

};

/**

 * @param {number} num

 * @return {void}

 */

SmallestInfiniteSet.prototype.addBack = function (num) {

    if (num < this.current) {

        this.heap.add(num);

    }

};

Code Walkthrough

1. Tracking numbers added back

this.heap = new Set();

Despite its variable name, heap is not a heap. It is a JavaScript Set.

Its purpose is to store numbers that have previously been removed and then added back. The Set also automatically prevents duplicates.

2. Tracking the untouched sequence

this.current = 1;

current starts at 1 because 1 is initially the smallest number.

Whenever there are no added-back numbers waiting in the Set, the next smallest number is simply current.

3. Finding the smallest added-back number

let smallest = Infinity;

for (let num of this.heap) {
    smallest = Math.min(smallest, num);
}

JavaScript's standard Set does not automatically provide the minimum element. Therefore, when the Set contains values, the solution scans through them and keeps the smallest value found.

This is the main reason the actual time complexity of this solution is O(k) for this case, rather than O(log k) as it would be with a real min-heap.

4. Removing the selected number

this.heap.delete(smallest);
return smallest;

Once the smallest added-back number is found, it is removed from the Set because popSmallest() must remove the returned number from the set.

5. Moving through untouched numbers

return this.current++;

If there are no added-back numbers, the next smallest number comes from the untouched sequence.

The post-increment returns the current value and then moves the pointer forward by one.

6. Adding a number back

if (num < this.current) {
    this.heap.add(num);
}

This condition is the key to avoiding unnecessary entries.

If num < current, that number has already been passed by the pointer and therefore could have been removed. It may genuinely need to be added back.

If num >= current, the number is already part of the untouched infinite sequence, so adding it again would not change anything.

Complexity Analysis

Time Complexity

popSmallest() with a non-empty Set: O(k), where k is the number of values currently stored in the Set, because the code scans every value to find the minimum.

popSmallest() with an empty Set: O(1), because it simply returns current and increments it.

addBack(): O(1) average, because JavaScript Set insertion is expected constant time.

With at most 1000 total operations, the maximum Set size is bounded by the problem's operation limit, so this implementation is fast enough for the given constraints.

Space Complexity

O(k), where k is the number of currently stored added-back values. The solution does not store the infinite set itself.

Edge Cases

  • Calling addBack() on a number that was never removed: If the number is greater than or equal to current, it is already available, so nothing is added.
  • Adding the same number multiple times: JavaScript's Set automatically prevents duplicate entries.
  • Multiple numbers added back: The solution scans all values and returns the smallest one.
  • Empty Set: When there are no added-back numbers, current supplies the next smallest value.
  • Added-back number smaller than current: It must be returned before the untouched sequence because it is smaller than current.

Common Mistakes and Tips

  • Do not try to create the infinite set. Track only the part of the state that can change.
  • Do not add numbers that are already present. The num < current check handles this.
  • Remember that JavaScript Set is not a min-heap. Finding the minimum requires iteration.
  • Use Set semantics to handle duplicate add-back operations. A number should not appear multiple times in the stored collection.

FAQ

Why do we need the current pointer?

The pointer represents the smallest positive integer that has never been removed. Instead of storing all untouched numbers, we simply generate them one at a time using current++.

Why does addBack only store numbers smaller than current?

Numbers greater than or equal to current are still part of the untouched infinite sequence. Only numbers below current could have previously been removed and therefore need to be tracked separately.

Is the Set really a heap in this JavaScript solution?

No. The variable is named heap, but its actual type is Set. The code finds the minimum by iterating through the Set, which gives O(k) time for that operation.

Related Problems

  • 41. First Missing Positive
  • 1942. The Number of the Smallest Unoccupied Chair
  • 2336. Smallest Number in Infinite Set
  • 902. Numbers At Most N Given Digit Set

Conclusion

The main lesson from this problem is that an infinite data structure does not necessarily require infinite storage. We only need to represent the numbers that have deviated from the normal increasing sequence.

The current pointer handles untouched numbers, while the Set remembers numbers that have been added back. When the Set is non-empty, we search it for the smallest value; otherwise, we continue from current.

This LeetCode 2336 Smallest Number in Infinite Set solution is particularly useful for learning how to represent an infinite sequence with a small amount of state and how a simple Set can be sufficient when the constraints are small.

Tuesday, September 8, 2026

Longest Substring Without Repeating Characters | Sliding Window in JavaScript | Leetcode #3

Longest Substring Without Repeating Characters | Sliding Window in JavaScript | Leetcode #3


Some string problems look deceptively simple. You read the string, look for repeating characters, and think: "I'll just keep checking until I find the longest one."

But once the input gets large, that approach quickly becomes expensive.

The real trick is to avoid solving the same problem repeatedly. Instead of restarting every time we encounter a duplicate character, we can maintain a sliding window and move only the boundaries that actually need to change.

Key idea: Keep a window containing only unique characters. When a duplicate appears, move the left boundary directly past the previous occurrence of that character.

1. The Problem

We are given a string s. We need to find the length of the longest substring without repeating characters.

A substring must contain consecutive characters from the original string.

Example 1

s = "abcabcbb"

The longest substring without repeating characters is:

"abc"

Its length is 3.

Example 2

s = "bbbbb"

The longest valid substring is simply:

"b"

So the answer is 1.

Example 3

s = "pwwkew"

The longest substring without repeating characters is:

"wke"

Therefore, the answer is 3.

2. Naive Thinking

Before jumping into the optimized solution, let's think about the most straightforward approach.

We could start from every character and try to build a substring until we encounter a duplicate.

For example, for:

"abcabcbb"

Start at a:

a → ab → abc → stop at a

Then start at b:

b → bc → bca → stop at b

And continue doing the same for every position.

This works, but we are repeatedly checking characters that we have already processed.

The problem: We keep rebuilding substrings and repeating work. For a string of length n, this can lead to O(n²) time.

Can we process the string only once?

Yes. This is where the sliding window technique becomes useful.

3. The Key Insight — Sliding Window

Instead of generating every possible substring, maintain a window between two pointers:

left ---------------- right

Everything between left and right represents our current substring.

Our goal is to maintain one important rule:

The current window must contain no duplicate characters.

We move right through the string one character at a time.

Whenever the current character has appeared before, we need to move left forward.

But there is an important optimization.

We don't need to move left one position at a time. We can remember the last index where every character appeared.

Then, when a duplicate is found, we can jump directly to:

previousIndex + 1

This is exactly what the Map in our solution does.

4. Visual Walkthrough

Let's walk through:

s = "abcabcbb"

Step 1 — Start

Initially:

left = 0
right = 0
max = 0

We encounter a.

a
↑
window

Store:

a → 0

Current window length:

0 - 0 + 1 = 1

Step 2 — Add b

a b
↑   ↑
L   R

b is new, so we simply add it to the map.

Current window:

"ab"

Length = 2.

Step 3 — Add c

a b c
↑     ↑
L     R

c is also new.

Current window:

"abc"

Length = 3.

So far:

max = 3

Step 4 — Another a Appears

Now we encounter another a.

a b c a
↑       ↑
L       R

The map tells us that the previous a was at index 0.

Therefore, we can move:

left = 0 + 1
left = 1

The new window becomes:

b c a
  ↑   ↑
  L   R

Notice what happened.

Instead of removing a, then b, then c manually, we jumped directly to the correct position.

Step 5 — Another b Appears

The next character is b.

Its previous position was index 1.

So:

left = 1 + 1
left = 2

The window becomes:

c a b
    ↑
    R

Again, the left pointer jumps directly to the correct location.

The Important Detail

There is one subtle condition in the code:

if ((map.get(s[i]) + 1) >= l)
    l = map.get(s[i]) + 1;

Why do we need this condition?

Because the previous occurrence of a character might already be outside the current window.

In that situation, moving left backward would break the sliding window.

Therefore, left should only move forward, never backward.

Rule: The left pointer should always be at least where it currently is. We only update it when the duplicate's previous position requires it to move forward.

5. Your Code

Here is the JavaScript solution:

  
  /**
   * @param {string} s
   * @return {number}
   */
  var lengthOfLongestSubstring = function (s) {
      let map = new Map()
      let max = 0
      let l = 0

      for (let i = 0; i < s.length; i++) {
          if (map.has(s[i])) {
              if ((map.get(s[i]) + 1) >= l) l = map.get(s[i]) + 1
          }
          map.set(s[i], i)
          max = Math.max(max, i - l + 1)
      }
      return max
  };

Breaking Down the Code

First, we create a Map:

let map = new Map()

This stores each character and its most recent index.

For example:

{
    a → 3,
    b → 4,
    c → 5
}

Next, we maintain:

let max = 0
let l = 0

max stores the longest valid substring found so far.

l represents the left boundary of our sliding window.

The for loop moves the right boundary:

for(let i = 0; i < s.length; i++)

Whenever the character already exists in the map, we check its previous index and potentially move l.

Finally, the current window length is:

i - l + 1

And we update the answer:

max = Math.max(max, i - l + 1)

6. Edge Cases

A good interview solution should also handle edge cases naturally.

Empty String

s = ""

The loop never executes, so the answer remains:

0

Single Character

s = "a"

There is no duplicate, so the answer is:

1

All Characters Are the Same

s = "aaaaa"

Every new character causes the left pointer to move forward. The longest valid substring is only one character.

Answer: 1

All Characters Are Unique

s = "abcdef"

No duplicate is found, so the window continuously grows.

Answer: 6

7. Complexity

The most important advantage of this solution is that every character is processed only once while the left pointer only moves forward.

Time Complexity

O(n)

where n is the length of the string.

Even though we sometimes move the left pointer, it never moves backward. Across the entire algorithm, both pointers move at most a linear number of times.

Space Complexity

O(min(n, k))

where k represents the number of possible distinct characters.

In the general case, we can simply describe it as O(n) auxiliary space.

8. How I Would Explain This in an Interview

If asked to explain this solution in an interview, I would keep it simple:

"I use a sliding window to maintain a substring containing only unique characters."

```

"I keep two pointers: a left pointer and the current index acting as the right pointer."

"I also use a Map to store the most recent index of every character."

"When I encounter a character that already exists in the Map, I move the left pointer to one position after its previous occurrence."

"However, I only move the left pointer forward because the previous occurrence might already be outside the current window."

"After processing each character, I calculate the current window size and update the maximum. This gives O(n) time complexity."

```

9. The Reusable Pattern

The biggest takeaway from this problem isn't just the answer. It's recognizing a pattern.

Whenever you see a problem involving a contiguous substring or subarray with some kind of condition, you should immediately consider the sliding window technique.

Typical signals include:

  • Longest substring satisfying a condition
  • Shortest substring satisfying a condition
  • Subarray with a particular sum or property
  • Finding a window with at most K distinct elements
  • Finding a window with exactly K distinct elements
  • Maintaining frequency/count information inside a range

A common template looks like this:

let left = 0

for (let right = 0; right < n; right++) {

    // Add current element

    // If window becomes invalid:
    // move left until valid again

    // Update answer
}

In this particular problem, the Map allows us to make the window adjustment even faster by jumping directly to the required position.

10. Conclusion

Longest Substring Without Repeating Characters is a classic problem for learning the sliding window pattern.

The naive approach repeatedly examines overlapping substrings, which leads to unnecessary work.

The optimized solution keeps a dynamic window of unique characters and uses a Map to remember where each character was last seen.

The most important idea to remember is:

Don't restart the search when the window becomes invalid. Move the window forward and reuse the work you've already done.

Once this way of thinking becomes familiar, many substring and subarray problems that initially look like O(n²) brute-force problems become straightforward O(n) sliding-window solutions.

Pattern to remember: Sliding Window + HashMap + Two Pointers

Friday, September 4, 2026

Container With Most Water – Two Pointer Solution in JavaScript | LeetCode #11

What if the best answer is hiding at the two ends?

At first glance, Container With Most Water looks like a problem where we need to try every possible pair of lines.

There are potentially thousands or even millions of pairs. So the obvious solution quickly becomes too slow.

But there is a simple observation that lets us eliminate huge numbers of possibilities without checking them.

The trick is a classic Two Pointer technique.

1. The Problem

You are given an integer array height. Each element represents the height of a vertical line.

The line at index i and the line at index j can form a container. The goal is to find the pair of lines that can hold the maximum amount of water.

The amount of water between two lines is determined by two things:

  • The distance between the two lines.
  • The height of the shorter line.
Formula:

Area = min(height[i], height[j]) × (j - i)

For example, if two lines have heights 8 and 5, and they are 4 positions apart:

Area = min(8, 5) × 4 = 5 × 4 = 20

2. Naive Thinking: Try Every Pair

The first idea that naturally comes to mind is:

  1. Pick every possible left line.
  2. Pick every possible right line.
  3. Calculate the area.
  4. Keep track of the maximum.

That means checking every pair:

for (let i = 0; i < height.length; i++) {
    for (let j = i + 1; j < height.length; j++) {
        // calculate area
    }
}

If the array contains n elements, there can be roughly pairs.

Therefore, the brute-force approach has:

Time Complexity: O(n²)

That is the part we need to improve.

3. The Key Insight: The Shorter Line Controls the Water

This is the most important observation in the problem.

Consider two lines:

Left = 3
Right = 7

Even though the right line is height 7, the container can only hold water up to height 3.

So the area is:

min(3, 7) × width

Now suppose we move the taller line.

The width becomes smaller, but the limiting height is still potentially 3.

So moving the taller line cannot give us a better answer while the shorter line remains unchanged.

The Rule

Always move the pointer pointing to the shorter line.

This single observation reduces the problem from O(n²) to O(n).

4. Visual Walkthrough

Let's imagine the array:


Index:   0   1   2   3   4   5   6   7
Height:  1   8   6   2   5   4   8   3

We start with two pointers:


i = 0
j = 7

The two heights are:


height[i] = 1
height[j] = 3

width = 7

Therefore:

Area = min(1, 3) × 7 = 7

The shorter line is at i, so we move i forward.


i → 1
j → 7

Now:


height[i] = 8
height[j] = 3
width = 6

Area = min(8, 3) × 6
     = 18

The shorter line is now at j, so we move j backward.

We continue this process until the two pointers meet.

Why don't we miss the answer?

Suppose the left line is shorter. Keeping that same left line and moving the right pointer inward can only reduce the width.

Since the left line is already limiting the height, those possibilities cannot produce a better container than the current pair.

Therefore, we can safely discard that shorter line and move its pointer.

5. Your JavaScript Solution

Here is the two-pointer implementation:

/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function (height) {
    let area = 0
    let i = 0; j = height.length - 1

    while (i < j) {
        if (height[i] > height[j]) {
            area = Math.max(area, height[j] * (j - i));
            j--
        } else {
            area = Math.max(area, height[i] * (j - i));
            i++
        }
    }

    return area
};

How the Code Works

We initialize two pointers:

let i = 0;
let j = height.length - 1;

One pointer starts at the beginning and the other starts at the end.

Then we repeatedly calculate the current container area:

height[shorterPointer] * (j - i)

If the left line is taller than the right line:

j--

Otherwise, we move the left pointer:

i++

Notice that your implementation handles equal heights by moving the left pointer. This is completely valid because either pointer can be moved when both heights are equal.

6. Edge Cases

Case 1: Two Elements

If there are only two lines, there is only one possible container.

[5, 8]

Area = min(5, 8) × 1
     = 5

Case 2: All Heights Are Equal

[5, 5, 5, 5]

The algorithm still works normally. When both heights are equal, the else branch moves the left pointer.

Case 3: Increasing Heights

[1, 2, 3, 4, 5]

The algorithm starts from both ends and keeps moving the shorter pointer. No nested loops are required.

Case 4: Very Large Input

This is where the two-pointer solution really matters. Instead of checking every pair, each pointer moves across the array only once.

7. Complexity Analysis

Metric Complexity
Time O(n)
Space O(1)

Why O(n)?

Both pointers only move toward each other. The left pointer never moves backward, and the right pointer never moves forward.

Therefore, across the entire algorithm, there are at most n pointer movements.

The algorithm also uses only a few variables, so the extra space is O(1).

8. How to Explain This in an Interview

If you get this problem in an interview, avoid immediately jumping into code. Explain the reasoning first.

Step 1: The area between two lines is determined by the shorter line and the distance between them.

Step 2: Start with pointers at both ends to maximize the initial width.

Step 3: Calculate the current area and update the maximum.

Step 4: Move the pointer pointing to the shorter line because keeping that line while reducing the width cannot produce a better result.

Step 5: Continue until the pointers meet.

A concise interview explanation could be:

I use two pointers starting from both ends. For every pair, the shorter height limits the amount of water, while the distance gives the width. After calculating the area, I move the pointer at the shorter height because moving the taller pointer would only decrease the width without increasing the limiting height. This allows us to eliminate impossible candidates and solve the problem in O(n) time and O(1) space.

9. The Reusable Pattern: Two Pointers

The most valuable thing about this problem isn't memorizing the solution. It's recognizing the pattern.

The Two Pointer technique is useful when:

  • You are working with an array or string.
  • You need to compare elements from opposite sides.
  • The search space can be reduced based on a condition.
  • Moving one pointer allows you to safely eliminate possibilities.

The general structure looks like this:

let left = 0;
let right = arr.length - 1;

while (left < right) {

    // calculate / evaluate current state

    if (some_condition) {
        left++;
    } else {
        right--;
    }
}

The important question to ask yourself when you see a similar problem is:

"Can I eliminate an entire set of possibilities by moving one pointer?"

If the answer is yes, there is a good chance a two-pointer solution can turn a quadratic approach into a linear one.

10. Conclusion

Container With Most Water is a great example of how an algorithmic insight can completely change the performance of a solution.

The brute-force approach checks every possible pair and takes O(n²). The two-pointer approach intelligently eliminates impossible candidates and brings the complexity down to O(n).

The key idea to remember is simple:

Find the shorter line → calculate area → move the shorter pointer.

Once this reasoning becomes familiar, you'll start seeing the same pattern in many other array and string problems.

Don't just remember the code. Remember why moving the shorter pointer is safe. That's the real solution.

LeetCode #1, Two Sum Explained: The HashMap Pattern Behind O(n) Solutions

Two Sum in C# & JavaScript: The HashMap Pattern Explained

LeetCode Problem #1 — Two Sum

The Two Sum problem is one of the most popular beginner problems on LeetCode. It looks simple at first, but it teaches one of the most important patterns in coding interviews: using a HashMap to reduce an O(n²) problem to O(n).

In this article, we will understand the problem, derive the efficient approach step by step, walk through the algorithm using an example, and implement it in both C# and JavaScript.

💡 The key idea:
Don't search for the second number. Calculate what the second number needs to be.

📌 Problem Statement

You are given an integer array nums and an integer target. Your task is to find the indices of two numbers whose sum is equal to the target.

You can assume that exactly one valid solution exists, and you cannot use the same element twice. The answer can be returned in any order.

For the official problem statement and constraints, see LeetCode — Two Sum .

Example

nums = [2, 7, 11, 15]
target = 9

We need to find two numbers whose sum is 9.

Here:

2 + 7 = 9

The indices are:

2 → index 0
7 → index 1

Therefore, the answer is:

[0, 1]

🧠 First Thought: The Brute Force Approach

The most straightforward solution is to compare every number with every other number.

For example:

for every i
    for every j
        check if nums[i] + nums[j] == target

This works, but there is a problem.

If the array contains 10,000 elements, checking every possible pair can require roughly comparisons.

⚠️ Problem:
Brute force has a time complexity of O(n²). The Two Sum problem specifically asks us to find an approach better than O(n²).

🚀 The Important Observation

Instead of asking:

"Which number should I pair with the current number?"

We can ask:

"What number do I need to reach the target?"

Suppose:

current number = 2
target = 9

The number we need is:

9 - 2 = 7

So instead of searching through the rest of the array for 7, we can simply check whether we have already seen 7.

🧩 The Formula

If the current number is x, we need another number y such that:

x + y = target

Rearranging the equation:

y = target - x

That gives us the key formula:

complement = target - current number

🗂️ Why Do We Need a Dictionary / HashMap?

We need a data structure that can quickly tell us whether a number has already appeared.

A HashMap is perfect for this.

In C#, we use:

Dictionary<int, int>

We store:

number → index

For example:

Number Index
2 0
7 1

This allows us to check whether a required number exists in approximately constant time.

🔄 How the Algorithm Works

For every element in the array, we perform three simple steps.

1️⃣ Calculate the complement
complement = target - nums[i]
2️⃣ Check the Dictionary
If the complement already exists, we have found the answer.
3️⃣ Store the current number
If the complement doesn't exist, store the current number and its index.

🔍 Step-by-Step Example

Let's use:

nums = [2, 7, 11, 15]
target = 9

Step 1 — Number 2

Current number:

2

Calculate the complement:

9 - 2 = 7

Does the dictionary contain 7?

No.

So we store:

2 → 0

Step 2 — Number 7

Current number:

7

Calculate the complement:

9 - 7 = 2

Now we check the dictionary.

We already have:

2 → 0

So we found the required pair.

The current index is 1, therefore:

[0, 1]

We can immediately return the result without processing the remaining elements.

📊 Visualizing the Process

Array

[ 2, 7, 11, 15 ]

Current = 2

Complement = 9 - 2 = 7

Is 7 already stored?

❌ No

Store 2 → index 0


Current = 7

Complement = 9 - 7 = 2

Is 2 already stored?

✅ Yes

Return [0, 1]

⚠️ Why Do We Check First and Store Later?

This is a very important detail in the implementation.

The order should be:

1. Calculate complement
2. Check dictionary
3. Store current number

Consider this example:

nums = [3, 3]
target = 6

For the first 3:

6 - 3 = 3

The dictionary doesn't contain 3, so we store the first occurrence.

For the second 3, the complement is again 3. This time, the first 3 already exists in the dictionary.

Therefore we return:

[0, 1]
✅ Important:
Checking before storing ensures that we never accidentally use the same array element twice. It also naturally handles duplicate values.

💻 C# Solution

Here is the C# implementation using Dictionary<int, int>:

public class Solution {
    public int[] TwoSum(int[] nums, int target) {
        Dictionary<int, int> dic = new Dictionary<int, int>();

        for(int i = 0; i < nums.Length; i++){
            int val = target - nums[i];

            if(dic.ContainsKey(val)){
                return [dic.GetValueOrDefault(val), i];
            }
            else{
                dic[nums[i]] = i;
            }
        }

        return [];
    }
}

💻 JavaScript Solution

The same HashMap approach can also be implemented in JavaScript using the built-in Map.

JavaScript solution:

var twoSum = function(nums, target) {
    const map = new Map();

    for (let i = 0; i < nums.length; i++) {
        const complement = target - nums[i];

        if (map.has(complement)) {
            return [map.get(complement), i];
        }

        map.set(nums[i], i);
    }

    return [];
};

⏱️ Time and Space Complexity

Approach Time Space
Brute Force O(n²) O(1)
HashMap / Dictionary O(n) average O(n)

Why is the optimized solution O(n)?

We iterate through the array only once. Each dictionary lookup and insertion takes O(1) average time.

Therefore:

n elements × O(1) lookup
= O(n)

🎯 The General Pattern Behind Two Sum

The most valuable part of this problem isn't just the solution. It's the pattern that you can reuse in many other problems.

Store what you've seen

Calculate what you need

Look it up

Return the answer

Whenever you see a problem involving:

  • Finding a pair
  • Finding a complement
  • Checking whether something appeared before
  • Counting occurrences
  • Finding duplicates
  • Matching values efficiently

you should consider whether a HashMap / Dictionary can help.

🎤 Interview Explanation

If an interviewer asks you to explain your solution, you can describe it simply:

"I iterate through the array and calculate the complement of the current number by subtracting it from the target. I store previously seen numbers along with their indices in a HashMap. Before storing the current number, I check whether its complement already exists in the map. If it does, I return the stored index and the current index. This allows the problem to be solved in O(n) average time instead of O(n²)."

🧠 What You Should Remember

  • Brute force checks every possible pair and takes O(n²).
  • The target tells us exactly what number we need.
  • Use target - current to calculate the complement.
  • Store numbers along with their indices.
  • Check the HashMap before storing the current number.
  • The HashMap approach takes O(n) average time.
  • The same pattern works across different programming languages.

🚀 Final Takeaway

Two Sum is often presented as a very easy problem, but the underlying idea is extremely useful. The important lesson isn't simply knowing how to solve Two Sum. It's recognizing when you can replace repeated searching with constant-time HashMap lookups.

Instead of repeatedly asking:

"Where is the number I need?"

calculate it first:

What I need = Target − What I currently have

That simple change in thinking turns the solution from a nested-loop O(n²) approach into an efficient O(n) average-time solution.


📌 Problem: LeetCode — Two Sum

Languages covered: C# and JavaScript

LeetCode 4 Median of Two Sorted Arrays in JavaScript

LeetCode 4, Median of Two Sorted Arrays , asks us to find the median of two already sorted arrays without actually merging them. The key ...

horizontal ads