Interview Pattern Lab: Pair Finder

Build and compare JavaScript pair finders using loops and Map lookups.

Introduction

30 Second Summary

A coding interview solution can look convincing when it handles four numbers. A larger example reveals whether the same idea can cope with real pressure.

In this project, you will build a local JavaScript pair finder that locates two package weights matching a target. You will compare nested loops with a JavaScript Map to see how a data structure changes the amount of work.

What You'll Build

Picture running one file to see the direct solution make 499,500 comparisons while the Map-based solution needs only 1,000 lookups on the same no-solution case.

By the end of this project, you'll have:

  • A working pair finder that returns the matching indices for a small package-weight example.
  • A visible performance comparison showing how nested loops scale on a 1,000-item input.
  • An interview-ready solution backed by five passing edge-case checks plus a one-minute trade-off explanation.
  • Secret Mission: Extend the lab to return every unique value pair while handling duplicates correctly.

Are there any prerequisites?

You need basic familiarity with JavaScript variables, functions, loops, and arrays. Step 1 verifies the installed Node.js runtime plus the Visual Studio Code editor before the lab begins.

Before We Start

Start by locking in the interview skill you are about to prove. You will compare two correct pair-finding approaches to make their time and space trade-offs observable.

Set Up the Interview Lab

Interview practice stalls when uncertain local tools interrupt the first run. A clean workspace keeps your attention on the algorithm problem.

In this step, you'll verify the local tools that power the lab. By the end, your workspace runs app.js successfully.

In this step, get ready to:
  • Verify your local development tools.
  • Create the runnable lab workspace.
  • Run the starter file from the workspace terminal.
Verify the installed tools

Node.js runs JavaScript outside the browser. Visual Studio Code provides the editor and integrated terminal for your lab.

A Linux terminal gives you a direct way to confirm that both tools are available.

  • Open a Linux terminal from your application menu.
  • Check the installed Node.js version by running this command:
node --version

What does this check?

The node command starts the Node.js runtime. The --version option asks it to print its installed version.

✔️ I see a Node.js version

The printed version confirms that your terminal can reach the installed Node.js runtime.

ⓧ Command not found

Node.js is already installed for this project. A missing command usually means the current shell cannot locate the executable.

  • Close the current terminal window.
  • Return to the Linux application menu to open a fresh terminal.
  • Retry the same Node.js version check.
  • Use the official Node.js download page if the refreshed shell still cannot find Node.js.

Node.js version still missing?

Your shell path may need environment-specific attention. Help me restore access to the installed Node.js command.

  • Check the Visual Studio Code command-line shortcut by running this command:
code --version

What does this check?

The code command connects your terminal to Visual Studio Code. The version information confirms that the shortcut is available.

✔️ I see Visual Studio Code information

The printed information confirms that the Visual Studio Code command-line shortcut works.

ⓧ Command not found

The missing shortcut does not block this project. The installed Visual Studio Code desktop application is enough for every remaining action.

Node.js is now verified. Visual Studio Code is either verified from the terminal or ready through the installed desktop application.

Create the workspace

A workspace keeps the source file and its terminal in one place. This gives every later algorithm run the same starting point.

  • Open the Linux application menu.
  • Search for Visual Studio Code.
  • Select Visual Studio Code to launch it.
  • Choose the option for opening a folder from the start screen.
  • Select your home folder in the folder picker.
  • Create a folder named interview-pattern-lab.
  • Select interview-pattern-lab as the workspace.

You should see interview-pattern-lab at the top of the Explorer view. This confirms that Visual Studio Code has opened the correct workspace.

  • Use the Explorer file-creation control to create app.js inside interview-pattern-lab.
  • Add the starter message by pasting this code into app.js:
console.log("Interview Pattern Lab ready");

What does this code do?

The console.log call writes the starter message to the terminal. That message gives you a visible signal when Node.js executes the file.

  • Save app.js by pressing Ctrl+S.
  • Confirm that app.js appears beneath interview-pattern-lab in the Explorer view.

✔️ Awesome, I've got everything!

Your saved app.js contains the starter message.

ⓧ I'd like to double check the full code

  • Compare your entire app.js file with this reference:
