Data for Everyone
How to solve a problem at an interview

Interviewers rarely judge only whether you reach the answer. They watch how you think: whether you clarify the task, start from something that works, and improve it deliberately. Silence for ten minutes followed by perfect code scores worse than reasoning aloud and arriving at a good solution. Six steps make that process visible.

  1. ClarifyRestate the task in your own words. Ask about the edges: can the array be empty? Are there negatives or duplicates? Is the input sorted? Each answer can change the solution.
  2. Work an example by handTake a small input and solve it on paper. This is where you notice the pattern — and catch a misunderstanding before writing a line of code.
  3. Say the brute forceDescribe the obvious solution and its complexity, even if it is slow. It proves you understand the task and gives a baseline to improve on.
  4. Find the bottleneckAsk what work is repeated. Usually it is a nested loop re-scanning data you have already seen — the classic place where a dictionary, two pointers or a running sum helps.
  5. Write the codeClear names, small steps, comments where intent is not obvious. Readable code is part of the grade, not decoration.
  6. Test it yourselfWalk through your example line by line, then the edges: empty input, one element, all equal values. Finding your own bug is a plus; the interviewer finding it is a minus.
The most common mistake is jumping straight to code. Two minutes spent clarifying and working an example save twenty minutes of rewriting a solution to the wrong problem.
How to come up with a solution: the method

The section above is about how to behave in the interview; this one is about where the idea itself comes from. Most easy problems are not hard because of the code — they are hard because it is not clear where to start. This section gives you a start that always works: solve a tiny example by hand, notice what you had to keep in your head, and turn that into variables. After that the code almost writes itself. Below are the method, a catalogue of the six kinds of “memory” that cover nearly every easy problem, and six families taken apart slowly — including run-length encoding, counting characters and the missing number. The explanations are written so that they make sense even if you skip the code.

Six questions, always in the same order
  1. What goes in, what comes out? Write two small examples with their answers, plus one edge case — usually the empty input. If you cannot write the answer for an example, you do not understand the task yet: ask.
  2. Solve a tiny example by hand — slowly. Do it the way you would on paper and say out loud what you are doing: “I look at the letter, it is the same as before, so I add one…”. That sentence is the algorithm.
  3. What did you have to keep in your head? A count, the previous letter, the names you had already seen, the best value so far. Whatever you were holding becomes the variables you create before the loop. This is the most important question on the page.
  4. What did you do with each new element? Usually one decision: “same as before → add one, otherwise → write it down and start again”. Say it as one sentence with “if … otherwise …”. That sentence becomes the body of the loop.
  5. What is left at the end? When the data runs out, is the answer ready, or is something still “in your hands” — an unfinished run, a table you still have to read? And what happens on the edges: empty input, one element, everything equal?
  6. Check with a trace. Walk through your code on the example in a small table: one row per element, one column per variable. The row that surprises you is the bug.
Every solution below has the same skeleton

Questions 3, 4 and 5 map onto three parts of the code, and on this page they are coloured. state — what you kept in your head, created before the loop. step — what happens to each element. end — how the state becomes the answer. edge — an edge case handled on purpose. Once you see the parts, every solution reads the same way.

def solve(data):
    memory = ...                 # STATE: what you kept in your head
    for x in data:
        ...                      # STEP: what you did with each new element
    return ...                   # END: how the memory becomes the answer
Six kinds of memory

Ask question 3 and you will almost always get one of the answers below. Each one is a family of problems with its own skeleton. Learn the six, and most easy problems turn into recognising which one you are looking at.

In your head, solving by handIn the codeWords in the taskFamily
how many of each I have metcounts = {}“how many times”, “most frequent”, “anagram”1 · Frequencies
which run I am in and how long it iscurrent, count“in a row”, “consecutive”, “compress”, “streak”2 · Runs in a row
whom I have already metseen = set()“duplicate”, “unique, keep order”, “first repeated”, “pair with a sum”3 · Seen before?
the full counts — but the answer follows the original ordercounts + 2nd loop“first unique”, “exactly once”, “share of the total”4 · Two passes
the best value, or the total, so farbest, total“largest”, “sum”, “best profit”, “longest”5 · Running result
how much there should be and how much there isexpected - actual“missing”, “lost”, “appears twice”, “numbers 1 to n”6 · Expected vs actual
two places in the data at onceleft, right“palindrome”, “sorted + pair”, “in place”Two pointers
the data laid out in ordersorted(...)“median”, “closest”, “largest three”Sorting first
1 · Frequencies: “how many of each”
The task

Given a string, count how many times each character appears in it and return the result as one string: the count followed by the character, for every distinct character, in the order it first appears.

  • The count is the total for the whole string: in abab both a’s are counted together → 2a2b.
  • An empty string gives an empty string.
Function: count_chars(s: str) -> str
Given
s = "banana"
Answer
count_chars("banana") → '1b3a2n'
Step 1 · Solve it by hand

Read the word letter by letter and keep a tally on paper: b — one mark; a — one mark; n — one mark; a again — a second mark next to a… In the word the a’s are not next to each other, but on paper all their marks are in one place. At the end you read the paper: b once, a three times, n twice.

Step 2 · What did you keep in your head? → the state
The paper with tallies is a dictionary: character → how many so far. Before the loop it is empty.
Step 3 · What did you do with each element? → the step
If……then
the letter is already on the paperadd a mark: its count + 1
the letter is newwrite it down with 0, then add a mark — counts.get(ch, 0) + 1 does both in one go
Step 4 · What is left at the end? → the end and the edges

When the word is over, the answer is already on the paper — just read it out in the required format. Nothing is left “in your hands”. The edge case takes care of itself: an empty string gives an empty table and an empty answer without any special code.

Step 5 · Now the code — the three parts are coloured
def count_chars(s):
    counts = {}                              # STATE: character -> how many so far
    for ch in s:
        counts[ch] = counts.get(ch, 0) + 1   # STEP: one more of this character
    return "".join(f"{n}{ch}" for ch, n in counts.items())   # END: read the table
# output when run:>>> count_chars("banana") '1b3a2n' >>> count_chars("abab") '2a2b' >>> count_chars("") ''
Step 6 · Check with a trace
#chcounts.get(ch, 0) + 1counts
1b0 + 1 = 1{'b': 1}
2a0 + 1 = 1{'b': 1, 'a': 1}
3n0 + 1 = 1{'b': 1, 'a': 1, 'n': 1}
4a1 + 1 = 2{'b': 1, 'a': 2, 'n': 1}
5n1 + 1 = 2{'b': 1, 'a': 2, 'n': 2}
6a2 + 1 = 3{'b': 1, 'a': 3, 'n': 2}
end··→ '1b3a2n' ✓

Rows 4 and 6: the second and third a are not neighbours of the first, yet the count simply grows. Neighbours never matter in this family — only the total for each character.

The typical mistake — and what it really prints
def count_chars_buggy(s):
    counts = {}
    for ch in s:
        counts[ch] += 1                      # the first time we meet a letter it has no entry yet
    return "".join(f"{n}{ch}" for ch, n in counts.items())
# output when run:>>> count_chars_buggy("banana") KeyError: 'b'

counts[ch] += 1 means “take the old count and add one” — but the very first b has no old count, so Python stops with an error. That is why the solution uses counts.get(ch, 0): “the old count, or 0 if there is none”.

One skeleton, many problems

Build the table once — t = tally(s), the same loop as above — and every question becomes a different last line. Only the END changes.

QuestionWhat changesReal result
Characters in alphabetical order"".join(f"{t[c]}{c}" for c in sorted(t))"".join(f"{t[c]}{c}" for c in sorted(t))
→ '3a1b2n'
The most frequent charactermax(t, key=t.get)max(t, key=t.get)
→ 'a'
Characters that repeat[c for c, n in t.items() if n > 1][c for c, n in t.items() if n > 1]
→ ['a', 'n']
Is “nabana” an anagram of it?tally("nabana") == ttally("nabana") == t
→ True
Can “ban” be built from its letters?all(t.get(c, 0) >= n for c, n in tally("ban").items())all(t.get(c, 0) >= n for c, n in tally("ban").items())
→ True
How to recognise itThe task asks “how many” and the order of neighbours does not matter. Test on abab: the answer must be 2a2b. If it should be 1a1b1a1b, you are in family 2.
Next: 2 · Runs in a row →
2 · Runs in a row: run-length encoding (RLE)
The task

Given a string, compress it: replace every run of identical characters standing next to each other with the run’s length followed by the character. The runs keep their order.

  • Only neighbours form a run: abab is four runs of length 1 → 1a1b1a1b.
  • A run of length 1 is written too, as 1b. An empty string gives an empty string.
Function: compress(s: str) -> str
Given
s = "aaabccdd"
Answer
compress("aaabccdd") → '3a1b2c2d'
Step 1 · Solve it by hand

Read left to right and keep a finger on the run you are in: a, a, a — still the same letter, three of them. Then b: the run of a’s is over, so you write “3a” and start counting b’s from one. The b ends at once (c is next): write “1b”. And so on. At the very end the run of d’s is in your hand, but no new letter comes to close it — you have to write “2d” yourself.

aaa3ab1bcc2cdd2d
Step 2 · What did you keep in your head? → the state
Two things in your hand: which letter the current run is and how long it is so far — current and count. Before the loop: the first letter and 1.
Step 3 · What did you do with each element? → the step
If……then
the letter equals the current onethe run continues: count + 1
the letter is differentthe run is over: write count + letter, then start a new run — current = this letter, count = 1
Step 4 · What is left at the end? → the end and the edges

The loop writes a run only when a different letter arrives. The last run never gets one, so it is written after the loop — the line most people forget. Edge case: an empty string has no first letter, so return "" before anything else.

Step 5 · Now the code — the three parts are coloured
def compress(s):
    if not s:                                # EDGE: nothing to compress
        return ""
    parts = []
    current, count = s[0], 1                 # STATE: the run I am inside and its length
    for ch in s[1:]:
        if ch == current:                    # STEP: the run continues
            count += 1
        else:                                # STEP: the run broke: write it, start anew
            parts.append(f"{count}{current}")
            current, count = ch, 1
    parts.append(f"{count}{current}")        # END: the last run is still open, write it
    return "".join(parts)
# output when run:>>> compress("aaabccdd") '3a1b2c2d' >>> compress("abab") '1a1b1a1b' >>> compress("a") '1a' >>> compress("") ''
Step 6 · Check with a trace
#chch == current?actionwritten
starta—current = 'a', count = 1''
2aTruecount = 2''
3aTruecount = 3''
4bFalsewrite "3a"; current = 'b', count = 1'3a'
5cFalsewrite "1b"; current = 'c', count = 1'3a1b'
6cTruecount = 2'3a1b'
7dFalsewrite "2c"; current = 'd', count = 1'3a1b2c'
8dTruecount = 2'3a1b2c'
end··write "2d"'3a1b2c2d' ✓

The last row is not a letter but the end of the data: the run “2d” is still open and is written only there.

