Imagine you’re throwing a party but you’re broke. Snacks cost $50 and you need to find two guests who can cover it between them. You could grab the first person, drag them around the room, and ask every other guest “hey, do you two have $50 together?” Then do it again with the next person. And the next. You’d find your pair eventually but you’d totally annoy everyone and miss the big game.
But you’re not gonna do that. Your gut tells you to just write stuff down. So you grab a notebook. Walk up to each guest, ask what they can pitch in, and check your notes: “Has anyone I’ve already talked to got the rest of what I need to hit $50?” If yes, snack run. If no, jot down their amount and move on. One lap around the room, no repeated conversations, and you’re out the door.
That’s the notebook pattern. And it’s the same trick behind most efficient solutions to problems involving lookups and matching in programming.
To be clear, the notebook has to work as a lookup table for this to be effective. This allows you to instantly answer the question: “Have I seen this before?” That instant answer is what makes your code dramatically faster. (We’ll break down exactly how much faster in the Time Complexity section below.)
Code Example:
Question: From an array of numbers find 2 numbers that add up to a target. Or in the party example: Find 2 people who have a total of 50 dollars.
Array of People’s Money: [5, 17, 1, 33, 11, 15]
Total Cash Needed: 50
// 1. Grab your notebook (a Set, or Map)
// 2. Ask a person how much they have (they have 5)
// 3. Calculate what you need (50 - 5 = 45)
// 4. Read your notebook (is 45 in there? Nope)
// 5. Write down what they have in your notebook (write 5)
// 6. Move to the next person at the party
// 7. Repeat step 2-5 (they have 17, need 33, not in notebook, write 17)
// 8. Repeat step 2-5 (they have 1, need 49, not in notebook, write 1)
// 9. Repeat step 2-5 (they have 33, need 17, yes in notebook! Return [17, 33])
var peeps = [5, 17, 1, 33, 11, 15];
var target = 50;
function findMoney(arr, target) {
const notebook = new Set();
for (const n of arr) {
const need = target - n;
if (notebook.has(need)) {
return [need, n];
}
notebook.add(n);
}
}
The notebook pattern is a powerful way to solve problems efficiently by keeping track of what you’ve encountered and what you need to find. It allows you to avoid unnecessary loops and checks, making your code cleaner and faster.
The nested loop approach (brute force as we like to call it) would be like asking the first person at the party to go around the room and ask everyone else how much they have and come report back to you each time. And doing that for every person you meet. The notebook pattern is much more efficient because you only ask each person once yourself, and keep track of what you’ve learned in your notebook.
Tracking Positions with a Map
If you also need to know the location of the people at the party, all you need to change is to store both their location and how much they have in your notebook. The only thing that changes is that now you’ll use a Map instead of a Set. So your seen.add(num) becomes seen.set(num, index). When done you get the value using [seen.get(need), index].
function findMoney(arr, target) {
const notebook = new Map();
for (const [index, n] of arr.entries()) {
const need = target - n;
if (notebook.has(need)) {
return [
[need, notebook.get(need)],
[n, index],
];
}
notebook.set(n, index);
}
}
Time Complexity using a Counter
Going through the party only once is nice because not only do you not annoy people by asking them the same question multiple times, you also save time. This means you will be able to make your snack run and be back just in time for the big game.
If you have to make only 1 round of Q&A with your notebook, we say the time complexity is O(n) because you at most have to only bother each person at the party once.
If you did this a bit more naively and had multiple people asking each other how much they have, this would be O(n^2) because you have to ask each person about every other person.
We can update our example to keep a counter to track this easily.
Brute Force (O(n^2))
let counter = 0;
function findMoney(arr, target) {
for (let i = 0; i < arr.length; i++) {
counter++;
for (let j = i + 1; j < arr.length; j++) {
counter++;
if (arr[i] + arr[j] === target) {
return [arr[i], arr[j]];
}
}
}
}
findMoney(peeps, target);
console.log(counter); // 9
Notebook Pattern (O(n))
let counter = 0;
function findMoney(arr, target) {
const notebook = new Set();
for (const n of arr) {
counter++;
const need = target - n;
if (notebook.has(need)) {
return [need, n];
}
notebook.add(n);
}
}
findMoney(peeps, target);
console.log(counter); // 4
We end up with a much more efficient solution that only requires 4 checks instead of 9.
Patterns for using the notebook pattern
Anytime you are looking for “a pair”, “a match”, or “a pattern” in a collection, try the notebook pattern and see if it can help you find a more efficient solution.
Here are 10 patterns that often use the notebook pattern along with simple analogies to help you understand the problem and how the notebook pattern applies:
1. Two Sum
This is the example we just went through with the party analogy. It’s such a common problem that it has its own name. The notebook pattern is the go-to solution for a Two Sum problem.
2. First Duplicate
The Analogy: You’re a bouncer scanning tickets at the entrance. People walk in one at a time and you scan their tickets. If someone tries to enter with a ticket you’ve already seen, you know it’s a duplicate.
Given an array, return true if any value appears at least twice.
What goes in the notebook: Each value as you encounter it → Set
What you check for: “Have I already let this one in?“
function containsDuplicate(arr) {
const notebook = new Set();
for (const n of arr) {
if (notebook.has(n)) return true;
notebook.add(n);
}
return false;
}
3. Intersection of Two Arrays
The Analogy: Two people each have a bag of Scrabble tiles. Dump the first bag into your notebook. Then check each tile from the second bag — if it’s in the notebook, it’s a match.
Given two arrays, find which values appear in both.
What goes in the notebook: Everything from array 1 → Set
What you check for: “Is this item from array 2 also in array 1?“
function intersection(arr1, arr2) {
const notebook = new Set(arr1);
return [...new Set(arr2.filter((n) => notebook.has(n)))];
}
4. Valid Anagram
The Analogy: You bought groceries for a recipe. You check the receipt against your shopping list and cross off each item as you pull it out of the bag. If everything crosses off perfectly with nothing left over on either side, you bought exactly what you needed.
Given two strings, determine if they’re anagrams of each other.
What goes in the notebook: Character frequencies from string 1 → Map
What you check for: Does string 2 balance out every count to zero?
function isAnagram(s, t) {
if (s.length !== t.length) return false;
const notebook = {};
for (const c of s) notebook[c] = (notebook[c] || 0) + 1;
for (const c of t) {
if (!notebook[c]) return false;
notebook[c]--;
}
return true;
}
5. Most Frequent Element
The Analogy: You’re a bartender trying to figure out the most popular drink of the night. Every time someone orders, you make a tally mark next to that drink on a napkin. At closing time, you look down and whatever has the most marks is your best seller.
Given an array, find the element that appears most often.
What goes in the notebook: Each value → its count → Map
What you check for: The highest count at the end.
function mostFrequent(arr) {
const notebook = {};
let maxCount = 0;
let winner = null;
for (const n of arr) {
notebook[n] = (notebook[n] || 0) + 1;
if (notebook[n] > maxCount) {
maxCount = notebook[n];
winner = n;
}
}
return winner;
}
6. Longest Substring Without Repeating Characters
The Analogy: You’re running a conga line at a costume party and the rule is: no duplicate costumes in the line. When someone joins the back wearing a costume that’s already in the line, you can’t just pull that one person out of the middle — it would break the chain. So you cut everyone off the front up to and including the matching costume, then add the new person to the back. Your record is the longest the conga line ever got.
Given a string, find the length of the longest substring with no repeated characters.
What goes in the notebook: Characters in the current window → Set
What you check for: “Is this character already in my current run?“
function longestUnique(str) {
const notebook = new Set();
let left = 0;
let max = 0;
for (let right = 0; right < str.length; right++) {
while (notebook.has(str[right])) {
notebook.delete(str[left]);
left++;
}
notebook.add(str[right]);
max = Math.max(max, right - left + 1);
}
return max;
}
7. Group Anagrams
The Analogy: Your kid built a few different LEGO structures. You need to figure out which ones used the exact same pieces. So you break each one apart, sort the pieces by size and color, and line them up. Any two that produce the same lineup of pieces go in the same bin since they’re just rearrangements of each other.
Given an array of words, group words that are anagrams of each other.
What goes in the notebook: Sorted letters → list of original words → Map
What you check for: Which bin does this word belong in?
function groupAnagrams(words) {
const notebook = new Map();
for (const word of words) {
const key = word.split("").sort().join("");
if (!notebook.has(key)) notebook.set(key, []);
notebook.get(key).push(word);
}
return [...notebook.values()];
}
// ["eat","tea","tan","ate","nat","bat"]
// → [["eat","tea","ate"], ["tan","nat"], ["bat"]]
8. Subarray Sum Equals K
The Analogy: You’re on a road trip and you want to know how many stretches of your drive were exactly 50 miles long. You jot down your odometer reading at every town. At each stop, you check your notebook: “Is there a previous reading that’s exactly 50 less than where I am now?” If yes, the road between those two towns is exactly 50 miles. The notebook remembers every previous reading so you can check instantly.
Given an array and integer k, find how many continuous subarrays sum to k.
What goes in the notebook: Running sum → how many times you’ve hit that sum → Map
What you check for: “Have I seen a running sum that is exactly current - k?“
function subarraySum(arr, k) {
const notebook = new Map([[0, 1]]); // sum of 0 seen once (empty prefix)
let sum = 0;
let count = 0;
for (const n of arr) {
sum += n;
if (notebook.has(sum - k)) count += notebook.get(sum - k);
notebook.set(sum, (notebook.get(sum) || 0) + 1);
}
return count;
}
9. Longest Consecutive Sequence
The Analogy: Someone shuffles a deck of cards and tosses them face up on a table. You want to find the longest run of consecutive cards like 5, 6, 7, 8, 9. You don’t sort them. Instead, you scan for cards that start a run (there’s no card with the number right before it on the table). When you find one, you count forward: “5… is there a 6? Yes. A 7? Yes. An 8? Yes. A 9? Yes. That’s a run of 5.” Longest run wins.
Given an unsorted array, find the length of the longest consecutive sequence (e.g., [1,2,3,4]).
What goes in the notebook: All numbers → Set
What you check for: “Does n - 1 exist?” If no, start counting n+1, n+2...
function longestConsecutive(arr) {
const notebook = new Set(arr);
let max = 0;
for (const n of notebook) {
if (!notebook.has(n - 1)) {
// only start from chain beginnings
let length = 1;
let current = n;
while (notebook.has(current + 1)) {
current++;
length++;
}
max = Math.max(max, length);
}
}
return max;
}
// [100, 4, 200, 1, 3, 2] → 4 (the sequence 1,2,3,4)
10. Happy Number
The Analogy: You’re driving through a small town following road signs looking for Main Street. Each sign points you to a new intersection. You jot down every intersection you pass through. If you eventually arrive at Main Street, you made it! But if you pull up to an intersection that’s already in your notebook, you’re driving in circles and you’ll never get there.
Determine if a number eventually reaches 1 when you repeatedly sum the squares of its digits. If it loops forever, it’s not happy.
What goes in the notebook: Every number you’ve visited → Set
What you check for: “Have I been here before?” (cycle detection)
function isHappy(n) {
const notebook = new Set();
while (n !== 1) {
if (notebook.has(n)) return false; // going in circles
notebook.add(n);
n = String(n)
.split("")
.reduce((sum, d) => sum + d * d, 0);
}
return true;
}
// 19 → 82 → 68 → 100 → 1 ✓ happy!
// 2 → 4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4 ✗ cycle!
Which Notebook? Object, Map, or Set
As a side note, you may have noticed that I used object in some of the examples above, as opposed to map or set. This is because there are different types of notebooks you can use, and each has its own strengths and weaknesses. Here’s a quick rundown:
Set — A guest list. “Are you on the list?” Yes or no. No extra info.
Use when you only care about existence.
Map — A guest list with notes. “Are you on the list? And what table are you at?”
Use when you need to store a value alongside each key.
{} (plain object) — Works like a Map but every key becomes a string.
obj[5] and obj["5"] are the same key. Quick to type, fine for characters
and simple string keys. Not fine when the difference between 5 and "5" matters.
Why use {} over Map? Less ceremony. obj[key] = val vs map.set(key, val).
If you don’t need .size, iteration, or non-string keys, it’s just fewer characters.
Rule of thumb: Use
Setfor “have I seen this?”,Mapfor “what do I know about this?”, and{}only when your keys are already strings and you want shorter syntax.
Universal Summary of the Notebook Pattern
Every single one of these follows the same core:
- Walk through the data once
- At each step, check the notebook — is what I need already there?
- Update the notebook — record what I now know
The notebook replaces the inner loop so your time complexity drops to O(n) instead of O(n²). The only thing that changes per problem is what you store and what you check for.
The pattern always comes down to three things: what you’ve seen, what you need, and what you have. At each step:
- Calculate what I need (target - current)
- Check if what I need is in the notebook (have I seen it?)
- If not, write down what I have (so a future person can find me)
The loop: Need → Seen? → Have
The notebook contains what people HAVE at the party. The question you ask is what you NEED to meet your goal of buying snacks for everyone. The magic is that those two things meet in the middle.
This pattern holds for almost every Map/Set problem. You’re always storing facts about what you’ve already passed, and checking if the current element completes something with what’s stored. The specific “fact” changes per problem:
- Two Sum: store the number, check for the complement
- Contains Duplicate: store the number, check if it’s already there
- Anagram: store character counts, check if they balance out
The core is always: look backward without looping backward by using your notebook.