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

Tuesday, August 11, 2026

Quick Sort Explained: How It Works, Partitioning, Complexity & JavaScript Implementation

Quick Sort Explained: Partitioning, Recursion & Time Complexity

A practical explanation of Quick Sort using the first element as the pivot, with JavaScript implementation and a visual walkthrough of the partition process.


Sorting an array looks simple until you start asking a more interesting question:

How can we sort an array without repeatedly creating new arrays or moving every element around?

This is where Quick Sort becomes interesting.

Quick Sort doesn't try to find the smallest element one by one. Instead, it chooses an element called a pivot and rearranges the array so that elements smaller than the pivot move toward one side and elements greater than the pivot move toward the other.

Once the pivot reaches its correct position, the same process is repeated on the left and right portions of the array.

The algorithm sounds simple.

The interesting part is how the partitioning actually works.

Quick Sort in One Picture

At a high level, Quick Sort follows three steps:

```
1️⃣
Choose a Pivot

Pick an element from the current range.

2️⃣
Partition

Move smaller and larger elements to their appropriate sides.

3️⃣
Repeat

Recursively sort the two sides.

```

In this implementation, we'll always choose the first element of the current range as the pivot.

Let's Start With an Example

Consider this array:

[2, 5, 6, 3, 1, 4, 7, 9, 8]

We start with the complete array.

Since we're using the first element as the pivot:

Pivot = 2

Now our job is to rearrange the array so that values smaller than 2 end up on the left and values greater than 2 end up on the right.

In this particular array, there is only one value smaller than 2:

1

Once partitioning finishes, the pivot will be placed between those two groups.

Understanding the Partition Function

The most important part of Quick Sort is not the recursion.

It is the partition function.

Our partition function receives:

partition(arr, low, high)

Here:

  • arr is the array.
  • low is the beginning of the current section.
  • high is the end of the current section.

The first thing we do is select the pivot:

const pivot = arr[low];

Because we're using the first element, the pivot is simply:

pivot = arr[low]

Now We Need Two Pointers

This is where the algorithm becomes interesting.

We use two pointers:

```
Left pointer

Starts at the pivot and moves toward the right.

Right pointer

Starts at the end and moves toward the left.

```

In code:

let left = low;
let right = high;

You can think of the pointers as two people searching from opposite directions.

LEFT → → → → → → ← ← ← ← ← RIGHT

What Does the Left Pointer Look For?

The left pointer searches for an element that is greater than the pivot.

Why?

Because a value greater than the pivot doesn't belong on the left side. We want to eventually move it toward the right.

while (left < high && arr[left] <= pivot) {
    left++;
}

As long as the current value is less than or equal to the pivot, we keep moving.

The pointer stops when it finds a value that is greater than the pivot.

Left pointer's job:
Find something that belongs on the right side.

What Does the Right Pointer Look For?

The right pointer does the opposite.

It searches for an element that is smaller than the pivot.

while (right > low && arr[right] >= pivot) {
    right--;
}

As long as the current value is greater than or equal to the pivot, the pointer moves left.

It stops when it finds a value that is smaller than the pivot.

Right pointer's job:
Find something that belongs on the left side.

The Key Idea Behind the Swap

Now both pointers have found something useful.

The left pointer found an element that is too large for the left side.

The right pointer found an element that is too small for the right side.

So we swap them.

[arr[left], arr[right]] = [arr[right], arr[left]];

This is the heart of the partition operation.

Too large ← LEFT       RIGHT → Too small
↓ SWAP ↓
Too small → LEFT       RIGHT ← Too large

We continue doing this until the two pointers meet or cross.

Then Comes the Important Pivot Swap

Once the two pointers are finished, we still haven't placed the pivot in its final position.

Remember that our pivot is still sitting at:

arr[low]

The right pointer has now stopped at the position where the pivot belongs.

So we perform:

[arr[low], arr[right]] = [arr[right], arr[low]];

This final swap is what puts the pivot into its correct sorted position.

After partition:

Every element to the left of the pivot is smaller than or equal to it, and every element to the right is greater than or equal to it.

Quick Sort's Secret: Recursion

Once the pivot is in its correct position, we have solved one part of the problem.

We don't need to move that pivot again.

Instead, we divide the problem into two smaller problems.

Left side    | PIVOT |    Right side



Quick Sort left       Quick Sort right

That's exactly what these two lines do:

quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);

The same partitioning process keeps happening until each section contains zero or one element.

Visualizing the Recursion

You can imagine Quick Sort breaking the array down like this:

[2, 5, 6, 3, 1, 4, 7, 9, 8]
        ↓
[1]   [2]   [5, 6, 3, 4, 7, 9, 8]
                   ↓
               [3, 4]   [5]   [6, 7, 9, 8]
                            ↓
                             [6] [7] [8] [9]