The typical mistake — and what it really prints
def compress_buggy(s):
    if not s:
        return ""
    parts = []
    current, count = s[0], 1
    for ch in s[1:]:
        if ch == current:
            count += 1
        else:
            parts.append(f"{count}{current}")
            current, count = ch, 1
    return "".join(parts)                    # forgot: the last run is never written
# output when run:>>> compress_buggy("aaabccdd") '3a1b2c' >>> compress_buggy("a") ''

The “2d” is lost: the run was still open when the loop ended. A one-letter string loses everything. Always test a string that ends with a run of two or more letters, and a string of one letter.

Another way to think: find where each run starts and ends

Instead of reacting to each letter, you can say: “a run starts at i; move j forward while the letter stays the same; the run is s[i:j] and its length is j − i; the next run starts at j”. Two positions, and no special case at the end — each run is written as soon as its end is found. Both ways are correct; pick the one that matches how you think.

def compress_runs(s):
    parts = []
    i = 0                                    # STATE: where the current run starts
    while i < len(s):
        j = i
        while j < len(s) and s[j] == s[i]:   # STEP: walk to the end of the run
            j += 1
        parts.append(f"{j - i}{s[i]}")       # STEP: the run is s[i:j], its length is j - i
        i = j                                # STEP: the next run starts where this one ended
    return "".join(parts)                    # END: every run was written inside the loop
# output when run:>>> compress_runs("aaabccdd") '3a1b2c2d' >>> compress_runs("") ''
ijs[i:j]j − iwritten
03'aaa'3'3a'
34'b'1'3a1b'
46'cc'2'3a1b2c'
68'dd'2'3a1b2c2d'
One skeleton, many problems

The state — the current run and its length — stays the same. What changes is what you do when a run ends:

QuestionWhat changesReal result
Letter first: a3b1c2d2write f"{current}{count}" instead of f"{count}{current}"compress_letter_first("aaabccdd")
→ 'a3b1c2d2'
Skip the 1swrite the count only if it is above 1compress_skip_ones("aaabccdd")
→ '3ab2c2d'
Use it only if shorterat the end compare lengths and keep the shorter stringcompress_if_shorter("abcd")
→ 'abcd'
Length of the longest runinstead of writing a run, update a record — family 5longest_run("aaabccdd")
→ 3
Decode backthe state is the number read so far; a letter closes itdecompress("3a1b2c2d")
→ 'aaabccdd'

Look at the longest run: there is no line after the loop. The record is updated on every step, so when the data ends it is already complete. Whether you need an END line depends on one thing — is the answer finished after the last step, or is something still open?

def longest_run(s):
    best = count = 0                         # STATE: the current run and the record
    prev = None
    for ch in s:
        count = count + 1 if ch == prev else 1   # STEP: continue the run or start a new one
        best = max(best, count)              # STEP: update the record on every step
        prev = ch
    return best                              # END: the record is always up to date
# output when run:>>> longest_run("aaabccdd") 3 >>> longest_run("") 0

Decoding has its own trap: a count can have several digits. In 12z the number is twelve, not one and two. So the state is not one digit but all digits read so far, and only a letter closes the number.

def decompress(code):
    out = []
    digits = ""                              # STATE: the number read so far
    for ch in code:
        if ch.isdigit():
            digits += ch                     # STEP: the number is not finished yet
        else:
            out.append(ch * int(digits))     # STEP: a letter closes the number
            digits = ""
    return "".join(out)                      # END: a letter always closes a pair
# output when run:>>> decompress("3a1b2c2d") 'aaabccdd' >>> decompress("12z") 'zzzzzzzzzzzz'
How to recognise it“In a row”, “consecutive”, “compress”, “streak”. The test that separates it from family 1 is abab: here it gives four runs, 1a1b1a1b.
Next: 3 · Seen before? →
3 · “Have I seen it before?”
The task

Given a list, remove the repeated values and keep only the first occurrence of each. What remains must stay in the original order.

  • [3, 1, 3, 2, 1] → [3, 1, 2]: the second 3 and the second 1 are dropped.
  • Values can be numbers or strings; an empty list gives an empty list.
Function: dedup_keep_order(items: list) -> list
Given
items = [3, 1, 3, 2, 1]
Answer
dedup_keep_order([3, 1, 3, 2, 1]) → [3, 1, 2]
Step 1 · Solve it by hand

Imagine checking guests in at the door with a list. Each guest: is the name on my list already? No — let them in and add the name. Yes — they are already inside, skip them. The list at the door is not the answer itself: it exists only to answer “seen before?” quickly.

Step 2 · What did you keep in your head? → the state
A set seen — “who is already inside”. A set answers “is x here?” instantly, however large it is. If the answer must keep the order, add a second structure, the list result: the set checks, the list remembers the order.
Step 3 · What did you do with each element? → the step
If……then
x is not in seenfirst meeting: add it to seen and to result
x is in seena repeat: skip it — or, if the task is about repeats, this is the moment to answer
Step 4 · What is left at the end? → the end and the edges

result is the answer. In the “find a repeat” versions the answer often comes earlier: the moment you meet a repeat you can return. Edge case: an empty list gives an empty result, no special code needed.

Step 5 · Now the code — the three parts are coloured
def dedup_keep_order(items):
    seen = set()                             # STATE: what I have already taken
    result = []                              # STATE: the answer, in the original order
    for x in items:
        if x not in seen:                    # STEP: the first time I meet x
            seen.add(x)
            result.append(x)
    return result                            # END: the list is the answer
# output when run:>>> dedup_keep_order([3, 1, 3, 2, 1]) [3, 1, 2] >>> dedup_keep_order([]) []
Step 6 · Check with a trace
xx in seen?actionseenresult
3Falsetake ✓[3][3]
1Falsetake ✓[1, 3][3, 1]
3Trueskip[1, 3][3, 1]
2Falsetake ✓[1, 2, 3][3, 1, 2]
1Trueskip[1, 2, 3][3, 1, 2]
The typical mistake — and what it really prints
list(set([3, 1, 3, 2, 1]))
# output when run:>>> list(set([3, 1, 3, 2, 1])) [1, 2, 3]

The shortcut list(set(items)) removes repeats but loses the order: a set promises nothing about order. Here the result even happens to be sorted, which hides the bug on small numbers.

One skeleton, many problems

The set stays; what changes is how you react to a repeat:

QuestionWhat changesReal result
Is there any duplicate?on a repeat, return True at oncehas_duplicate([3, 1, 3, 2, 1])
→ True
The first value that repeatson a repeat, return that valuefirst_repeated([3, 1, 3, 2, 1])
→ 3
How many distinct values?the size of seen at the endlen(set([3, 1, 3, 2, 1]))
→ 3
Two numbers that add up to 5 (Two Sum)seen becomes a dict value → position; ask “is 5 − x in seen?”two_sum([3, 1, 3, 2, 1], 5)
→ [2, 3]
def first_repeated(items):
    seen = set()                             # STATE: what I have met so far
    for x in items:
        if x in seen:                        # STEP: met before, so this is the first repeat
            return x
        seen.add(x)
    return None                              # END: nothing repeats
# output when run:>>> first_repeated([3, 1, 3, 2, 1]) 3 >>> first_repeated([1, 2]) None
How to recognise it“Duplicate”, “unique”, “already”, “first repeated”, “pair with a sum”. The question to ask: what must I check about each element against the ones before it?
Next: 4 · Two passes →
4 · Two passes: collect first, then choose
Problems of this family on this page:First unique characterValues that appear exactly once
The task

Given a string, return the position (index from 0) of the first character that appears in the whole string exactly once. If there is no such character, return −1.

  • “Exactly once” is about the whole string, not about the part already read.
  • loveleetcode → 2 (the letter v); aabb → −1.
Function: first_uniq_char(s: str) -> int
Given
s = "loveleetcode"
Answer
first_uniq_char("loveleetcode") → 2
Step 1 · Solve it by hand

Try to answer while reading “loveleetcode”: is l unique? You cannot know yet — another l might come later (and it does, at position 4). By hand you would first count every letter, and only then go back to the beginning and look for the first letter whose count is 1. The answer depends on information that arrives after the element — that is the signal for two passes.

Step 2 · What did you keep in your head? → the state
Pass 1 builds the full table of counts — exactly family 1. Pass 2 walks the original data again, in its order, and asks the table.
Step 3 · What did you do with each element? → the step
If……then
pass 1, any letterits count + 1
pass 2, the count is 1this is the first unique letter: return its position
pass 2, the count is above 1keep walking
Step 4 · What is left at the end? → the end and the edges

If pass 2 ends without a hit, there is no unique letter: return −1. Two passes over n letters is still O(n) — reading the data twice is cheap; comparing every pair would not be.

Step 5 · Now the code — the three parts are coloured
def first_uniq_char(s):
    counts = {}                              # STATE: pass 1 builds the full table
    for ch in s:
        counts[ch] = counts.get(ch, 0) + 1
    for i, ch in enumerate(s):               # STEP: pass 2 walks again, in the original order
        if counts[ch] == 1:
            return i
    return -1                                # END: no unique character at all
# output when run:>>> first_uniq_char("loveleetcode") 2 >>> first_uniq_char("leetcode") 0 >>> first_uniq_char("aabb") -1
Step 6 · Check with a trace
ichcounts[ch]== 1?
0l2False
1o2False
2v1True → return 2

Pass 1 gave the table {'l': 2, 'o': 2, 'v': 1, 'e': 4, 't': 1, 'c': 1, 'd': 1}. The trace shows pass 2: it stops at the first letter whose count is 1.

The typical mistake — and what it really prints
def first_uniq_char_buggy(s):
    seen = set()
    for i, ch in enumerate(s):
        if ch not in seen:                   # "new so far" is not "unique": a repeat may come later
            return i
        seen.add(ch)
    return -1
# output when run:>>> first_uniq_char_buggy("loveleetcode") 0

“Not seen before” only means “new so far”. At position 0 the l is new — but it repeats at position 4, which a single pass has not reached yet.

When is one pass enough? When everything needed for the decision is known at the moment you meet the element. “First repeated value” — yes: the moment you see a value for the second time, you know (family 3). “First unique value” — no: uniqueness depends on the future. Ask yourself: at the moment I look at this element, do I already have everything to decide?
One skeleton, many problems

Pass 1 is always the same; pass 2 asks a different question:

QuestionWhat changesReal result
Values that appear exactly oncepass 2 keeps x when its count is 1only_unique([3, 1, 3, 2, 1])
→ [2]
Share of each valuethe total is known only after pass 1shares(["a", "b", "a", "a"])
→ {'a': 0.75, 'b': 0.25}
First repeated letter — one pass is enoughdecided at the moment of the repeat: family 3first_repeated("loveleetcode")
→ 'l'
How to recognise it“First unique”, “appears exactly once”, “share”, “above average” — anything where the decision about one element depends on the whole data.
Next: 5 · Running result →
5 · A running result: the total or the record so far
The task

Given a list of numbers, return the largest one without using max(). For an empty list return None.

  • The numbers can be negative: [-3, -1, -2] → -1.
Function: largest(nums: list) -> number | None
Given
nums = [4, 9, 2, 7]
Answer
largest([4, 9, 2, 7]) → 9
Step 1 · Solve it by hand

