Showing posts with label JavaScript. Show all posts
Showing posts with label JavaScript. Show all posts

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.

Monday, October 16, 2023

Mastering JavaScript Array Functions: A Comprehensive Guide

1. `map()`: Transforming Arrays

The `map()` function is used to create a new array by applying a provided function to each element in the original array. It is particularly handy for transforming data without modifying the original array. Here's an example:

const numbers = [1, 2, 3, 4, 5];

const squaredNumbers = numbers.map(x => x * x);

// squaredNumbers will be [1, 4, 9, 16, 25]


2. `filter()`: Filtering Arrays

`filter()` is used to create a new array that contains all elements from the original array that meet a certain condition defined by a provided function. For instance:

const numbers = [1, 2, 3, 4, 5];

const evenNumbers = numbers.filter(x => x % 2 === 0);

// evenNumbers will be [2, 4]

3. `reduce()`: Reducing Arrays

The `reduce()` function is employed to reduce an array into a single value by applying a given function cumulatively to each element. It's quite versatile, allowing you to perform a wide range of operations, such as summing an array of numbers:

const numbers = [1, 2, 3, 4, 5];

const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);

// sum will be 15

4. `forEach()`: Iterating Over Arrays

`forEach()` is used to iterate over an array and perform a function on each element. Unlike `map()`, it doesn't create a new array but is useful for executing side effects or performing actions on array elements:

const fruits = ["apple", "banana", "cherry"];

fruits.forEach(fruit => console.log(fruit));

// This will log each fruit to the console

5. `find()`: Finding Elements

The `find()` function returns the first element in an array that satisfies a provided condition, as defined by a given function. If no element is found, it returns `undefined`:

const people = [

  { name: "Alice", age: 25 },

  { name: "Bob", age: 30 },

  { name: "Charlie", age: 35 }

];

const alice = people.find(person => person.name === "Alice");

// alice will be { name: "Alice", age: 25 }


6. `sort()`: Sorting Arrays

The `sort()` function allows you to sort the elements of an array in place. It can be used with a comparison function for customized sorting:

const fruits = ["apple", "banana", "cherry"];

fruits.sort();

// fruits will be ["apple", "banana", "cherry"]


7. `concat()`: Merging Arrays

`concat()` is used to merge two or more arrays, creating a new array that contains the elements from all the input arrays:

const arr1 = [1, 2];

const arr2 = [3, 4];

const combined = arr1.concat(arr2);

// combined will be [1, 2, 3, 4]

Monday, April 3, 2023

How to install node.js in Windows Machine

Node.js is a popular open-source JavaScript runtime that allows developers to build server-side applications using JavaScript. In this blog post, we will go through a step-by-step guide on how to install Node.js on a Windows machine.

Step 1: Download Node.js Installer

The first step in installing Node.js on your Windows machine is to download the installer from the official website nodejs.org. You can go to the website and download the LTS version.

Step 2: Run the Installer

Once the installer has finished downloading, double-click on the installer file to run it. The installer will guide you through the installation process. You can select the "Next" button to proceed through the installation steps.

Step 3: Accept License Agreement

When you run the installer, you will be presented with a license agreement. Read through the agreement and select the "I accept the terms in the License Agreement" checkbox if you agree to the terms. Then click on the "Next" button to proceed.

Step 4: Select Destination Folder

The next step in the installation process is to select the destination folder where you want to install Node.js. You can either accept the default location or choose a custom location. Once you have selected the destination folder, click on the "Next" button to proceed.

Step 5: Choose Components

In this step, you can choose which components you want to install. By default, all components are selected. If you want to deselect any components, you can do so by unchecking the checkbox next to the component name. Once you have selected the components you want to install, click on the "Next" button to proceed.

Step 6: Install

After you have selected the components you want to install, click on the "Install" button to start the installation process. The installer will begin installing Node.js on your machine.

Step 7: Complete Installation

Once the installation process is complete, you will see a "Completed" message. Click on the "Finish" button to close the installer.

Step 8: Verify Installation

To verify that Node.js has been installed successfully, open a command prompt and type "node -v". This will display the version of Node.js that has been installed on your machine. If you see the version number, it means that Node.js has been installed successfully.

Conclusion

In this blog post, we went through a step-by-step guide on how to install Node.js on a Windows machine. By following these steps, you should be able to install Node.js without any issues. Once you have installed Node.js, you can start building server-side applications using JavaScript.

 


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

horizontal ads