console.log("Interview Pattern Lab ready");

Why this is the full file

The starter needs one output line. Later steps replace this line with the pair-finder code.

Starter file not listed?

  • Check that the Explorer heading shows interview-pattern-lab.
  • Check that the filename is exactly app.js.
  • Save the file again with Ctrl+S.
  • Still stuck? Help me create and save app.js in my Visual Studio Code workspace.
Run the starter

The integrated terminal opens within your selected workspace. Commands can reach app.js without a separate folder change.

  • Right-click interview-pattern-lab in the Explorer view.
  • Select Open in Integrated Terminal.

You should see the terminal panel below the editor. Its prompt should show the interview-pattern-lab workspace folder.

Before you run the file, what message do you expect the terminal to print?

  • Confirm the starter works by running this command:
node app.js

What does this command do?

Node.js executes the JavaScript stored in app.js. The terminal displays anything written by its console.log call.

Your terminal prints Interview Pattern Lab ready. You have your first successful run, which means the lab is ready for algorithm code.

Starter message missing?

  • Check that the terminal prompt shows interview-pattern-lab as the current folder.
  • Check that app.js contains the exact starter line from the reference tab.
  • Save app.js before retrying the command.
  • Still stuck? Help me run app.js from the correct Visual Studio Code terminal.

Your local interview lab is ready. Next, you'll build the direct solution and make the cost of nested pair scanning visible.

Make the Brute-Force Cost Visible

Your Node.js workspace is ready. You can now focus on how the pair finder behaves as its input grows.

A correct result on a short array can hide the cost of nested loops. This step counts every candidate pair so that the scaling problem becomes visible.

In this step, get ready to:
  • Implement a brute-force pair finder with a comparison counter.
  • Confirm the correct pair on a small input.
  • Expose the nested-loop cost on a larger no-solution input.
Build the direct solution

A brute-force pair finder checks each index against every later index. Starting the inner loop one position after the outer loop keeps the indices distinct.

  • In app.js, select the starter message line at the top of the file.
  • Replace the selected line with the following function:
function findPairBruteForce(numbers, target) {
  let comparisons = 0;

  for (let left = 0; left < numbers.length; left += 1) {
    for (let right = left + 1; right < numbers.length; right += 1) {
      comparisons += 1;

      if (numbers[left] + numbers[right] === target) {
        return { indices: [left, right], comparisons };
      }
    }
  }

  return { indices: null, comparisons };
}

What does this function do?

  • The left loop chooses the first index in each candidate pair.
  • The right loop begins at left + 1. This keeps the two indices distinct.
  • The comparisons counter increases once for every candidate pair.
  • The function returns matching indices immediately. It returns null after checking every pair without a match.
  • Save app.js by pressing Ctrl+S.
  • Check that Node.js can parse the function by running this command in the integrated terminal:
node app.js

What should you see?

Node.js accepts the file. You return to the terminal prompt without a syntax error.

Seeing a syntax error?

  • Check that every opening brace in findPairBruteForce() has a matching closing brace.
  • Check that the inner loop begins with right = left + 1.
  • Still stuck? Help me check the syntax of my brute-force pair finder.
Prove it on a small input

A short input makes the expected pair easy to verify by hand. The comparison counter also reveals how much work happens before the function finds it.

  • Place the following code directly below the findPairBruteForce() function:
const sampleNumbers = [11, 7, 2, 15];
const sampleTarget = 9;

console.log("\nSmall input");
console.log(
  "Brute force:",
  findPairBruteForce(sampleNumbers, sampleTarget),
);

How does the small check work?

  • The sampleNumbers array contains one pair whose values add to sampleTarget.
  • The matching values are 7 at index 1 plus 2 at index 2.
  • The final call prints the returned indices with the number of comparisons required to reach that pair.
  • Save app.js by pressing Ctrl+S.

Before you run the file, which two indices do you expect the function to return?

  • Test the small input by running this command in the integrated terminal:
node app.js

What should you see?

You should see Small input followed by a result containing indices [1, 2]. The result also shows 4 comparisons.

The function has found the correct pair. The next input reveals what happens when early return cannot help.