Look at 4, 9, 2, 7 one by one and hold the biggest so far: 4; then 9 beats it — now 9; 2 does not; 7 does not. At the end you are holding 9. You never needed to look back: the record already contains everything you need.

Step 2 · What did you keep in your head? → the state
The record best. Start it with a real element — the first one — not with 0: 0 is not in the data, and it beats every negative number.
Step 3 · What did you do with each element? → the step
If……then
x is bigger than the recorda new record: best = x
otherwisenothing changes
Step 4 · What is left at the end? → the end and the edges

The record is the answer the moment the data ends — there is nothing to flush. Edge case: an empty list has no largest value, so return None (agree on this with the interviewer).

Step 5 · Now the code — the three parts are coloured
def largest(nums):
    if not nums:                             # EDGE: an empty list has no largest value
        return None
    best = nums[0]                           # STATE: the record so far (start with a real value)
    for x in nums[1:]:
        if x > best:                         # STEP: a new record
            best = x
    return best                              # END: the record is the answer
# output when run:>>> largest([4, 9, 2, 7]) 9 >>> largest([-3, -1, -2]) -1 >>> largest([]) None
The typical mistake — and what it really prints
def largest_buggy(nums):
    best = 0                                 # looks harmless, but 0 is not one of the numbers
    for x in nums:
        if x > best:
            best = x
    return best
# output when run:>>> largest_buggy([-3, -1, -2]) 0

Starting with 0 looks harmless, but 0 is not one of the numbers. With only negative numbers nothing ever beats it, and the function returns a value that is not in the list.

Two records at once: the best time to buy and sell
The task

Given prices by day, return the largest profit from buying once and selling on a later day. If no deal makes money, return 0.

  • You must buy before you sell: [7, 6, 4, 3, 1] → 0.
Function: max_profit(prices: list) -> number
Given
prices = [7, 1, 5, 3, 6, 4]
Answer
max_profit([7, 1, 5, 3, 6, 4]) → 5

Prices by day: 7, 1, 5, 3, 6, 4. You buy once and sell later. Walk through the days and ask each day: “if I sold today, what is the best I could earn?”. To know that, you need the cheapest price before today — so you keep it in mind and lower it whenever a cheaper day comes. Two numbers in your head: the cheapest price so far and the best profit so far.

def max_profit(prices):
    cheapest = float("inf")                  # STATE: the lowest price seen so far
    best = 0                                 # STATE: the best profit so far
    for p in prices:
        cheapest = min(cheapest, p)          # STEP: could I have bought cheaper?
        best = max(best, p - cheapest)       # STEP: what if I sold today?
    return best                              # END: the record is the answer
# output when run:>>> max_profit([7, 1, 5, 3, 6, 4]) 5 >>> max_profit([7, 6, 4, 3, 1]) 0
Step 6 · Check with a trace
daypcheapestp − cheapestbest
17700
21100
35144
43124
56155
64135

Day 2 lowers the cheapest price to 1; day 5 (price 6) gives the record profit 6 − 1 = 5. The best day to buy came long before the best day to sell — which is why we carry the cheapest price along.

One skeleton, many problems

The idea “carry the result along” covers totals, averages and several records at once:

QuestionWhat changesReal result
The averagea running total and a count; divide at the endrunning_mean([4, 9, 2, 7])
→ 5.5
The second largest distinct valuetwo records: first and second; a new leader pushes the old one downsecond_largest([4, 9, 2, 7])
→ 7
The longest run of equal lettersthe current run (family 2) plus a recordlongest_run("aaabccdd")
→ 3
How to recognise it“Largest”, “smallest”, “total”, “best”, “longest”. Ask: what is the best so far, and what does one new element do to it?
Next: 6 · Expected vs actual →
6 · Expected versus actual: the missing number
Problems of this family on this page:Missing number
The task

A list contains every number from 0 to n except one, in any order, so its length is n. Return the missing number.

  • [3, 0, 1, 4, 6, 2] has length 6, so the full set is 0..6 and 5 is missing.
  • The missing number can be 0 or n itself: [0, 1] → 2.
Function: missing_number(nums: list) -> int
Given
nums = [3, 0, 1, 4, 6, 2]
Answer
missing_sum([3, 0, 1, 4, 6, 2]) → 5

The list holds the numbers 0..n with exactly one missing. Slow down here: people find the answer by hand in three different ways — and each way becomes a different algorithm. That is the real lesson: an algorithm is your manual method, written down.

Way A · Lay them out in order and look for the gap

Lay out the cards 0, 1, 2, 3, 4, 6 — the hole is obvious: 5. In code: sort, then position i should hold the number i; the first place where it does not is the answer. Edge case: if there is no hole inside (the list is 0..n−1), the missing number is n itself — that is the line after the loop. Cost: sorting, O(n log n).

def missing_sorted(nums):
    for i, x in enumerate(sorted(nums)):     # STEP: position i should hold the number i
        if x != i:
            return i
    return len(nums)                         # EDGE: no hole inside, so n itself is missing
# output when run:>>> missing_sorted([3, 0, 1, 4, 6, 2]) 5 >>> missing_sorted([0, 1]) 2 >>> missing_sorted([1]) 0
Way B · Tick a checklist

Write 0 to 6 on a sheet and tick each number you read; the unticked one is missing. In code: a list of n + 1 boxes, one pass ticks them, and the box left False is the answer. Cost: O(n) time, plus O(n) extra memory for the boxes.

def missing_checklist(nums):
    present = [False] * (len(nums) + 1)      # STATE: one box for each number 0..n
    for x in nums:
        present[x] = True                    # STEP: tick the box
    return present.index(False)              # END: the box nobody ticked
# output when run:>>> missing_checklist([3, 0, 1, 4, 6, 2]) 5 >>> missing_checklist([0, 1]) 2 >>> missing_checklist([1]) 0
Way C · Reconcile the totals

Like a cash register: you know what the total should be and what is actually there; the shortfall is what is missing. The full set 0..n adds up to n·(n + 1)/2 — for n = 6 that is 21. The list adds up to 16. 21 − 16 = 5. Cost: O(n) time and O(1) memory — two numbers instead of a sheet of boxes.

def missing_sum(nums):
    n = len(nums)                            # n numbers are here, so the full set is 0..n
    expected = n * (n + 1) // 2              # STATE: what the total should be
    return expected - sum(nums)              # END: the shortfall is the missing number
# output when run:>>> missing_sum([3, 0, 1, 4, 6, 2]) 5 >>> missing_sum([0, 1]) 2 >>> missing_sum([1]) 0
WayWhat you holdTimeExtra memoryReal result
A · sort, find the gapthe sorted copyO(n log n)O(n)missing_sorted([3, 0, 1, 4, 6, 2]) → 5
B · checklistone box per numberO(n)O(n)missing_checklist([3, 0, 1, 4, 6, 2]) → 5
C · totalstwo numbers: expected and actualO(n)O(1)missing_sum([3, 0, 1, 4, 6, 2]) → 5
The typical mistake — and what it really prints
def missing_sum_buggy(nums):
    expected = sum(range(len(nums)))         # 0..n-1: forgets that the full set goes up to n
    return expected - sum(nums)
# output when run:>>> missing_sum_buggy([0, 1]) 0 >>> missing_sum([0, 1]) 2

0..n is n + 1 numbers, not n. With [0, 1] the full set is 0, 1, 2, so the expected total is 3, not 1. An off-by-one at the edge of a range is the most common bug in this family — always test the case where the missing number is the last one.

One skeleton, many problems

The same reconciliation solves a whole family. The rule: count the unknowns. One unknown number needs one fact — a sum. Two unknowns need two independent facts.

QuestionWhat changesReal result
1..n, one number appears twice — which?the surplus: actual − expectedfind_duplicate([1, 3, 4, 2, 2])
→ 2
Every number twice except oneexpected “all twice” = 2 × sum of distinct; the shortfall is the lonely onesingle_number([4, 1, 2, 1, 2])
→ 4
1..n, one number twice and one missingtwo unknowns → two facts: a seen-set finds the twin, the totals give the gapset_mismatch([1, 2, 2, 4])
→ [2, 3]

The last one is worth reading: fact 1 comes from family 3 (the value met twice), fact 2 from the totals (they differ by exactly “twin − missing”). Combining two simple families is how most “medium” problems are built.