Eventually, every element reaches a position where there is nothing left to sort around it.

When Does the Recursion Stop?

Every recursive algorithm needs a base case.

For Quick Sort, the base case is very simple:

if (low >= high) {
    return;
}

Why?

Because an array section containing zero or one element is already sorted.

0 elements → already sorted
1 element → already sorted

The Complete JavaScript Implementation

Putting everything together, the implementation looks like this:

const arr = [2, 5, 6, 3, 1, 4, 7, 9, 8];

function partition(arr, low, high) {
const pivot = arr[low];


let left = low;
let right = high;

while (left < right) {

    // Find an element greater than the pivot
    while (left < high && arr[left] <= pivot) {
        left++;
    }

    // Find an element smaller than the pivot
    while (right > low && arr[right] >= pivot) {
        right--;
    }

    // Swap the elements
    if (left < right) {
        [arr[left], arr[right]] = [arr[right], arr[left]];
    }
}

// Put pivot in its correct position
[arr[low], arr[right]] = [arr[right], arr[low]];

return right;


}

function quickSort(arr, low, high) {


// Base case: zero or one element
if (low >= high) {
    return;
}

const pivotIndex = partition(arr, low, high);

// Sort left side of pivot
quickSort(arr, low, pivotIndex - 1);

// Sort right side of pivot
quickSort(arr, pivotIndex + 1, high);


}

quickSort(arr, 0, arr.length - 1);

console.log(arr);

Time Complexity: Where Quick Sort Gets Interesting

Quick Sort doesn't always divide the array equally.

And that is exactly why its time complexity depends heavily on the pivot.

Best Case: O(n log n)

The best situation occurs when the pivot divides the array into roughly equal halves every time.

n

n/2    n/2
↓        ↓
n/4 n/4  n/4 n/4

...

Each level processes approximately n elements, and there are roughly log n levels.

Therefore:

O(n log n)

Average Case: O(n log n)

Even when the partitions aren't perfectly balanced, Quick Sort generally performs very well on average.

With a reasonably good pivot selection strategy, the expected complexity is:

O(n log n)

Worst Case: O(n²)

Here's where our choice of the first element as the pivot becomes important.

Imagine the array is already sorted:

[1, 2, 3, 4, 5, 6, 7, 8, 9]

We choose 1 as the pivot.

There are no elements smaller than 1, so the pivot ends up at the beginning.

Now Quick Sort has to process:

9 → 8 → 7 → 6 → 5 → 4 → 3 → 2 → 1

Instead of dividing the problem in half, we're effectively removing only one element at each step.

Worst-case complexity: O(n²)

So Why Not Just Always Use the First Element?

This is the obvious question.

If choosing the first element works and keeps the implementation simple, why do many implementations use a random pivot?

The answer isn't that the first-element approach is wrong.

It's that the input data can make its performance predictable.

If you know your data is already sorted, reverse sorted, or has a structure that repeatedly produces poor partitions, always choosing the first element can lead to the worst case.

Randomizing the pivot makes it much harder for a particular input arrangement to consistently produce terrible partitions.

First element pivot
Simple, predictable, easy to understand.

``` Random pivot
Reduces the likelihood of repeatedly getting highly unbalanced partitions. ```

For learning Quick Sort, however, using the first element is an excellent way to understand the algorithm because the partition logic is easier to reason about.

What About Space Complexity?

Quick Sort is an in-place sorting algorithm in this implementation because we're rearranging elements inside the original array instead of creating separate arrays for every partition.

However, recursion still consumes stack space.

Case Time Recursion Stack
Best O(n log n) O(log n)
Average O(n log n) O(log n)
Worst O(n²) O(n)

Quick Sort vs Merge Sort

Quick Sort is often compared with Merge Sort because both have an average time complexity of O(n log n).

Feature Quick Sort Merge Sort
Average time O(n log n) O(n log n)
Worst case O(n²) O(n log n)
Extra array memory Low Higher
In-place Yes Typically no
Main idea Partition around pivot Split and merge

The Easiest Way to Remember Quick Sort

If you forget the implementation details, remember these three questions:

```

1. What is my pivot?
In our implementation: the first element.

2. What is partition doing?
Finding misplaced elements on both sides and swapping them.

3. What happens after partition?
The pivot is in its correct position, so recursively sort the left and right sides.

```
Pick → Partition → Place Pivot → Recurse

Final Thoughts

Quick Sort is a great example of how an algorithm can look complicated in code while being surprisingly simple at the conceptual level.

We choose a pivot, use two pointers to find misplaced elements, swap them, place the pivot in its correct position, and repeat the same process on the two smaller sections.

The implementation above deliberately uses the first element as the pivot. That makes the algorithm easy to understand and is a good starting point for learning partitioning and recursion.