Getting different indices?

  • Check that sampleNumbers is exactly [11, 7, 2, 15].
  • Check that sampleTarget is 9.
  • Still stuck? Help me debug the small-input result.
Hit the designed obstacle

The small input ends as soon as a match appears. A larger no-solution input removes that early exit.

  • Place the following stress test below the small-input code:
const stressNumbers = [];
for (let value = 1; value <= 1000; value += 1) {
  stressNumbers.push(value);
}

const stressTarget = -1;
const bruteStress = findPairBruteForce(stressNumbers, stressTarget);

console.log("\n1,000-item no-solution input");
console.log("Brute-force comparisons:", bruteStress.comparisons);

How does the stress test work?

  • The loop fills stressNumbers with the integers from 1 through 1000.
  • The stressTarget value is -1. Two positive values cannot produce this sum.
  • The printed counter exposes how many pairs the nested loops inspect before returning null.
  • Save app.js by pressing Ctrl+S.

Before you run the file, how large do you think the comparison count will become?

  • Reveal the brute-force cost by running this command in the integrated terminal:
node app.js

What does the result reveal?

You should see the small result first. You should then see Brute-force comparisons: 499500 for the no-solution input.

The nested loops inspect every possible pair. This is the quadratic growth described by Big O as O(n²).

Is the comparison count different?

  • Check that the stress loop begins with value = 1.
  • Check that the stress loop continues through value <= 1000.
  • Check that stressTarget is -1.
  • Still stuck? Help me trace the stress-test comparison count.

You have made the scaling problem visible. The pair finder is correct on the small input while the larger input exposes its excessive work.

✔️ Awesome, I've got everything!

Great work. Double check that you saved app.js after exposing the full brute-force comparison count.

ⓧ I'd like to double check the full code

Compare your complete app.js file with this reference:

function findPairBruteForce(numbers, target) {
  let comparisons = 0;

  for (let left = 0; left < numbers.length; left += 1) {
    for (let right = left + 1; right < numbers.length; right += 1) {
      comparisons += 1;

      if (numbers[left] + numbers[right] === target) {
        return { indices: [left, right], comparisons };
      }
    }
  }

  return { indices: null, comparisons };
}

const sampleNumbers = [11, 7, 2, 15];
const sampleTarget = 9;

console.log("\nSmall input");
console.log(
  "Brute force:",
  findPairBruteForce(sampleNumbers, sampleTarget),
);

const stressNumbers = [];
for (let value = 1; value <= 1000; value += 1) {
  stressNumbers.push(value);
}

const stressTarget = -1;
const bruteStress = findPairBruteForce(stressNumbers, stressTarget);

console.log("\n1,000-item no-solution input");
console.log("Brute-force comparisons:", bruteStress.comparisons);

Your direct solution now proves both correctness and cost. Next, you'll use a Map to replace repeated pair scanning with a one-pass complement lookup.

Replace Pair Scanning with a Map

Your brute-force finder returns the right pair. The stress run exposed how often its nested loops revisit possible pairs.

The optimized version needs a memory of values already processed. In this step, you'll use a JavaScript Map to look up the needed complement during one pass.

In this step, get ready to:
  • Build a one-pass complement lookup with a Map.
  • Compare both algorithms on the small input.
  • Measure both operation counters on the stress input.
Add the complement lookup

A Map stores key-value pairs. Each earlier number becomes a key whose value is its array index. The current number determines the complement that the function searches for.

  • In app.js, place your cursor after the closing brace of findPairBruteForce.
  • Add findPairWithMap by pasting this code:
function findPairWithMap(numbers, target) {
  const seenIndices = new Map();
  let lookups = 0;

  for (let index = 0; index < numbers.length; index += 1) {
    const currentNumber = numbers[index];
    const complement = target - currentNumber;
    lookups += 1;

    if (seenIndices.has(complement)) {
      return {
        indices: [seenIndices.get(complement), index],
        lookups,
      };
    }

    seenIndices.set(currentNumber, index);
  }

  return { indices: null, lookups };
}

What does this code do?

  • The seenIndices Map stores each earlier number with its array index.
  • The complement value is the number needed to reach the target.
  • The has() check happens before set(). This order stops one array element from pairing with itself.
  • The lookups counter records one complement check for each processed number.
  • Press Ctrl+S to save app.js.
  • Confirm the expanded file still runs by running this command:
node app.js

What should I see?

Node.js executes the complete file successfully. You should see the existing Small input section finish.

You should also see the existing 1,000-item section report its brute-force comparison count. This confirms the new function parses correctly inside app.js.

File no longer running?

  • Check that findPairWithMap sits outside the closing brace of findPairBruteForce.
  • Compare every opening brace in the new function with its matching closing brace.
  • Still stuck? Help me debug the new Map function.

Solid start. Your Map-based function is now part of a runnable pair finder.

Compare equivalent results

A fair comparison gives both algorithms the same numbers. It also gives both algorithms the same target.

  • In app.js, locate the small-input console block that calls findPairBruteForce.
  • Add the Map result directly below that block by pasting:
console.log("Map lookup:", findPairWithMap(sampleNumbers, sampleTarget));

Why reuse the sample variables?

Calling findPairWithMap with sampleNumbers plus sampleTarget holds the input constant. Only the search strategy changes.

  • Press Ctrl+S to save app.js.
  • Predict whether both functions return the same indices.
  • Compare the small-input results by running this command:
node app.js

What should I see?

The Brute force: line reports indices [1, 2]. The Map lookup: line reports the same indices.

The Map result has a lookups value of 3. It found the complement after processing three numbers.

Results do not match?

  • Confirm the Map output line sits below the complete brute-force console call.
  • Check that the function call uses findPairWithMap(sampleNumbers, sampleTarget) exactly.
  • Still stuck? Help me compare the pair finder results.

That agreement matters. You changed the search strategy without changing the correct answer.

Re-run the stress input

The no-solution input forces the nested loops to inspect every possible pair. The Map version performs one complement lookup for each input value.

  • In app.js, find this stress-test block:
const stressTarget = -1;
const bruteStress = findPairBruteForce(stressNumbers, stressTarget);

console.log("\n1,000-item no-solution input");
console.log("Brute-force comparisons:", bruteStress.comparisons);

What are you changing?

The current block measures only bruteStress. The replacement runs the Map function against the same stress input.

  • Replace that block with this expanded comparison:
const stressTarget = -1;
const bruteStress = findPairBruteForce(stressNumbers, stressTarget);
const mapStress = findPairWithMap(stressNumbers, stressTarget);

console.log("\n1,000-item no-solution input");
console.log("Brute-force comparisons:", bruteStress.comparisons);
console.log("Map lookups:", mapStress.lookups);

How does this comparison work?

The mapStress result uses the same 1,000 numbers as bruteStress. The final log line exposes its lookup count beside the brute-force comparison count.

  • Press Ctrl+S to save app.js.
  • Predict which operation count will be larger.
  • Run the completed comparison by running this command:
node app.js

What should I see?

The stress section reports 499500 brute-force comparisons. The Map line reports 1000 lookups.

Both functions correctly report no matching pair. Their operation counts reveal how differently they search.

  • State this invariant aloud: before each iteration, seenIndices contains only values from earlier indices.

Missing the Map lookup count?

  • Confirm mapStress is declared before the console line that reads its lookups value.
  • Confirm stressTarget still equals -1.
  • Still stuck? Help me debug the stress comparison.

You have turned the bottleneck into a measurable contrast. The optimized search reaches the same conclusions with far fewer operations.

✔️ Awesome, I've got everything!

Great. Double check that you saved app.js after the final stress comparison.

ⓧ I'd like to double check the full code

  • Compare your saved app.js with this complete version:
function findPairBruteForce(numbers, target) {
  let comparisons = 0;

  for (let left = 0; left < numbers.length; left += 1) {
    for (let right = left + 1; right < numbers.length; right += 1) {
      comparisons += 1;

      if (numbers[left] + numbers[right] === target) {
        return { indices: [left, right], comparisons };
      }
    }
  }

  return { indices: null, comparisons };
}

function findPairWithMap(numbers, target) {
  const seenIndices = new Map();
  let lookups = 0;

  for (let index = 0; index < numbers.length; index += 1) {
    const currentNumber = numbers[index];
    const complement = target - currentNumber;
    lookups += 1;

    if (seenIndices.has(complement)) {
      return {
        indices: [seenIndices.get(complement), index],
        lookups,
      };
    }

    seenIndices.set(currentNumber, index);
  }

  return { indices: null, lookups };
}