def set_mismatch(nums):
    seen, dup = set(), None
    for x in nums:                           # fact 1: which number appears twice
        if x in seen:
            dup = x
        seen.add(x)
    n = len(nums)
    missing = dup - (sum(nums) - n * (n + 1) // 2)   # fact 2: the totals differ by dup - missing
    return [dup, missing]
# output when run:>>> set_mismatch([1, 2, 2, 4]) [2, 3]
How to recognise it“Missing”, “lost”, “appears twice”, “all numbers from 1 to n”. When you know what the complete data should look like, compare it with what you have.
Next: Practice: which family is it? →
Practice: which family is it?

Four kinds of practice. First, recognise the family from the wording — the skill that unlocks the code. Then read code the way an interviewer does: find the bug in a script that returns the wrong answer, predict what code prints, and fill in the missing piece of a solution. Every answer was checked by running the code.

Correct answers: 0 / 32
Part 1 · Which family is it?
1
From the list of pages a user opened, find the page opened most often.
2
Turn WWWWBBBWW into 4W3B2W.
3
Is any order ID duplicated in today’s export?
4
The log is in time order. Find the first customer who placed exactly one order this month.
5
Find the longest stretch of consecutive days with sales above plan.
6
Tickets numbered 1 to 500 were sold; one number is missing from the register. Which one?
7
Given daily revenue, find the best single day.
8
Do two words consist of exactly the same letters?
9
Remove repeated e-mails from a sign-up list, keeping the first sign-up of each.
10
For each product, what share of all orders does it take?
11
Restore the original text from the code 3a2b1c.
12
Every user ID was logged twice except one, logged once. Find it.
Part 2 · Find the bug

Each script below was run. You see what it should have returned and what it really returned — pick the cause.

13
def compress(s):
    if not s:
        return ""
    parts = []
    current, count = s[0], 1
    for ch in s[1:]:
        if ch == current:
            count += 1
        else:
            parts.append(f"{count}{current}")
            current, count = ch, 1
    return "".join(parts)
Call: compress("aaabccdd") · expected '3a1b2c2d' · got '3a1b2c'
14
def count_chars(s):
    counts = {}
    for ch in s:
        counts[ch] += 1
    return "".join(f"{n}{ch}" for ch, n in counts.items())
Call: count_chars("banana") · expected '1b3a2n' · got KeyError: 'b'
15
def largest(nums):
    best = 0
    for x in nums:
        if x > best:
            best = x
    return best
Call: largest([-3, -1, -2]) · expected -1 · got 0
16
def missing_number(nums):
    expected = sum(range(len(nums)))
    return expected - sum(nums)
Call: missing_number([0, 1]) · expected 2 · got 0
17
def first_uniq_char(s):
    seen = set()
    for i, ch in enumerate(s):
        if ch not in seen:
            return i
        seen.add(ch)
    return -1
Call: first_uniq_char("loveleetcode") · expected 2 · got 0
18
def two_sum(nums, target):
    seen = {}
    for i, x in enumerate(nums):
        seen[x] = i
        if target - x in seen:
            return [seen[target - x], i]
    return []
Call: two_sum([5, 1, 4], 10) · expected [] · got [0, 0]
19
def median(nums):
    n = len(nums)
    mid = n // 2
    if n % 2 == 1:
        return nums[mid]
    return (nums[mid - 1] + nums[mid]) / 2
Call: median([340, 55, 120, 80, 80]) · expected 80 · got 120
20
def unique_sorted(items):
    result = list(set(items))
    result = result.sort()
    return result
Call: unique_sorted([3, 1, 3, 2]) · expected [1, 2, 3] · got None
Part 3 · What will the code print?

Trace the code in your head — or on paper, row by row — before you pick.

21
def compress(s):
    parts = []
    current, count = s[0], 1
    for ch in s[1:]:
        if ch == current:
            count += 1
        else:
            parts.append(f"{count}{current}")
            current, count = ch, 1
    parts.append(f"{count}{current}")
    return "".join(parts)

print(compress("abab"))
22
counts = {}
for ch in "abca":
    counts[ch] = counts.get(ch, 0) + 1
print(counts)
23
seen = set()
for x in [2, 5, 1, 5, 2]:
    if x in seen:
        print(x)
        break
    seen.add(x)
24
best, count, prev = 0, 0, None
for ch in "aabbbab":
    count = count + 1 if ch == prev else 1
    best = max(best, count)
    prev = ch
print(best)
25
cheapest, best = float("inf"), 0
for p in [7, 6, 4, 3, 1]:
    cheapest = min(cheapest, p)
    best = max(best, p - cheapest)
print(best)
26
nums = [0, 1, 2, 3]
n = len(nums)
print(n * (n + 1) // 2 - sum(nums))
27
out, digits = [], ""
for ch in "12z3a":
    if ch.isdigit():
        digits += ch
    else:
        out.append(ch * int(digits))
        digits = ""
print(len("".join(out)))
Part 4 · Fill in the missing piece

One piece of a working solution is replaced by ???. Only one option makes all the tests pass.

28
def count_chars(s):
    counts = {}
    for ch in s:
        counts[ch] = ???
    return "".join(f"{n}{ch}" for ch, n in counts.items())
count_chars("banana") → '1b3a2n' · count_chars("") → ''
29
def two_sum(nums, target):
    seen = {}
    for i, x in enumerate(nums):
        if ??? in seen:
            return [seen[target - x], i]
        seen[x] = i
    return []
two_sum([3, 8, 11, 5, 4], 9) → [3, 4] · two_sum([2, 7, 11, 15], 9) → [0, 1]
30
def missing_number(nums):
    n = len(nums)
    expected = ???
    return expected - sum(nums)
missing_number([3, 0, 1]) → 2 · missing_number([0, 1]) → 2 · missing_number([1]) → 0
31
def compress(s):
    if not s:
        return ""
    parts = []
    current, count = s[0], 1
    for ch in s[1:]:
        if ch == current:
            count += 1
        else:
            parts.append(f"{count}{current}")
            current, count = ch, 1
    ???
    return "".join(parts)
compress("aaabccdd") → '3a1b2c2d' · compress("a") → '1a'
32
def max_profit(prices):
    cheapest, best = float("inf"), 0
    for p in prices:
        cheapest = min(cheapest, p)
        best = max(best, ???)
    return best
max_profit([7, 1, 5, 3, 6, 4]) → 5 · max_profit([7, 6, 4, 3, 1]) → 0
The logic behind the solution

Knowing the code by heart does not help when the task is worded slightly differently. What transfers is the reasoning. Three topics come up at almost every analyst interview — strings and counting, sorting, and two pointers — so here they are taken apart slowly, with the questions you should be asking yourself at each step.

Four questions that start almost any solution
  1. Does order matter? "Count the letters" and "count letters in a row" are different tasks.
  2. What must I remember while walking through the data? A counter, the previous element, the best value so far, what I have already seen.
  3. When does what I remember change? This moment — "the run broke", "the sum got too big" — is the heart of the algorithm.
  4. What is left over at the end? Many bugs live here: the last group is never written because the loop ended first.

1 · Strings and counting characters

A string is a sequence of characters, just like a list. You can loop over it, take s[i] by position and look back at the previous character with s[i - 1]. One difference matters: strings cannot be changed in place. So the result is collected in a list and glued together once at the end with "".join(parts).

The classic task: count every character

The most frequent string problem at analyst interviews: print how many times each character appears, as "count + character". The count is the total for the whole string, so abab gives 2a2b — the two a's are counted together even though they are not neighbours. Characters go in the order they first appear.

Given
s = "aabbccc"
s = "abab"
Answer
count_chars("aabbccc") → '2a2b3c'
count_chars("abab") → '2a2b'
How to reason about it
  1. Order of neighbours does not matter — only "how many of each". So we need a place to keep a running tally per character: a dictionary character → count.
  2. Walk through the string once. For each character: take its current count, or 0 if it is new, and add one — counts.get(ch, 0) + 1.
  3. A dictionary remembers the order in which keys were first added (Python 3.7+). So walking through it gives the characters in order of first appearance — no extra work needed.
  4. Build the answer from the dictionary: for every pair write "count + character" and join.
#ichcounts[ch] = counts.get(ch, 0) + 1counts
10a1{'a': 1}
21b1{'a': 1, 'b': 1}
32a2{'a': 2, 'b': 1}
43b2{'a': 2, 'b': 2}
5end··→ "2a2b" ✓

Look at the row with i = 2: the second a is not next to the first one, yet its count simply goes from 1 to 2. Neighbours never matter — only the running total for each character.

def count_chars(s):
    """'aabbccc' -> '2a2b3c', 'abab' -> '2a2b': how many times each char appears
    in total, in the order chars first appear."""
    counts = {}                       # a dict remembers insertion order (Python 3.7+)
    for ch in s:
        counts[ch] = counts.get(ch, 0) + 1
    return "".join(f"{n}{ch}" for ch, n in counts.items())
Variations you will meet

The same dictionary answers a whole family of questions — only the last step changes. Sort its keys for alphabetical order; sort its items by count for "most frequent first"; filter by n > 1 for the characters that repeat; take max for the single most frequent one. Learn the tally once and every variant is one line.

Given
s = "aabbccc"
Answer
count_chars_sorted(s) → '2a2b3c'
count_by_frequency(s) → '3c2a2b'
most_frequent_char(s) → 'c'
repeated_chars(s) → ['a', 'b', 'c']
Without a dictionary? Counter

Counter from the collections module does the tally in one word and also keeps first-appearance order. Interviewers often ask for the version without imports first — show the dictionary, then mention Counter as the shortcut.

from collections import Counter

counts = Counter("abab")
print(counts)
print("".join(f"{n}{ch}" for ch, n in counts.items()))
# output when run:Counter({'a': 2, 'b': 2}) 2a2b
Not to be confused with: counting runs in a row

A different, rarer task — run-length encoding — counts only neighbouring repeats: abab becomes 1a1b1a1b. For aabbccc both tasks print the same 2a2b3c, so test your solution on abab to make sure you solved the one that was asked. If the wording is ambiguous, ask.

Given
s = "abab"
s = "aabbcca"
Answer
count_chars("abab") → '2a2b'
compress("abab") → '1a1b1a1b'
compress("aabbcca") → '2a2b2c1a'
#is[i]s[i] == s[i-1]?countwritten
10a—1""
21aTrue → count += 12""
32bFalse → write "2a"1"2a"
43bTrue → count += 12"2a"
54cFalse → write "2b"1"2a2b"
65cTrue → count += 12"2a2b"
76aFalse → write "2c"1"2a2b2c"
8end·flush last run → "1a"·"2a2b2c1a" ✓

Here the dictionary is replaced by a single counter that resets whenever the character changes — and the last run has to be written after the loop ends.

Where people slip: counting only neighbours when the task asks for totals (test on abab); sorting alphabetically when the order of appearance was expected; and forgetting the empty string, which should return an empty answer, not crash.

2 · Mean, median and mode — by hand

An analyst's favourite warm-up: "compute the average, the median and the most frequent value without NumPy". It checks three things at once — that you know what each statistic means, that you handle the edge cases, and that you know which one to trust.

Given
amounts = [120, 80, 340, 55, 80, 2000]
Answer
round(mean(amounts), 2) → 445.83
median(amounts) → 100.0
mode(amounts) → [80]
Mean: sum divided by count

The definition is the algorithm: add everything up, divide by how many there are. The only trap is the empty list — dividing by zero crashes, so return None first.

def mean(nums):
    if not nums:
        return None                   # an empty list has no mean (and avoids / 0)
    return sum(nums) / len(nums)
Median: the middle of the sorted data

Sort first — the median of unsorted data is meaningless. With an odd number of values there is one middle element. With an even number there are two, and the median is their average. The index of the middle is n // 2.

Given
odd = [340, 55, 120, 80, 80]
even = [340, 55, 120, 80, 80, 2000]
Answer
median(odd) → 80
median(even) → 100.0
1558080M120340n=5 odd → s[2] = 80
2558080M120M3402000n=6 even → (s[2] + s[3]) / 2 = (80 + 120) / 2 = 100.0

Row 1 has five values: the single middle one is the median. Row 2 adds one more value, so there are two middles and the median is their average.

def median(nums):
    if not nums:
        return None
    s = sorted(nums)                  # the median only makes sense on sorted data
    n = len(s)
    mid = n // 2
    if n % 2 == 1:                    # odd length: one middle element
        return s[mid]
    return (s[mid - 1] + s[mid]) / 2  # even length: average of the two middles
Mode: the most frequent value — and there can be several

The mode is the counting pattern from the strings section applied to numbers: tally every value, find the highest count, return the values that have it. Return a list — two values can tie, and silently picking one would hide that.

Given
amounts = [120, 80, 340, 55, 80, 2000]
tie = [1, 1, 2, 2, 3]
Answer
mode(amounts) → [80]
mode(tie) → [1, 2]
#xcounts[x]c == top?
11201—
2802✓ 2 == 2
33401—
4551—
520001—
def mode(nums):
    """All most frequent values, sorted (there can be several)."""
    if not nums:
        return []
    counts = {}
    for x in nums:
        counts[x] = counts.get(x, 0) + 1
    top = max(counts.values())
    return sorted(x for x, c in counts.items() if c == top)
Which one to trust: one large order of 2000 pulls the mean up to almost 446, while the median stays at 100 — closer to a typical order. With skewed data like revenue, report the median alongside the mean. Saying this unprompted is what turns a coding answer into an analyst's answer.
Where people slip: forgetting to sort before taking the median; using n / 2 instead of n // 2 (a float cannot be an index); forgetting the even-length case; and returning one mode when there are several.

3 · Sorting: when it helps and how to control it

Sorting is rarely the answer by itself — it is the step that makes the answer easy. After sorting, similar things stand next to each other: duplicates sit in a row, close values become neighbours, and the smallest and largest are at the two ends. That is what makes two pointers and "compare with the neighbour" work. The price is O(n log n), so it pays off when it removes an O(n²) search.

sorted() or .sort() — and the classic bug

sorted(x) returns a new sorted list and leaves x untouched. x.sort() sorts the list itself and returns None. So x = x.sort() silently replaces your data with None — one of the most frequent mistakes in live coding.

data = [3, 1, 2]
result = sorted(data)
print(result, data)    # a new list; data is unchanged

result = data.sort()
print(result, data)    # None — and data itself is now sorted
# output when run:[1, 2, 3] [3, 1, 2] None [1, 2, 3]
Sorting by several rules at once: the key

The key argument is a function that turns each element into "what to compare". If it returns a tuple, Python compares the first values, and only on a tie looks at the second. A minus sign flips a number into descending order. So key=lambda kv: (-kv[1], kv[0]) reads as: "most frequent first; on a tie — alphabetically".

Given
s = "mississippi"
Answer
count_by_frequency(s) → '4i4s2p1m'
from collections import Counter

counts = Counter("mississippi")
ranked = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))
print(ranked)
print("".join(f"{n}{ch}" for ch, n in ranked))
# output when run:[('i', 4), ('s', 4), ('p', 2), ('m', 1)] 4i4s2p1m
#charcountkey = (−count, char)place
1i4(-4, "i")1 ✓
2s4(-4, "s")2
3p2(-2, "p")3
4m1(-1, "m")4