But it also teaches an important lesson about algorithms:

The algorithm itself is only part of the story.

How you choose the pivot can determine whether Quick Sort behaves like O(n log n) or falls all the way to O(n²).

Once you understand that trade-off, you're no longer just memorizing Quick Sort.

You're understanding why the algorithm behaves the way it does.

Quick Takeaways

  • Quick Sort uses a pivot to partition an array.
  • This implementation chooses the first element as the pivot.
  • The left pointer searches for an element greater than the pivot.
  • The right pointer searches for an element smaller than the pivot.
  • Those misplaced elements are swapped.
  • The pivot is finally placed in its correct position.
  • Quick Sort recursively sorts the two sides.
  • Average and best-case time complexity is O(n log n).
  • Worst-case time complexity is O(n²).
  • Pivot selection can significantly affect performance.

Tags: Quick Sort, Quick Sort JavaScript, Sorting Algorithms, Algorithms, Data Structures, JavaScript Algorithms, DSA, Recursion, Partition Algorithm, Time Complexity, Space Complexity, Coding Interview, Software Development

Friday, August 7, 2026

AI Coding Agents Replace IDEs: What Developers Need to Know

The IDE Is Dying: How AI Coding Agents Are Changing Software Development in 2026

Software development is moving from writing code line by line to directing AI agents that can understand, modify, test, and debug entire codebases.


For decades, the software development workflow was remarkably consistent.

Open your IDE. Find the right file. Write some code. Run the application. See an error. Fix it. Run the tests. Commit the changes. Open a pull request.

The developer was at the center of almost every step.

That workflow is now changing.

In 2026, AI coding agents can do much more than autocomplete the next line of code. They can inspect an entire repository, understand relationships between files, execute terminal commands, modify multiple files, run tests, investigate failures, and iterate on their own.

The uncomfortable question is no longer:

"Can AI write code?"

The more interesting question is:

"If AI can already write much of the code, what exactly should software developers be doing?"

From Code Completion to Code Execution

The first generation of AI coding tools primarily helped developers write code faster.

You typed a function and the AI suggested the next few lines. You accepted the suggestion, changed it, and continued working.

This was useful, but the developer remained firmly in control of the implementation.

AI coding agents represent a different model.

Instead of asking an AI to complete a function, you can give it a higher-level objective:

Add authentication to this .NET API.
Protect the orders endpoint.
Add appropriate tests.
Run the test suite and fix any failures.

The important difference is that the developer isn't specifying every implementation step.

The agent decides how to approach the task, explores the repository, makes changes, executes commands, observes the results, and continues working.

That is a fundamental shift in the development workflow.

What Makes an AI Coding Agent Different?

It helps to separate traditional AI-assisted coding from agentic coding.

Traditional AI Assistant AI Coding Agent
Suggests code Executes development tasks
Usually focused on the current context Can inspect the repository
Developer drives the workflow Developer delegates parts of the workflow
Generates code Generates, executes, tests and iterates
Usually reactive Can operate through multiple steps

Tools such as Claude Code, OpenAI Codex, GitHub Copilot's agentic capabilities, and other emerging coding agents are pushing development in this direction.

The important innovation isn't simply a better language model.

It is the combination of the model with tools, context, a terminal, a filesystem, source control, tests, and an iterative feedback loop.

The New Software Development Loop

The traditional workflow looks something like this:

Developer → IDE → Code → Test → Debug → Git → Pull Request

An agent-based workflow looks different:

Developer

AI Agent

Repository + Terminal + Tools + Tests

Implementation

Testing & Feedback

Pull Request

The developer hasn't disappeared.

The developer has moved one level higher in the workflow.

The Developer Becomes the Orchestrator

This may be the most important change.

Developers have traditionally been judged by how effectively they can turn requirements into code.

But when an AI agent can produce hundreds of lines of implementation in seconds, typing speed becomes much less important.

Instead, developers increasingly need to answer questions such as:

  • What should the system actually do?
  • What constraints must the implementation follow?
  • Which architectural approach is appropriate?
  • What should the agent be allowed to change?
  • How do we verify that the implementation is correct?
  • How do we know the tests actually validate the intended behavior?

This is why AI-assisted development doesn't necessarily eliminate engineering expertise.

In many cases, it makes engineering judgment more important.

But There Is a Problem

There is a dangerous assumption surrounding AI coding agents:

Faster code generation does not automatically mean better software.

An agent can confidently implement the wrong architecture.

It can misunderstand a business requirement. It can introduce unnecessary dependencies. It can modify code that it shouldn't touch. It can produce tests that pass while failing to test the actual requirement.

And perhaps most importantly, it can produce code that looks correct.

That last part makes AI-generated code particularly interesting.