const sampleNumbers = [11, 7, 2, 15];
const sampleTarget = 9;

console.log("\nSmall input");
console.log(
  "Brute force:",
  findPairBruteForce(sampleNumbers, sampleTarget),
);
console.log("Map lookup:", findPairWithMap(sampleNumbers, sampleTarget));

const stressNumbers = [];
for (let value = 1; value <= 1000; value += 1) {
  stressNumbers.push(value);
}

const stressTarget = -1;
const bruteStress = findPairBruteForce(stressNumbers, stressTarget);
const mapStress = findPairWithMap(stressNumbers, stressTarget);

console.log("\n1,000-item no-solution input");
console.log("Brute-force comparisons:", bruteStress.comparisons);
console.log("Map lookups:", mapStress.lookups);

What should match?

This reference keeps the brute-force function first. The Map function follows it before the shared sample inputs.

The final stress block calls both functions with identical inputs. It prints one operation count from each result.

Your pair finder now proves why remembering earlier values beats scanning every pair. Next up, you'll test edge cases before shaping an interview explanation.

Test and Explain the Solution

Your brute-force finder now agrees with the Map-based finder on the sample. A fast answer still falls short in an interview when an edge case breaks it.

This step turns both implementations into a repeatable test target across five edge cases. Each test checks the same expected indices against both approaches.

You will also create a concise interview explanation. It gives you a one-minute answer you can rehearse without reading the implementation.

In this step, get ready to:
  • Prove both pair finders handle five edge cases.
  • Write an interview-ready explanation of the solution.
  • Run the completed lab without any failed checks.
Add repeatable edge-case checks

Both implementations return an indices array or null. A shared comparison helper lets every test judge those two result shapes consistently.

  • In app.js, find const sampleNumbers = [11, 7, 2, 15].
  • Add the pair comparison helper immediately above that line by copying this code:
function samePair(actual, expected) {
  if (actual === null || expected === null) {
    return actual === expected;
  }

  return actual[0] === expected[0] && actual[1] === expected[1];
}

How does the comparison helper work?

  • The null check confirms that both values represent the same no-solution result.
  • The final return checks the left index against the expected left index.
  • It also checks the right index against the expected right index.
  • Save app.js by pressing Ctrl+S.
  • Confirm the helper parses successfully by running this command:
node app.js

What should I see?

You should see the small-input result followed by the stress comparison. This confirms Node.js can parse the new helper.

Seeing a syntax error?

  • Check that samePair appears after the closing brace of findPairWithMap.
  • Check that the helper has both closing braces shown above.
  • Still stuck? Help me fix the samePair helper in app.js.

The next helper gives every case one testing path. It runs the same input through both implementations before deciding whether the case passed.

  • In app.js, find the closing brace of samePair.
  • Add the test runner immediately below that brace by copying this code:
function runTest(name, numbers, target, expectedIndices) {
  const bruteResult = findPairBruteForce(numbers, target);
  const mapResult = findPairWithMap(numbers, target);
  const passed =
    samePair(bruteResult.indices, expectedIndices) &&
    samePair(mapResult.indices, expectedIndices);

  console.log(`${passed ? "PASS" : "FAIL"}: ${name}`);

  if (!passed) {
    throw new Error(`${name} failed`);
  }
}

What does the test runner prove?

  • The runner sends one input to findPairBruteForce.
  • It sends the same input to findPairWithMap.
  • The passed value becomes true only when both results match expectedIndices.
  • A failed case throws an error so a wrong result cannot hide among successful output.
  • Save app.js by pressing Ctrl+S.
  • Confirm the test runner parses successfully by running this command:
node app.js

What should I see?

You should see the small-input result followed by both stress measurements. Reaching the final measurement confirms the new test runner loaded without a runtime error.

Does the script stop early?

  • Check that runTest appears outside samePair.
  • Check the spelling of bruteResult.
  • Check the spelling of mapResult.
  • Still stuck? Help me debug the runTest helper.