Look at i and s: both appear 4 times, so the first part of the key is equal. The tie is broken by the second part — the letter itself — and i comes before s.

If you are asked to write a sort yourself

Insertion sort is the easiest to explain, because it is how people sort cards in their hand. The left part is always sorted. Take the next card and slide it left past every bigger card until it fits. It is O(n²), so in real work you use sorted() — Python's built-in algorithm is O(n log n). But being able to state the rule "the left part stays sorted" is exactly what the interviewer is checking.

Given
nums = [5, 2, 4, 6, 1, 3]
Answer
insertion_sort(nums) → [1, 2, 3, 4, 5, 6]
def insertion_sort(nums):
    nums = nums[:]
    for i in range(1, len(nums)):
        current = nums[i]
        j = i - 1
        while j >= 0 and nums[j] > current:   # shift bigger values one step right
            nums[j + 1] = nums[j]
            j -= 1
        nums[j + 1] = current                 # drop current into the gap
    return nums
15✓24613a[:1] → sorted ✓
22▲54613insert 2 at index 0
324▲5613insert 4 at index 1
42456▲13insert 6 at index 3
51▲24563insert 1 at index 0
6123▲456insert 3 at index 2

The shaded cells are the part that is already sorted — it grows by one on every row. ▲ marks where the new element landed.

Where people slip: x = x.sort(); sorting numbers stored as strings ("10" comes before "9"); and sorting when the answer needs the original positions — as in Two Sum, where sorting would lose them and a dictionary is the right tool.

4 · Two pointers and duplicates

Two indexes move through the data by a rule, instead of a loop inside a loop. Every step moves at least one pointer, so the whole walk takes at most n steps — O(n). Everything depends on the rule: which pointer moves, and why that move can never skip the right answer. If you can say that sentence out loud, you understand the solution.

Shape A · towards each other

One pointer at each end, moving inwards. Works on sorted data and on symmetric questions (palindromes, reversing). Take a sorted list and a target sum. If the sum of the two ends is too small, the left number cannot be part of any answer — even paired with the largest remaining number it falls short. So we drop it and move left. If the sum is too big, the same logic drops the right number.

Given
nums = [1, 3, 4, 6, 8, 11]
target = 10
Answer
two_sum_sorted(nums, target) → [2, 3]
def two_sum_sorted(nums, target):
    """Sorted list: positions of two numbers that add up to target (opposite pointers)."""
    left, right = 0, len(nums) - 1
    while left < right:
        total = nums[left] + nums[right]
        if total == target:
            return [left, right]
        if total < target:
            left += 1                 # need a bigger sum: only moving left can give one
        else:
            right -= 1                # need a smaller sum: only moving right can give one
    return []
012345
11L346811R1 + 11 = 12 > 10 → R −= 1
21L3468R111 + 8 = 9 < 10 → L += 1
313L468R113 + 8 = 11 > 10 → R −= 1
413L46R8113 + 6 = 9 < 10 → L += 1
5134L6R8114 + 6 = 10 = target ✓

Each row throws away one number that provably cannot be in the answer. Five steps instead of fifteen pair checks.

Shape B · same direction, fast and slow

Both pointers start on the left. F (fast) reads every element. S (slow) marks where the "good" part of the array ends. When fast finds something worth keeping, it is copied to the slow position and slow moves on. This is how you clean an array in place without a second list: removing duplicates, moving zeroes to the end, compressing.

Given
nums = [1, 1, 2, 3, 3, 3, 4]
Answer
remove_duplicates(nums) → 4
nums[:4] → [1, 2, 3, 4]
def remove_duplicates(nums):
    """Sorted list: keep unique values at the front, return how many."""
    if not nums:
        return 0
    slow = 0                          # last position of the unique part
    for fast in range(1, len(nums)):  # fast scans everything
        if nums[fast] != nums[slow]:
            slow += 1
            nums[slow] = nums[fast]
    return slow + 1
11S1F23334a[F]=1 == a[S] → skip
212S2F3334a[F]=2 != a[S] → S += 1, a[S] = 2
3123S3F334a[F]=3 != a[S] → S += 1, a[S] = 3
4123S33F34a[F]=3 == a[S] → skip
5123S333F4a[F]=3 == a[S] → skip
61234S334Fa[F]=4 != a[S] → S += 1, a[S] = 4

The shaded part is the clean result growing on the left. When F sees a value equal to the one at S, it is a duplicate and simply skipped.

Removing duplicates: two different questions

Shape B above removes duplicates in place from a sorted array — that is a LeetCode-style constraint. The everyday interview version is simpler and more common: "return the list without duplicates". Then order matters. list(set(x)) removes repeats but does not keep the original order: a set makes no promise about order. In the example below it even happens to come out sorted — a coincidence of how small numbers are stored, not something to rely on. To keep the first occurrence of each value, walk once and remember what you have already taken.

Given
items = [3, 1, 3, 2, 1]
Answer
list(set(items)) → [1, 2, 3]
dedup_keep_order(items) → [3, 1, 2]
unique_sorted(items) → [1, 2, 3]
only_unique(items) → [2]
find_duplicates(items) → [3, 1]
#xx in seen?seenresult
13False → take ✓[3][3]
21False → take ✓[1, 3][3, 1]
33True → skip[1, 3][3, 1]
42False → take ✓[1, 2, 3][3, 1, 2]
51True → skip[1, 2, 3][3, 1, 2]

The set seen answers "have I taken this already?" instantly; the list result keeps the order. Two structures, each doing what it is good at.

def dedup_keep_order(items):
    """[3, 1, 3, 2, 1] -> [3, 1, 2]: drop repeats, keep the first occurrence."""
    seen = set()                      # what we have already taken
    result = []
    for x in items:
        if x not in seen:
            seen.add(x)
            result.append(x)
    return result
The questionTool
Without duplicates, original order keptseen + result / list(dict.fromkeys(x))
Without duplicates, sortedsorted(set(x))
Only values that appear exactly oncecounts[x] == 1
Which values repeatcounts[x] > 1
In place, in a sorted arrayslow / fast
Shape C · one pointer on each of two lists

When two sorted lists have to be combined or compared, give each its own pointer. At every step compare the two current elements, use the right one, and move only that pointer. Merging sorted lists, checking whether one string is a subsequence of another, and finding common elements all follow this shape.

Given
s = "abc"
t = "ahbgdc"
Answer
is_subsequence(s, t) → True
def is_subsequence(s, t):
    i = 0                             # how much of s is already matched
    for ch in t:
        if i < len(s) and s[i] == ch:
            i += 1
    return i == len(s)
Which shape? Read the wording
If the task says…Shape
a sorted array and a pair with a target sum; a palindrome; reverseA
"in place", "without extra memory", remove or move elementsB
two sorted lists; "merge", "is a subsequence", "common elements"C
Where people slip: while left < right versus <= (decide whether the middle element can pair with itself); forgetting to move a pointer in one branch — an endless loop; and using shape A on data that is not sorted, where the "drop it" logic is no longer true.
The sentence to say at the interview: before writing code, describe your rule in one line — "I compare with the previous character and write the run when it breaks", "I move the pointer with the smaller value". Interviewers grade this sentence as much as the code.
Complexity: what Big-O actually means

Big-O answers one question: when the input grows, how fast does the work grow? It does not measure seconds — those depend on the computer. It describes the shape of growth. O(n) means "twice the data, twice the work". O(n²) means "twice the data, four times the work".

2026-10-04T06:42:48.946859 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ 0 10 20 30 40 n — size of the input 0 50 100 150 200 250 operations O(1) O(log n) O(n) O(n log n) O(n²)
2026-10-04T06:42:49.240278 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ 0 10 20 30 40 n — размер входных данных 0 50 100 150 200 250 операций O(1) O(log n) O(n) O(n log n) O(n²)

The chart shows why this matters more than any micro-optimisation. On small inputs every approach is fast. As the data grows, O(n²) leaves the others behind — and that is the curve your interviewer wants you to avoid.

ComplexityWhat it looks like in codeExample
O(1)A fixed number of steps, regardless of sizeReading a dict value by key
O(log n)The search space halves at every stepBinary search in a sorted array
O(n)One pass over the dataSum of a list, finding the maximum
O(n log n)Sorting, or log n work for each elementsorted()
O(n²)A loop inside a loop over the same dataComparing every pair
A real measurement, not a promise

Both functions below solve Two Sum. I timed them on arrays of growing length, on the worst case where no answer exists and every element must be checked. The numbers are measured, not modelled.

2026-10-04T06:42:49.143198 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ 0 1000 2000 3000 4000 5000 array length 1 0 − 1 1 0 0 1 0 1 1 0 2 time, ms (log scale) nested loops O(n²) dict lookup O(n) ×725 faster
2026-10-04T06:42:49.383740 image/svg+xml Matplotlib v3.10.8, https://matplotlib.org/ 0 1000 2000 3000 4000 5000 длина массива 1 0 − 1 1 0 0 1 0 1 1 0 2 время, мс (лог. шкала) вложенные циклы O(n²) поиск в словаре O(n) ×725 быстрее
Look at the step from 2,000 to 4,000 elements: the input doubled, the nested loops became roughly four times slower, and the dictionary only twice as slow. That is O(n²) against O(n) — visible in real milliseconds.
Memory counts too. The fast solution stores up to n values in a dictionary — O(n) extra memory. That is the usual trade: spend memory to save time. Interviewers often ask "can you do it with O(1) memory?" — be ready to say what it would cost.
Pattern map: which tool when

Most interview problems are variations of a small set of patterns. The skill is recognising the pattern from the wording. This table is the cheat sheet: a signal in the problem statement, and the technique it points to.