A syntax error is easy to catch.

A subtle architectural mistake is not.

The Verification Problem

Imagine asking an AI agent to implement payment processing.

The agent creates the API endpoint. It validates the request. It calls the payment provider. It stores the transaction. It creates tests.

The tests pass.

Everything looks great.

But what happens if the payment request succeeds while the database transaction fails?

What happens if the webhook arrives twice?

What happens if the payment expires at exactly the same time the webhook arrives?

These aren't syntax problems.

They are system design problems.

And this is where human engineering judgment remains extremely valuable.

AI Agents Are Good at Implementation. Humans Still Own the Intent.

This leads to an important distinction.

AI is increasingly capable of answering:

"How should I implement this?"

Developers still need to answer:

"What should actually be implemented, and why?"

The difference may sound small, but it changes the entire role of the developer.

The Rise of the Agentic Developer

I don't think the future of software development is "AI versus developers."

A more realistic future is developers working with multiple specialized AI agents.

One agent could investigate an issue.

Another could implement the fix.

Another could generate tests.

Another could review the changes for security or architectural problems.

The developer becomes the person coordinating the process and making the final engineering decisions.

A possible future workflow
Requirement

Planning Agent

Coding Agent

Testing Agent

Security Review Agent

Human Review

Production

This is much closer to managing a small engineering team than traditional autocomplete.

So Is the IDE Actually Dying?

Probably not.

At least not in the literal sense.

Developers will continue to use editors and IDEs because visual debugging, code navigation, profiling, design tools, and interactive development remain valuable.

But the role of the IDE may change.

Instead of being the place where developers manually write most of their code, it may increasingly become the place where developers inspect, review, debug, test, and supervise code produced by AI agents.

The IDE may not disappear.

Our relationship with it may.

What Developers Should Learn Now

If AI can increasingly write implementation code, should developers stop learning how to code?

Absolutely not.

In fact, understanding software deeply may become even more important.

Developers should focus on skills that allow them to evaluate and direct AI-generated work.

1. System Design

Understand APIs, databases, caching, queues, authentication, distributed systems, scalability, and failure scenarios.

2. Debugging

Don't just know how to write code. Learn how to understand why a system is behaving incorrectly.

3. Testing

AI can generate tests. Developers still need to know whether those tests actually prove anything useful.

4. Security

Never assume generated code is secure simply because it compiles and passes tests.

5. Architecture

Understanding trade-offs will become more valuable as implementation becomes cheaper.

6. AI Orchestration

Developers should learn how to give agents useful context, define constraints, provide acceptance criteria, and build reliable feedback loops.

The Biggest Skill May Be Knowing What Not to Delegate

There is another skill that doesn't get enough attention:

Knowing when not to use an AI agent.

Some tasks are straightforward and highly repeatable. These are excellent candidates for automation.

Other tasks involve ambiguous requirements, sensitive data, critical security decisions, or complex business rules.

These require much more human oversight.

The best developers won't be the ones who delegate everything to AI.

They'll be the ones who understand what to delegate, what to verify, and what to keep under direct human control.

What This Means for Junior Developers

This shift understandably creates anxiety for developers who are just starting their careers.

If an AI agent can generate a React component, create an API endpoint, write SQL queries, and generate unit tests, where does that leave junior developers?

The answer isn't to avoid AI.

It is to use AI while deliberately learning what it is doing.

Don't just ask an agent to fix an error.

Ask it why the error happened.

Don't blindly accept an architecture.

Ask what alternatives exist and what trade-offs they have.

AI should become a learning accelerator, not a substitute for understanding.

The Real Competitive Advantage

Software development used to have a relatively obvious bottleneck:

Writing code takes time.

AI is attacking that bottleneck directly.

But once code becomes cheaper to produce, another bottleneck becomes more important:

Knowing what code should exist in the first place.

That's why requirements, architecture, product understanding, testing, security, and engineering judgment may become more valuable rather than less.

Final Thoughts

The most important change brought by AI coding agents isn't that machines can write code.

We've already seen that.

The bigger change is that software development is gradually moving from a world where developers produce code to one where developers increasingly direct, evaluate, and verify code produced by machines.

The IDE isn't necessarily disappearing.

The keyboard isn't disappearing.

And developers certainly aren't disappearing.

But the definition of a software developer is changing.

The developers who thrive in this new environment won't necessarily be the ones who can type code the fastest.

They'll be the ones who can give AI the right problem, the right context, the right constraints, and the right feedback.

And perhaps that's the real beginning of the agentic software development era.


Tags: AI Coding Agents, Artificial Intelligence, Software Development, AI Programming, Claude Code, Codex, GitHub Copilot, Developer Tools, Agentic AI, Software Engineering

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 infi...

horizontal ads