A useful edge-case suite targets different assumptions. These five cases cover an ordinary match, repeated values, negative values, a missing result, and an empty input.

  • Scroll to the bottom of app.js.
  • Add the five edge-case checks below the existing stress output by copying this code:
console.log("\nEdge-case tests");
runTest("finds a normal pair", [11, 7, 2, 15], 9, [1, 2]);
runTest("handles duplicate values", [3, 3], 6, [0, 1]);
runTest("handles negative values", [-4, 3, 7], 3, [0, 2]);
runTest("returns null when no pair exists", [1, 2, 3], 50, null);
runTest("handles empty input", [], 10, null);

What do these cases cover?

  • The normal case confirms both functions still return [1, 2] for the original sample.
  • The duplicate case proves two equal values at distinct indices can form a pair.
  • The negative case proves the complement calculation works below zero.
  • The final cases prove both implementations return null when no valid indices exist.
  • Save app.js by pressing Ctrl+S.

Before you run the tests, do you expect every implementation to agree with all five expected results?

  • Run the edge-case suite by running this command:
node app.js

What should I see?

After Edge-case tests, you should see five lines beginning with PASS. The command should finish without throwing an error.

Seeing a failed edge case?

  • Check that every runTest call matches the input values shown above.
  • Check that the normal case expects [1, 2].
  • Check that the no-solution cases expect null.
  • Still stuck? Help me diagnose the failed pair-finder test.

✔️ Awesome, I've got everything!

Great. Your app.js file now tests both implementations across five edge cases.

ⓧ I'd like to double check the full code

  • Compare your saved app.js with this complete version:
function findPairBruteForce(numbers, target) {
  let comparisons = 0;

  for (let left = 0; left < numbers.length; left += 1) {
    for (let right = left + 1; right < numbers.length; right += 1) {
      comparisons += 1;

      if (numbers[left] + numbers[right] === target) {
        return { indices: [left, right], comparisons };
      }
    }
  }

  return { indices: null, comparisons };
}

function findPairWithMap(numbers, target) {
  const seenIndices = new Map();
  let lookups = 0;

  for (let index = 0; index < numbers.length; index += 1) {
    const currentNumber = numbers[index];
    const complement = target - currentNumber;
    lookups += 1;

    if (seenIndices.has(complement)) {
      return {
        indices: [seenIndices.get(complement), index],
        lookups,
      };
    }

    seenIndices.set(currentNumber, index);
  }

  return { indices: null, lookups };
}

function samePair(actual, expected) {
  if (actual === null || expected === null) {
    return actual === expected;
  }

  return actual[0] === expected[0] && actual[1] === expected[1];
}

function runTest(name, numbers, target, expectedIndices) {
  const bruteResult = findPairBruteForce(numbers, target);
  const mapResult = findPairWithMap(numbers, target);
  const passed =
    samePair(bruteResult.indices, expectedIndices) &&
    samePair(mapResult.indices, expectedIndices);

  console.log(`${passed ? "PASS" : "FAIL"}: ${name}`);

  if (!passed) {
    throw new Error(`${name} failed`);
  }
}

const sampleNumbers = [11, 7, 2, 15];
const sampleTarget = 9;

console.log("\nSmall input");
console.log(
  "Brute force:",
  findPairBruteForce(sampleNumbers, sampleTarget),
);
console.log("Map lookup:", findPairWithMap(sampleNumbers, sampleTarget));

const stressNumbers = [];
for (let value = 1; value <= 1000; value += 1) {
  stressNumbers.push(value);
}

const stressTarget = -1;
const bruteStress = findPairBruteForce(stressNumbers, stressTarget);
const mapStress = findPairWithMap(stressNumbers, stressTarget);

console.log("\n1,000-item no-solution input");
console.log("Brute-force comparisons:", bruteStress.comparisons);
console.log("Map lookups:", mapStress.lookups);

console.log("\nEdge-case tests");
runTest("finds a normal pair", [11, 7, 2, 15], 9, [1, 2]);
runTest("handles duplicate values", [3, 3], 6, [0, 1]);
runTest("handles negative values", [-4, 3, 7], 3, [0, 2]);
runTest("returns null when no pair exists", [1, 2, 3], 50, null);
runTest("handles empty input", [], 10, null);
Create the interview explanation