If the problem says…TryTypical complexity
"find a pair / a duplicate / have we seen this?"Arrays & hashingO(n)
a sorted array, a pair or triple with a target sum, a palindromeTwo pointersO(n)
"longest / shortest contiguous subarray or substring"Sliding windowO(n)
matching brackets, "the next greater element", undoStackO(n)
a sorted array and "find", or "the smallest value that works"Binary searchO(log n)
"maximum / minimum", where the best local choice is always safeGreedyO(n)
"sum of a range", "subarrays that add up to k"Prefix sumsO(n)
meetings, bookings, time ranges that overlapIntervalsO(n log n)
"how many ways", "the best total", choices that build on earlier onesDynamic programmingO(n)
Why an analyst needs this: these are not puzzles for their own sake. A hash map is how a JOIN works; a sliding window is a rolling metric; prefix sums are cumulative totals; intervals are session and subscription overlaps. You meet these patterns in data every week — the interview just strips them down.
Easy problems interviewers actually ask

Most analyst interviews use short, simple problems — the point is to watch how you think, not to stump you. Each problem below has its line of reasoning hidden separately from the solution: open it if you are stuck, but try to reach the idea yourself first.

Strings and characters 13

1Count characters: aabbccc → 2a2b3c
Strings and charactersEasyInterview classic

Print how many times each character appears in total, as "count + character", in the order characters first appear.

"aabbccc" → "2a2b3c" "abab" → "2a2b" "" → ""
2Count characters in alphabetical order
Strings and charactersEasyInterview classic

The same totals, but listed in alphabetical order of the characters.

"cabbac" → "2a2b2c" "banana" → "3a1b2n"
3Most frequent characters first
Strings and charactersEasyInterview classic

Print the counts with the most frequent character first. On a tie, keep the order of first appearance.

"aabbccc" → "3c2a2b" "abab" → "2a2b"
4Count letters only, ignoring case
Strings and charactersEasyInterview classic

Count the letters of a phrase, treating upper and lower case as the same and ignoring spaces and punctuation.

"Hello, World!" → "1h1e3l2o1w1r1d"
5The most frequent character
Strings and charactersEasyInterview classic

Return the character that appears most often. On a tie, return the one that appears first.

"abracadabra" → "a" "aabb" → "a"
6Which characters repeat
Strings and charactersEasyInterview classic

List every character that appears more than once, each one time, in order of first appearance.

"programming" → ["r", "g", "m"]
7Word frequency
Strings and charactersEasyInterview classic

Count how many times each word appears in a sentence, ignoring case.

"The cat and the hat" → {"the": 2, "cat": 1, "and": 1, "hat": 1}
8Not to be confused with: runs in a row
Strings and charactersEasyInterview classic

Count only neighbouring repeats (run-length encoding). Here abab is four separate runs.

"aabbccc" → "2a2b3c" "abab" → "1a1b1a1b" "aabbcca" → "2a2b2c1a"
9The reverse: 2a2b3c → aabbccc
Strings and charactersEasyInterview classic

Decode a string of "count + character" back. Counts can have more than one digit.

"2a2b3c" → "aabbccc" "12z" → "zzzzzzzzzzzz"
10First unique character
Strings and charactersEasy#387 Open on LeetCode ↗

Return the position of the first character that appears exactly once, or −1 if there is none.

"leetcode" → 0 "loveleetcode" → 2 "aabb" → -1
11Valid anagram
Strings and charactersEasy#242 Open on LeetCode ↗

Decide whether two strings consist of exactly the same letters, the same number of times.

("anagram", "nagaram") → True ("rat", "car") → False
12Reverse each word
Strings and charactersEasy#557 Open on LeetCode ↗

Reverse the letters of every word, keeping the words in their original order.

"Let's take LeetCode contest" → "s'teL ekat edoCteeL tsetnoc"
13Longest common prefix
Strings and charactersEasy#14 Open on LeetCode ↗

Find the longest beginning shared by all the words.

["flower", "flow", "flight"] → "fl" ["dog", "car"] → ""

Duplicates 5

14A list without duplicates, order kept
DuplicatesEasyInterview classic

Return the list with every repeat removed, keeping the first occurrence of each value in its original place.

[3, 1, 3, 2, 1] → [3, 1, 2]
15Unique values, sorted
DuplicatesEasyInterview classic

Return the distinct values in ascending order.

[3, 1, 3, 2, 1] → [1, 2, 3]
16Values that appear exactly once
DuplicatesEasyInterview classic

Keep only the values that appear once in the whole list; drop everything that has a repeat.

[3, 1, 3, 2, 1] → [2]
17Which values are duplicated
DuplicatesEasyInterview classic

List every value that appears more than once, each value one time.

[3, 1, 3, 2, 1] → [3, 1]
18Remove repeated characters from a string
DuplicatesEasyInterview classic

Keep the first occurrence of every character, in order.

"banana" → "ban" "mississippi" → "misp"

Mean, median, mode 3

19Mean without NumPy
Mean, median, modeEasyInterview classic

Return the arithmetic mean of a list, and None for an empty list.

[120, 80, 340, 55, 80, 2000] → 445.83… [] → None
20Median without NumPy
Mean, median, modeEasyInterview classic

Return the median: the middle value of the sorted data, or the average of the two middle values when the count is even.

[340, 55, 120, 80, 80] → 80 [340, 55, 120, 80, 80, 2000] → 100.0
21Mode — and what to do with ties
Mean, median, modeEasyInterview classic

Return the most frequent value. If several values share the top count, return all of them, sorted.

[120, 80, 340, 55, 80, 2000] → [80] [1, 1, 2, 2, 3] → [1, 2]

Two pointers 6

22Reverse a string in place

Reverse a list of characters without creating a new list.

["h","e","l","l","o"] → ["o","l","l","e","h"]
23Remove duplicates from a sorted array
Two pointersEasy#26 Open on LeetCode ↗

In a sorted list, move each distinct value to the front, in place, and return how many there are.

[1, 1, 2, 3, 3, 3, 4] → 4 # a[:4] = [1, 2, 3, 4]
24Move zeroes to the end

Move every zero to the end of the list, keeping the order of the other numbers, in place.

[0, 1, 0, 3, 12] → [1, 3, 12, 0, 0]
25Merge two sorted arrays
Two pointersEasy#88 Open on LeetCode ↗

The first list has room at its end for the second. Merge them into the first list so it stays sorted.

nums1 = [1, 2, 3, 0, 0, 0], m = 3, nums2 = [2, 5, 6], n = 3 → [1, 2, 2, 3, 5, 6]
26Squares of a sorted array

Given a sorted list that may contain negatives, return the squares in sorted order.

[-4, -1, 0, 3, 10] → [0, 1, 9, 16, 100]
27Is subsequence

Check whether the letters of one string appear in another in the same order (not necessarily side by side).

("abc", "ahbgdc") → True ("axc", "ahbgdc") → False

Sorting 3

28Maximum product of three numbers

Find the largest product of any three numbers in the list. Numbers can be negative.

[1, 2, 3, 4] → 24 [-10, -10, 1, 3, 2] → 300
29Height checker

Count how many positions differ between the list and its sorted version.

[1, 1, 4, 2, 1, 3] → 3
30Largest perimeter triangle

Pick three lengths that can form a triangle with the largest possible perimeter, or return 0.

[2, 1, 2] → 5 [1, 2, 1, 10] → 0

Counting and sets 4

31Majority element
Counting and setsEasy#169 Open on LeetCode ↗

One value appears more than half the time. Find it.

[3, 2, 3] → 3 [2, 2, 1, 1, 1, 2, 2] → 2
32Ransom note
Counting and setsEasy#383 Open on LeetCode ↗

Can the note be built from the magazine's letters, using each letter at most once?

("aa", "aab") → True ("aa", "ab") → False
33Single number
Counting and setsEasy#136 Open on LeetCode ↗

Every number appears twice except one. Find that one.

[4, 1, 2, 1, 2] → 4
34Missing number
Counting and setsEasy#268 Open on LeetCode ↗

The list holds every number from 0 to n except one. Find the missing one.

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

Loops and arithmetic 3

35FizzBuzz
Loops and arithmeticEasy#412 Open on LeetCode ↗

For numbers 1 to n: multiples of 3 become "Fizz", of 5 — "Buzz", of both — "FizzBuzz", others stay as numbers.

n = 15 → [..., "13", "14", "FizzBuzz"]
36Palindrome number
Loops and arithmeticEasy#9 Open on LeetCode ↗

Decide whether a number reads the same backwards — without converting it to a string.

121 → True -121 → False 10 → False
37Roman to integer
Loops and arithmeticEasy#13 Open on LeetCode ↗

Convert a Roman numeral to an integer.

"III" → 3 "IX" → 9 "MCMXCIV" → 1994
Similar problems: pointers, search, windows

Interviewers rarely ask the exact LeetCode problem you memorised — they ask a cousin of it. This section trains that skill: each block opens with a template of the technique, followed by original LeetCode problems and product-flavoured variations built on them. Notice how little changes between them: usually one condition or one line.

Two pointers 7

Two shapes cover every problem below. Opposite ends: the pointers walk towards each other, and at each step one element is ruled out. Fast and slow: the fast pointer reads, the slow one marks where the kept part ends. Before writing code, say which shape it is and what rule moves the pointer.

Template
# Shape A — opposite ends
left, right = 0, len(items) - 1
while left < right:
    if ...:              # the rule of the task decides
        left += 1        # which pointer is ruled out
    else:
        right -= 1

# Shape B — fast reads, slow writes
slow = 0
for fast in range(len(items)):
    if keep(items[fast]):
        items[slow] = items[fast]
        slow += 1
1Reverse only the vowels

Reverse the order of the vowels in a string, leaving every other character where it is.

"hello" → "holle" "leetcode" → "leotcede"
2Palindrome after at most one deletion

Decide whether a string can become a palindrome by deleting at most one character.

"aba" → True "abca" → True # − "c" "abc" → False
3Remove every occurrence of a value
Two pointersEasy#27 Open on LeetCode ↗

Remove every occurrence of a value in place, and return how many elements remain.

nums = [3, 2, 2, 3], val = 3 → 2 # nums[:2] = [2, 2]
4Common elements of two sorted lists

Return the values present in both sorted lists, each as many times as it appears in both.

[1, 2, 2, 3, 5], [2, 2, 4, 5] → [2, 2, 5]
5Compare strings typed with backspaces

In each string # deletes the previous character. Decide whether both strings end up the same.

("ab#c", "ad#c") → True ("a#c", "b") → False
6Count pairs with a sum below target

Count the pairs of different positions whose values add up to less than the target.

nums = [-1, 1, 2, 3, 1], target = 2 → 3
7Two products that best fit a budget
Two pointersEasyVariation of #167 ↗

Pick two products whose total price is as large as possible without exceeding the budget. Return the total, or −1 if no pair fits.

prices = [34, 23, 1, 24, 75, 33, 54, 8], budget = 60 → 58 # 34 + 24

Searching for a value 8

On unsorted data you search by walking once and remembering the best so far — that is O(n), and you cannot do better. On sorted data one template solves almost everything: find the first position where a condition becomes true. It is called lower bound. The search insert position, first and last occurrence, counting occurrences, the closest value and "the first day revenue reached the target" are all this template with a different question on top.