Passing tests proves the code behaves correctly on the cases you chose. An interview-ready explanation also needs the contract, invariant, complexity assumptions, and trade-off.

A Markdown file keeps that reasoning beside the implementation. You can use it as a rehearsal guide without reading through the code.

  • In the Explorer view of Visual Studio Code, select the interview-pattern-lab folder.
  • Use the file creation icon at the top of the Explorer view.
  • Enter INTERVIEW_NOTES.md as the filename.
  • Add the interview notes by copying this content into the new file:
# Pair Finder Interview Notes

## Clarify the contract

- Input: an array of numbers and a target number.
- Output: two distinct indices whose values add to the target, or `null`.
- Clarify whether one answer or every answer is required.

## Brute-force approach

Try every index pair with nested loops. This is simple and uses constant extra space, but its time cost grows quadratically: O(n²).

## Map-based approach

Scan from left to right. For each number, compute the complement needed to reach the target. If the complement is already in the Map, return its earlier index and the current index. Otherwise, store the current number and index.

Invariant: before processing index `i`, the Map contains only values from indices smaller than `i`. This proves the algorithm never reuses the same array element.

Under the usual average constant-time hash lookup assumption, this is expected O(n) time and O(n) extra space. JavaScript specifies sublinear average access for `Map`, rather than requiring one internal representation, so the linear result is an interview-model expectation rather than a language-level worst-case guarantee.

## Edge cases tested

- Duplicate values, such as `[3, 3]` with target `6`
- Negative values
- No matching pair
- Empty input

## One-minute explanation

I would start with two nested loops because that makes correctness obvious, but it checks up to every pair and is O(n²). We can trade space for fewer searches. While scanning once, I keep a Map from each earlier value to its index. For the current value, I calculate the complement and check whether it is already present. I check before inserting, so one element cannot pair with itself, while duplicate values at different indices still work. Under the standard hash-map assumption, the expected time is O(n) and the extra space is O(n).

What belongs in the explanation?

  • The contract defines what the function receives and what it returns.
  • The direct approach establishes a correct baseline with O(n²) time.
  • The invariant explains why seenIndices contains only earlier values.
  • The optimized analysis states expected O(n) time under the usual hash lookup assumption.
  • The spoken answer presents the direct solution before introducing the space-for-time trade-off.
  • Save INTERVIEW_NOTES.md by pressing Ctrl+S.
  • Confirm INTERVIEW_NOTES.md appears in the Explorer view.
  • Confirm the editor shows Pair Finder Interview Notes as the first heading.

Notes file missing or empty?

  • Check that INTERVIEW_NOTES.md sits inside the interview-pattern-lab folder.
  • Check that the filename ends with .md.
  • Press Ctrl+S again after pasting the notes.
  • Still stuck? Help me create the interview notes file.

✔️ Awesome, I've got everything!

Your notes now cover the contract, both approaches, the invariant, the complexity assumptions, the tested edge cases, and a one-minute answer.

ⓧ I'd like to double check the full code

  • Compare your saved INTERVIEW_NOTES.md with this complete version:
# Pair Finder Interview Notes

## Clarify the contract

- Input: an array of numbers and a target number.
- Output: two distinct indices whose values add to the target, or `null`.
- Clarify whether one answer or every answer is required.

## Brute-force approach

Try every index pair with nested loops. This is simple and uses constant extra space, but its time cost grows quadratically: O(n²).

## Map-based approach

Scan from left to right. For each number, compute the complement needed to reach the target. If the complement is already in the Map, return its earlier index and the current index. Otherwise, store the current number and index.

Invariant: before processing index `i`, the Map contains only values from indices smaller than `i`. This proves the algorithm never reuses the same array element.

Under the usual average constant-time hash lookup assumption, this is expected O(n) time and O(n) extra space. JavaScript specifies sublinear average access for `Map`, rather than requiring one internal representation, so the linear result is an interview-model expectation rather than a language-level worst-case guarantee.

## Edge cases tested

- Duplicate values, such as `[3, 3]` with target `6`
- Negative values
- No matching pair
- Empty input

## One-minute explanation