Template
def lower_bound(nums, target):
    """The first index whose value is >= target."""
    left, right = 0, len(nums)       # the answer is somewhere in [left, right]
    while left < right:
        mid = (left + right) // 2
        if nums[mid] < target:       # mid is too small:
            left = mid + 1           #   the answer is to the right
        else:                        # mid might be the answer:
            right = mid              #   keep it in range
    return left

Why right = len(nums) and not len(nums) - 1? Because "there is no such position" is a valid answer — the value is larger than everything. Keeping that slot in range is what makes the template work without special cases.

0123456
1120300455610M89010401310cum[3]=610 < 1000 → L = M + 1
21203004556108901040M1310cum[5]=1040 >= 1000 → R = M
3120300455610890M10401310cum[4]=890 < 1000 → L = M + 1
412030045561089010401310L == R = 5 ✓

Searching for the first day cumulative revenue reached 1000. Each row halves the range. M is the middle; when it is too small, everything to its left is ruled out too.

QuestionThrough lower bound
Where should the target be inserted?lower_bound(nums, target)
First occurrence of the targeti = lower_bound(nums, target); nums[i] == target?
Last occurrence of the targetlower_bound(nums, target + 1) - 1
How many times does it occur?lower_bound(nums, t + 1) - lower_bound(nums, t)
First day a growing metric reached a goallower_bound(cumulative, goal)
8Position of the maximum without max()
Searching for a valueEasyInterview classic

Return the index of the largest value — the first one if it repeats — without using max.

[3, 9, 2, 9, 1] → 1 [] → -1
9The second largest distinct value
Searching for a valueEasyVariation of #414 ↗

Find the second largest distinct value in one pass, or None if there is none.

[5, 3, 9, 9, 7] → 7 [4, 4] → None
10First and last position in a sorted list
Searching for a valueMedium#34 Open on LeetCode ↗

Find the first and last index of a value in a sorted list in O(log n), or [-1, -1].

[5, 7, 7, 8, 8, 10], 8 → [3, 4] 6 → [-1, -1]
11Count occurrences in a sorted list
Searching for a valueEasyVariation of #34 ↗

Count how many times a value appears in a sorted list, faster than walking through it.

[1, 2, 2, 2, 3], 2 → 3 4 → 0
12Integer square root
Searching for a valueEasy#69 Open on LeetCode ↗

Return the integer part of the square root of x, without ** or the math module.

8 → 2 16 → 4
13First day revenue reached the goal
Searching for a valueEasyVariation of #278 ↗

Cumulative revenue by day only grows. Find the first day (index) on which it reached the goal, or −1.

cumulative = [120, 300, 455, 610, 890, 1040, 1310], goal = 1000 → 5
14The value closest to a target
Searching for a valueEasyVariation of #658 ↗

In a sorted list, return the value closest to the target. On a tie, return the smaller one.

[1, 3, 8, 10, 15], 12 → 10 [1, 3, 8, 10, 15], 9 → 8
15The peak of a mountain
Searching for a valueMedium#852 Open on LeetCode ↗

The values strictly rise and then strictly fall. Find the index of the top in O(log n).

[0, 2, 5, 9, 6, 3, 1] → 3

Sliding window 3

A window is a contiguous stretch you move along the data. Two kinds: a fixed window of k elements (add the one entering, drop the one leaving), and a variable window that grows on the right and shrinks on the left while a condition holds. The second needs non-negative numbers: with negatives, shrinking no longer makes the sum smaller.

Template
left = 0
window = 0
for right, x in enumerate(nums):
    window += x                    # 1. grow on the right
    while condition_is_met(window):
        record_answer(left, right) # 2. note the answer
        window -= nums[left]       # 3. shrink from the left
        left += 1
16A repeat within k positions
Sliding windowEasy#219 Open on LeetCode ↗

Decide whether two equal values sit at most k positions apart.

[1, 2, 3, 1], k = 3 → True [1, 2, 3, 1, 2, 3], k = 2 → False
17Shortest stretch reaching a sum
Sliding windowMedium#209 Open on LeetCode ↗

Find the length of the shortest contiguous stretch whose sum is at least the target. Numbers are positive. Return 0 if there is none.

target = 7, nums = [2, 3, 1, 2, 4, 3] → 2 # [4, 3]
18Longest streak of daily growth
Sliding windowEasyVariation of #674 ↗

Given daily values, find the longest run of consecutive days in which each day was higher than the day before.

[100, 120, 150, 140, 160, 170, 180] → 4 # 140 → 180
Nine step-by-step walkthroughs

One key problem per pattern, taken apart in the order you would reason at an interview: the task, the obvious solution and why it is slow, the insight that makes it fast, the code, and a step-by-step trace generated by actually running it.

1Two Sum
Arrays & hashingEasy#1 Open on LeetCode ↗
The task

Given a list of numbers and a target, return the positions of the two numbers that add up to the target. Exactly one such pair exists, and you may not use the same element twice.

nums = [3, 8, 11, 5, 4], target = 9 → [3, 4] # 5 + 4
The obvious approach

Check every pair with two nested loops. It works, but for n numbers that is about n²/2 comparisons — 50 million for 10,000 elements.

The key insight
For each number x we already know exactly which partner we need: target − x. So instead of searching for it, we remember every number we have passed in a dictionary and simply ask "have I seen my partner?" — an instant lookup.
Step by step
#ixneed = target − xseenneed in seen?
1036{}—
2181{3:0}—
3211-2{3:0, 8:1}—
4354{3:0, 8:1, 11:2}—
5445{3:0, 8:1, 11:2, 5:3}✓ → [3, 4]

Read row 5: when we reach 4, its partner is 5 — and 5 is already in seen at position 3. One pass, done.

The solution
ComplexityO(n) time · O(n) memory
Where people slip
Adding the number to seen before checking for its partner. With target 10 and the number 5, the code would pair 5 with itself.
Where this shows up in analytics
This is exactly how a hash JOIN works in a database: build a lookup from one table, then walk the other once. It is also how you reconcile two payments that should net to zero.
2Container With Most Water
Two pointersMedium#11 Open on LeetCode ↗
The task

Each number is the height of a vertical wall. Pick two walls so that, together with the floor, they hold the most water. The water level is set by the lower wall, and the width is the distance between them.

height = [1, 8, 6, 2, 5, 4, 8, 3, 7] → 49 # min(8, 7) × (8 − 1)
The obvious approach

Try every pair of walls — O(n²). Correct, but slow, and it ignores a property of the problem that lets us discard most pairs.

The key insight
Start with the widest container: one pointer at each end. Now ask which wall to move. The water is limited by the lower wall. Moving the taller one can only make things worse — the width shrinks while the limit stays the same. So we always move the lower wall inward, and never miss the best answer.
Step by step
012345678
11L86254837Rarea=8 best=8 L→
218L6254837Rarea=49 best=49 ←R
318L625483R7area=18 best=49 ←R
418L62548R37area=40 best=49 ←R
518L6254R837area=16 best=49 ←R
618L625R4837area=15 best=49 ←R
718L62R54837area=4 best=49 ←R
818L6R254837area=6 best=49 ←R

Each row is one step. L and R are the pointers, the shaded cells are the current container. On step 2 the area reaches 49, and nothing later beats it.

The solution
ComplexityO(n) time · O(1) memory
Where people slip
Moving the taller wall, or moving both at once. Both can skip the optimal pair.
Where this shows up in analytics
Two pointers on sorted data is how you match two ordered lists in one pass — for example, pairing each event with the nearest session boundary without a nested loop.
3Longest Substring Without Repeating Characters
Sliding windowMedium#3 Open on LeetCode ↗
The task

Find the length of the longest stretch of a string in which no character appears twice.

s = "abcabcbb" → 3 # "abc"
The obvious approach

Check every substring and test whether its characters are unique — O(n³), or O(n²) with care.

The key insight
Keep a window that is always valid: no repeats inside. Extend it one character at a time to the right. When the new character already sits inside the window, jump the left edge just past its previous position. The window never moves backwards, so each character is handled once.
Step by step
01234567
1a▼bcabcbblen=1 best=1
2ab▼cabcbblen=2 best=2
3abc▼abcbblen=3 best=3
4abca▼bcbblen=3 best=3 a in window → start=1
5abcab▼cbblen=3 best=3 b in window → start=2
6abcabc▼bblen=3 best=3 c in window → start=3
7abcabcb▼blen=2 best=3 b in window → start=5
8abcabcbb▼len=1 best=3 b in window → start=7

The shaded cells are the window, ▼ marks the new character. On step 4 the second a arrives, so the window jumps forward and stays repeat-free.

The solution
ComplexityO(n) time · O(k) memory
Where people slip
Forgetting the check last_seen[ch] >= start. Without it, a character seen before the window would wrongly pull the left edge backwards.
Where this shows up in analytics
A sliding window is every rolling metric you compute: 7-day active users, a 30-minute session, a moving average. Same idea — add what enters, drop what leaves.
4Valid Parentheses
The task

A string contains only brackets of three kinds. Decide whether every bracket is closed by the same kind, in the correct order.

"{[()]}" → True "{[(])}" → False "{[()]}(" → False
The obvious approach

Repeatedly delete adjacent pairs like () until nothing changes, then check whether the string is empty. It works, but each pass rescans the whole string — O(n²).

The key insight
The bracket that must close next is always the most recently opened one. That "last in, first out" rule is exactly what a stack does: push each opening bracket, and on a closing one pop the top and check that it matches.
Step by step
#charactionstack
1{push {{
2[push [{[
3(push ({[(
4)pop ( ✓{[
5]pop [ ✓{
6}pop { ✓∅
7(push ((
8·stack == [] ?( → False

The first six characters pair up perfectly and the stack empties. Then a lone ( arrives and is never closed — the stack is not empty at the end, so the answer is False.

The solution
ComplexityO(n) time · O(n) memory
Where people slip
Two edge cases: a closing bracket when the stack is empty (crashes on pop), and opening brackets left over at the end. Both must return False.
Where this shows up in analytics
Stacks validate any nested structure: JSON and XML, nested funnel steps, or undo history in a product. Whenever "the last thing opened closes first", reach for a stack.
5Binary Search
Binary searchEasy#704 Open on LeetCode ↗
The task

Find the position of a value in a sorted list, or return −1 if it is not there.

nums = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91], target = 23 → 5
The obvious approach

Scan from the start until you find it — O(n). It ignores the one thing the task gives you for free: the list is sorted.

The key insight
Look at the middle element. Because the list is sorted, one comparison tells you which half the target must be in — and you can throw the other half away. Each step halves the search space, so a million elements take at most 20 steps.
Step by step
0123456789
12L581216M2338567291Rnums[4]=16 < 23 L = M + 1
2258121623L3856M7291Rnums[7]=56 > 23 R = M − 1
3258121623M38R567291nums[5]=23 = target ✓

L, M and R are left, middle and right. The shaded area is what is still being searched — watch it halve on every row until the middle lands on 23.

The solution
ComplexityO(log n) time · O(1) memory
Where people slip
Off-by-one errors: writing left < right instead of <= skips the last candidate, and left = mid instead of mid + 1 loops forever.
Where this shows up in analytics
Binary search is how a database index finds a row without scanning the table — and how you find the smallest sample size that reaches a target power without trying every value.
6Maximum Subarray
The task

Find the contiguous stretch of numbers with the largest possible sum, and return that sum.

nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4] → 6 # [4, -1, 2, 1]
The obvious approach

Sum every possible stretch — O(n²) even with a running total, O(n³) without one.

The key insight
Walk once and keep the best sum of a stretch ending here. At each number there is one decision: extend the previous stretch, or start fresh from this number. If the previous stretch is negative, it can only drag you down, so you drop it. This is Kadane's algorithm.
Step by step
#xcurrent + xdecisioncurrentbest
1-2—start-2-2
21-1restart11 ✓
3-3-2extend-21
442restart44 ✓
5-13extend34
625extend55 ✓
716extend66 ✓
8-51extend16
945extend56

Look at the decision column (extend the stretch, or restart it). On row 2 the running sum was −2, so starting fresh from 1 is better. On row 4 it restarts again — a stretch summing to −2 cannot help the 4. From there the stretch only extends, and best reaches 6 on row 7.

The solution
ComplexityO(n) time · O(1) memory
Where people slip
Starting best at 0. If every number is negative, the answer is the largest negative number, not 0.
Where this shows up in analytics
This finds the most profitable run in a P&L series, or the strongest consecutive period of a campaign — any time you need the best contiguous stretch of a metric.
7Product of Array Except Self
Prefix sumsMedium#238 Open on LeetCode ↗
The task

For every position, return the product of all the other numbers — without using division.

nums = [1, 2, 3, 4] → [24, 12, 8, 6]
The obvious approach

For each position, multiply everything else — O(n²). Division would be O(n), but it is forbidden and breaks on zeros anyway.

The key insight
The product of everything except position i is (everything to the left) × (everything to the right). One pass left-to-right collects the left products; a second pass right-to-left multiplies in the right products. Two passes, no division.
Step by step
#passinums[i]multiplieranswer
1→01prefix=1[1, 1, 1, 1]
2→12prefix=1[1, 1, 1, 1]
3→23prefix=2[1, 1, 2, 1]
4→34prefix=6[1, 1, 2, 6]
5←34suffix=1[1, 1, 2, 6]
6←23suffix=4[1, 1, 8, 6]
7←12suffix=12[1, 12, 8, 6]
8←01suffix=24[24, 12, 8, 6] ✓

Rows marked → are the first pass: each slot gets the product of everything before it. Rows marked ← are the second: each slot is multiplied by everything after it. The last row is the answer.

The solution
ComplexityO(n) time · O(1) extra memory
Where people slip
Reaching for division. Besides being forbidden, a single zero in the array makes it divide by zero.
Where this shows up in analytics
Prefix and suffix accumulation is how you compute cumulative revenue, running retention and "share of total up to this point" in a single pass — the same thing a window function does in SQL.
8Merge Intervals
The task

Given a list of time ranges, merge all the ones that overlap and return the result.

[[1,3], [8,10], [2,6], [15,18], [9,11]] → [[1,6], [8,11], [15,18]]
The obvious approach

Compare every interval with every other and merge repeatedly until nothing changes — O(n²) or worse.

The key insight
Sort by start time first. Once sorted, an interval can only overlap the one directly before it. Walk through: if the next interval starts before the current one ends, stretch the current end; otherwise begin a new block.
Step by step
sorted(intervals, key=start)
[1, 3]
[2, 6]
[8, 10]
[9, 11]
[15, 18]
merged
[1, 6][8, 11][15, 18]
1357911131517

Top: the intervals after sorting. Bottom: the result. [1,3] and [2,6] overlap and fuse into [1,6]; [8,10] and [9,11] fuse into [8,11]; [15,18] stands alone.

The solution
ComplexityO(n log n) time — the sort dominates
Where people slip
Setting the merged end to the new interval's end instead of the maximum of the two. An interval fully inside another would shrink the block.
Where this shows up in analytics
This is how you compute total active time from overlapping sessions, or a customer's real subscription coverage when plans overlap — the same logic as gap-and-island problems in SQL.
9Climbing Stairs
Dynamic programmingEasy#70 Open on LeetCode ↗
The task

You climb a staircase of n steps, taking 1 or 2 steps at a time. In how many different ways can you reach the top?

n = 3 → 3 # 1+1+1, 1+2, 2+1
The obvious approach

Recursion: ways(n) = ways(n−1) + ways(n−2). Correct, but it recomputes the same values again and again — exponential time, about 1.6ⁿ calls.

The key insight
To stand on step k, your last move came from step k−1 or step k−2. So ways(k) = ways(k−1) + ways(k−2). Compute it from the bottom up and remember each answer once. That is the whole idea of dynamic programming: solve small subproblems, reuse them.
Step by step
#step khowways[k]
11base case1
22base case2
33ways[2] + ways[1] = 2 + 13
44ways[3] + ways[2] = 3 + 25
55ways[4] + ways[3] = 5 + 38
66ways[5] + ways[4] = 8 + 513
77ways[6] + ways[5] = 13 + 821 ✓

Each row is built from the two above it. Step 7 has 21 ways — reached in 7 steps instead of the dozens of repeated calls plain recursion would make.

The solution
ComplexityO(n) time · O(1) memory
Where people slip
Wrong base cases. With 1 step there is 1 way, with 2 steps there are 2. Getting these wrong shifts every answer that follows.
Where this shows up in analytics
Dynamic programming is behind sequence alignment, optimal budget allocation across channels, and counting paths through a funnel — any problem where the best answer is built from smaller best answers.
Eighteen problems to practise

Two per pattern. Try each one before opening the hint, and open the hint before the solution — the struggle is where the learning happens. The tests at the bottom of every problem are the same ones the solution was checked against, so you can paste them into your own file.

Topic
Level
1Contains Duplicate
Arrays & hashingEasy#217 Open on LeetCode ↗

Return True if any value appears at least twice in the list, and False if every value is distinct.

[1, 2, 3, 1] → True [1, 2, 3, 4] → False
2Group Anagrams
Arrays & hashingMedium#49 Open on LeetCode ↗

Group words that are made of exactly the same letters, in any order.

["eat","tea","tan","ate","nat","bat"] → [["eat","tea","ate"], ["tan","nat"], ["bat"]]
3Valid Palindrome

Ignoring case and every non-alphanumeric character, decide whether a string reads the same forwards and backwards.

"A man, a plan, a canal: Panama" → True "race a car" → False
43Sum
Two pointersMedium#15 Open on LeetCode ↗

Find every unique triple of numbers that sums to zero. The same triple must not appear twice.

[-1, 0, 1, 2, -1, -4] → [[-1, -1, 2], [-1, 0, 1]]
5Best Time to Buy and Sell Stock
Sliding windowEasy#121 Open on LeetCode ↗

Given daily prices, choose one day to buy and a later day to sell for the biggest profit. If no profit is possible, return 0.

[7, 1, 5, 3, 6, 4] → 5 # 6 − 1 [7, 6, 4, 3, 1] → 0
6Maximum Average Subarray I
Sliding windowEasy#643 Open on LeetCode ↗

Find the contiguous stretch of exactly k numbers with the highest average, and return that average.

nums = [1, 12, -5, -6, 50, 3], k = 4 → 12.75
7Daily Temperatures

For each day, return how many days you must wait for a warmer temperature. If it never gets warmer, return 0 for that day.

[73, 74, 75, 71, 69, 72, 76, 73] → [1, 1, 4, 2, 1, 1, 0, 0]
8Min Stack

Design a stack that supports push, pop, top, and returning the current minimum — every operation in constant time.

push(-2), push(0), push(-3) → get_min() = -3; pop() → get_min() = -2
9Search Insert Position
Binary searchEasy#35 Open on LeetCode ↗

In a sorted list, return the index of the target if it exists, or the index where it would be inserted to keep the list sorted.

[1, 3, 5, 6], 5 → 2 [1, 3, 5, 6], 2 → 1 [1, 3, 5, 6], 7 → 4
10Find Minimum in Rotated Sorted Array
Binary searchMedium#153 Open on LeetCode ↗

A sorted list of distinct numbers was rotated at an unknown point. Find its smallest value in O(log n).

[3, 4, 5, 1, 2] → 1 [4, 5, 6, 7, 0, 1, 2] → 0
11Jump Game

Each number is the maximum jump length from that position. Starting at the first index, can you reach the last one?

[2, 3, 1, 1, 4] → True [3, 2, 1, 0, 4] → False
12Best Time to Buy and Sell Stock II

Same daily prices, but now you may buy and sell as many times as you like (holding at most one share at a time). Return the maximum profit.

[7, 1, 5, 3, 6, 4] → 7 # (5−1) + (6−3)
13Range Sum Query — Immutable

Prepare a list so that you can answer many questions of the form "sum from position i to j" instantly.

nums = [-2, 0, 3, -5, 2, -1]; sum_range(0, 2) → 1; sum_range(2, 5) → -1
14Subarray Sum Equals K
Prefix sumsMedium#560 Open on LeetCode ↗

Count how many contiguous stretches of the list add up to exactly k. Numbers may be negative.

[1, 1, 1], k = 2 → 2 [1, 2, 3], k = 3 → 2
15Insert Interval

A list of non-overlapping intervals is sorted by start. Insert a new interval, merging wherever needed, and keep the result sorted.

[[1,3], [6,9]] + [2,5] → [[1,5], [6,9]]
16Non-overlapping Intervals

Return the minimum number of intervals to remove so that the rest do not overlap. Touching at an endpoint is allowed.

[[1,2], [2,3], [3,4], [1,3]] → 1 # − [1,3]
17House Robber
Dynamic programmingMedium#198 Open on LeetCode ↗

Pick numbers from a row to get the largest total, with one rule: you may never take two neighbours.

[2, 7, 9, 3, 1] → 12 # 2 + 9 + 1
18Coin Change
Dynamic programmingMedium#322 Open on LeetCode ↗

Given coin denominations, find the fewest coins that make up an amount, or return −1 if it cannot be done.

coins = [1, 2, 5], amount = 11 → 3 # 5 + 5 + 1
How to practise so it sticks

Solving a hundred problems once teaches less than solving thirty properly. A few habits make the difference.

  • Learn the pattern, not the answer. After each problem, write one sentence: "this was a sliding window because…". That sentence is what you will recall at the interview.
  • Give yourself 25 minutes. If you are stuck after that, open the hint. Still stuck after 10 more — read the solution, then solve it again from scratch the next day.
  • Talk out loud. Practise explaining your reasoning as you go. At an interview, silent coding is the most common way to lose points you actually earned.
  • Revisit after a week. Spaced repetition beats volume. A problem you can still solve seven days later is a problem you have learnt.
  • Always state the complexity. Finish every solution by saying its time and memory cost. Interviewers expect it, and it forces you to check whether there is a faster way.
← Python for Analysts SQL for Analysts Home