I would start with two nested loops because that makes correctness obvious, but it checks up to every pair and is O(n²). We can trade space for fewer searches. While scanning once, I keep a Map from each earlier value to its index. For the current value, I calculate the complement and check whether it is already present. I check before inserting, so one element cannot pair with itself, while duplicate values at different indices still work. Under the standard hash-map assumption, the expected time is O(n) and the extra space is O(n).
Run and rehearse the final lab

The final run checks the complete executable before you practice the explanation. A clean result gives you evidence that the claims in your notes match the behavior of both implementations.

  • Switch to the app.js editor tab.
  • Save app.js by pressing Ctrl+S.
  • Switch to the INTERVIEW_NOTES.md editor tab.
  • Save INTERVIEW_NOTES.md by pressing Ctrl+S.

Before you run the completed lab, how many PASS lines do you expect if every edge case agrees across both implementations?

  • Run the completed lab in the integrated terminal by running this command:
node app.js

What should I see?

You should see both result objects for the small input. You should also see the brute-force comparison count beside the Map lookup count for the stress input.

The final section should print five PASS lines. The process should finish without throwing an error.

Missing a PASS line?

  • Count the runTest calls at the bottom of app.js.
  • Confirm each call appears below console.log("\nEdge-case tests").
  • Read the first failed test name to identify the input that needs attention.
  • Still stuck? Help me get all five pair-finder tests passing.
  • Switch back to INTERVIEW_NOTES.md.
  • Explain the optimized approach aloud in roughly one minute.
  • Use the notes only when you need a prompt.

That is the interview loop complete. Your lab now proves correctness across five edge cases and gives you a concise explanation of the optimization.

Secret mission

Find Every Unique Pair

Extend your pair finder so it returns every unique value pair for a target. Use frequencies to handle duplicate values correctly. Prove that the expected two pairs appear once each.

Clean Up Your Resources

Clean Up Your Resources

Your interview lab runs only on your local machine. It has no ongoing cloud costs.

Choose the cleanup option that fits what you want to do next.

Resources you used:

  • The interview-pattern-lab directory.

Keep everything running

No action needed. Choose this if you want to keep practising the pair-finder pattern.

  • Keep the interview-pattern-lab directory on your machine.
  • Use INTERVIEW_NOTES.md to rehearse your interview explanation.
  • Return to app.js when you want to test another input.

Pause - I'll come back to this later

Pause this local project by closing its editing session. Every project file stays on disk.

  • Confirm that the integrated terminal has returned to a prompt.
  • Close Visual Studio Code.
  • Leave the interview-pattern-lab directory on your machine.

Delete - I don't want to use this again

Deleting the directory is permanent. The command below targets only interview-pattern-lab.

Visual Studio Code stays installed. Node.js stays installed.

  • Return to the integrated terminal from earlier.
  • Delete the interview-pattern-lab directory by running these commands:
cd ..
rm -rf interview-pattern-lab

What Do These Commands Do?

  • The first line moves the terminal to the directory above the workspace.
  • The second line removes the project directory with every file inside it.
  • Select Explorer in the left sidebar of Visual Studio Code.
  • Confirm that app.js is no longer listed.
  • Confirm that INTERVIEW_NOTES.md is no longer listed.

Still See the Project Files?

A remaining directory can mean the terminal started outside the workspace. It can also mean the directory name did not match.

Check that the command names interview-pattern-lab exactly.

Help me safely remove the remaining project directory.

That is the cleanup complete. Your installed tools remain ready for another local project.

Nice Work!

Nice Work!

Excellent work! You built a tested JavaScript pair finder that makes the cost of nested loops visible before demonstrating a more scalable complement lookup.

You've learned how to:

  • Build a brute-force pair finder that returns matching indices. You also measured its quadratic cost on a 1,000-item no-solution input.
  • Replace repeated pair scanning with a JavaScript Map. You used the complement invariant to prove that each match uses two distinct indices.
  • Test edge cases across both implementations. You also created an interview-ready explanation of correctness plus complexity trade-offs in INTERVIEW_NOTES.md.
  • Complete the optional Secret Mission by extending the lab to return every unique value pair while handling duplicates correctly.

Ready to quiz yourself?