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.
- 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.
- 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.
- 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.
- 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.
- Write the codeClear names, small steps, comments where intent is not obvious. Readable code is part of the grade, not decoration.
- 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 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.
- 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.
- 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.
- 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.
- 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.
- 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?
- 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.
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
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 hand | In the code | Words in the task | Family |
|---|---|---|---|
| how many of each I have met | counts = {} | “how many times”, “most frequent”, “anagram” | 1 · Frequencies |
| which run I am in and how long it is | current, count | “in a row”, “consecutive”, “compress”, “streak” | 2 · Runs in a row |
| whom I have already met | seen = set() | “duplicate”, “unique, keep order”, “first repeated”, “pair with a sum” | 3 · Seen before? |
| the full counts — but the answer follows the original order | counts + 2nd loop | “first unique”, “exactly once”, “share of the total” | 4 · Two passes |
| the best value, or the total, so far | best, total | “largest”, “sum”, “best profit”, “longest” | 5 · Running result |
| how much there should be and how much there is | expected - actual | “missing”, “lost”, “appears twice”, “numbers 1 to n” | 6 · Expected vs actual |
| two places in the data at once | left, right | “palindrome”, “sorted + pair”, “in place” | Two pointers |
| the data laid out in order | sorted(...) | “median”, “closest”, “largest three” | Sorting first |
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.
count_chars(s: str) -> strs = "banana"count_chars("banana") → '1b3a2n'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.
| If… | …then |
|---|---|
| the letter is already on the paper | add a mark: its count + 1 |
| the letter is new | write it down with 0, then add a mark — counts.get(ch, 0) + 1 does both in one go |
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.
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
| # | ch | counts.get(ch, 0) + 1 | counts |
|---|---|---|---|
| 1 | b | 0 + 1 = 1 | {'b': 1} |
| 2 | a | 0 + 1 = 1 | {'b': 1, 'a': 1} |
| 3 | n | 0 + 1 = 1 | {'b': 1, 'a': 1, 'n': 1} |
| 4 | a | 1 + 1 = 2 | {'b': 1, 'a': 2, 'n': 1} |
| 5 | n | 1 + 1 = 2 | {'b': 1, 'a': 2, 'n': 2} |
| 6 | a | 2 + 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.
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())
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”.
Build the table once — t = tally(s), the same loop as above — and every question becomes a different last line. Only the END changes.
| Question | What changes | Real 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 character | max(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") == t | tally("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 |
abab: the answer must be 2a2b. If it should be 1a1b1a1b, you are in family 2.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.
compress(s: str) -> strs = "aaabccdd"compress("aaabccdd") → '3a1b2c2d'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.
current and count. Before the loop: the first letter and 1.| If… | …then |
|---|---|
| the letter equals the current one | the run continues: count + 1 |
| the letter is different | the run is over: write count + letter, then start a new run — current = this letter, count = 1 |
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.
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)
| # | ch | ch == current? | action | written |
|---|---|---|---|---|
| start | a | — | current = 'a', count = 1 | '' |
| 2 | a | True | count = 2 | '' |
| 3 | a | True | count = 3 | '' |
| 4 | b | False | write "3a"; current = 'b', count = 1 | '3a' |
| 5 | c | False | write "1b"; current = 'c', count = 1 | '3a1b' |
| 6 | c | True | count = 2 | '3a1b' |
| 7 | d | False | write "2c"; current = 'd', count = 1 | '3a1b2c' |
| 8 | d | True | count = 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.
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
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.
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
i | j | s[i:j] | j − i | written |
|---|---|---|---|---|
| 0 | 3 | 'aaa' | 3 | '3a' |
| 3 | 4 | 'b' | 1 | '3a1b' |
| 4 | 6 | 'cc' | 2 | '3a1b2c' |
| 6 | 8 | 'dd' | 2 | '3a1b2c2d' |
The state — the current run and its length — stays the same. What changes is what you do when a run ends:
| Question | What changes | Real result |
|---|---|---|
| Letter first: a3b1c2d2 | write f"{current}{count}" instead of f"{count}{current}" | compress_letter_first("aaabccdd")→ 'a3b1c2d2' |
| Skip the 1s | write the count only if it is above 1 | compress_skip_ones("aaabccdd")→ '3ab2c2d' |
| Use it only if shorter | at the end compare lengths and keep the shorter string | compress_if_shorter("abcd")→ 'abcd' |
| Length of the longest run | instead of writing a run, update a record — family 5 | longest_run("aaabccdd")→ 3 |
| Decode back | the state is the number read so far; a letter closes it | decompress("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
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
abab: here it gives four runs, 1a1b1a1b.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.
dedup_keep_order(items: list) -> listitems = [3, 1, 3, 2, 1]dedup_keep_order([3, 1, 3, 2, 1]) → [3, 1, 2]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.
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.| If… | …then |
|---|---|
| x is not in seen | first meeting: add it to seen and to result |
| x is in seen | a repeat: skip it — or, if the task is about repeats, this is the moment to answer |
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.
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
x | x in seen? | action | seen | result |
|---|---|---|---|---|
3 | False | take ✓ | [3] | [3] |
1 | False | take ✓ | [1, 3] | [3, 1] |
3 | True | skip | [1, 3] | [3, 1] |
2 | False | take ✓ | [1, 2, 3] | [3, 1, 2] |
1 | True | skip | [1, 2, 3] | [3, 1, 2] |
list(set([3, 1, 3, 2, 1]))
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.
The set stays; what changes is how you react to a repeat:
| Question | What changes | Real result |
|---|---|---|
| Is there any duplicate? | on a repeat, return True at once | has_duplicate([3, 1, 3, 2, 1])→ True |
| The first value that repeats | on a repeat, return that value | first_repeated([3, 1, 3, 2, 1])→ 3 |
| How many distinct values? | the size of seen at the end | len(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
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.
first_uniq_char(s: str) -> ints = "loveleetcode"first_uniq_char("loveleetcode") → 2Try 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.
| If… | …then |
|---|---|
| pass 1, any letter | its count + 1 |
| pass 2, the count is 1 | this is the first unique letter: return its position |
| pass 2, the count is above 1 | keep walking |
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.
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
i | ch | counts[ch] | == 1? |
|---|---|---|---|
| 0 | l | 2 | False |
| 1 | o | 2 | False |
| 2 | v | 1 | True → 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.
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
“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.
Pass 1 is always the same; pass 2 asks a different question:
| Question | What changes | Real result |
|---|---|---|
| Values that appear exactly once | pass 2 keeps x when its count is 1 | only_unique([3, 1, 3, 2, 1])→ [2] |
| Share of each value | the total is known only after pass 1 | shares(["a", "b", "a", "a"])→ {'a': 0.75, 'b': 0.25} |
| First repeated letter — one pass is enough | decided at the moment of the repeat: family 3 | first_repeated("loveleetcode")→ 'l' |
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.
largest(nums: list) -> number | Nonenums = [4, 9, 2, 7]largest([4, 9, 2, 7]) → 9Look 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.
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.| If… | …then |
|---|---|
| x is bigger than the record | a new record: best = x |
| otherwise | nothing changes |
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).
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
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
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.
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.
max_profit(prices: list) -> numberprices = [7, 1, 5, 3, 6, 4]max_profit([7, 1, 5, 3, 6, 4]) → 5Prices 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
| day | p | cheapest | p − cheapest | best |
|---|---|---|---|---|
| 1 | 7 | 7 | 0 | 0 |
| 2 | 1 | 1 | 0 | 0 |
| 3 | 5 | 1 | 4 | 4 |
| 4 | 3 | 1 | 2 | 4 |
| 5 | 6 | 1 | 5 | 5 |
| 6 | 4 | 1 | 3 | 5 |
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.
The idea “carry the result along” covers totals, averages and several records at once:
| Question | What changes | Real result |
|---|---|---|
| The average | a running total and a count; divide at the end | running_mean([4, 9, 2, 7])→ 5.5 |
| The second largest distinct value | two records: first and second; a new leader pushes the old one down | second_largest([4, 9, 2, 7])→ 7 |
| The longest run of equal letters | the current run (family 2) plus a record | longest_run("aaabccdd")→ 3 |
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.
missing_number(nums: list) -> intnums = [3, 0, 1, 4, 6, 2]missing_sum([3, 0, 1, 4, 6, 2]) → 5The 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.
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
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
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
| Way | What you hold | Time | Extra memory | Real result |
|---|---|---|---|---|
| A · sort, find the gap | the sorted copy | O(n log n) | O(n) | missing_sorted([3, 0, 1, 4, 6, 2]) → 5 |
| B · checklist | one box per number | O(n) | O(n) | missing_checklist([3, 0, 1, 4, 6, 2]) → 5 |
| C · totals | two numbers: expected and actual | O(n) | O(1) | missing_sum([3, 0, 1, 4, 6, 2]) → 5 |
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)
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.
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.
| Question | What changes | Real result |
|---|---|---|
| 1..n, one number appears twice — which? | the surplus: actual − expected | find_duplicate([1, 3, 4, 2, 2])→ 2 |
| Every number twice except one | expected “all twice” = 2 × sum of distinct; the shortfall is the lonely one | single_number([4, 1, 2, 1, 2])→ 4 |
| 1..n, one number twice and one missing | two unknowns → two facts: a seen-set finds the twin, the totals give the gap | set_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]
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.
Each script below was run. You see what it should have returned and what it really returned — pick the cause.
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") · expected '3a1b2c2d' · got '3a1b2c'def count_chars(s): counts = {} for ch in s: counts[ch] += 1 return "".join(f"{n}{ch}" for ch, n in counts.items())
count_chars("banana") · expected '1b3a2n' · got KeyError: 'b'def largest(nums): best = 0 for x in nums: if x > best: best = x return best
largest([-3, -1, -2]) · expected -1 · got 0def missing_number(nums): expected = sum(range(len(nums))) return expected - sum(nums)
missing_number([0, 1]) · expected 2 · got 0def first_uniq_char(s): seen = set() for i, ch in enumerate(s): if ch not in seen: return i seen.add(ch) return -1
first_uniq_char("loveleetcode") · expected 2 · got 0def 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 []
two_sum([5, 1, 4], 10) · expected [] · got [0, 0]def median(nums): n = len(nums) mid = n // 2 if n % 2 == 1: return nums[mid] return (nums[mid - 1] + nums[mid]) / 2
median([340, 55, 120, 80, 80]) · expected 80 · got 120def unique_sorted(items): result = list(set(items)) result = result.sort() return result
unique_sorted([3, 1, 3, 2]) · expected [1, 2, 3] · got NoneTrace the code in your head — or on paper, row by row — before you pick.
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"))
counts = {}
for ch in "abca":
counts[ch] = counts.get(ch, 0) + 1
print(counts)seen = set() for x in [2, 5, 1, 5, 2]: if x in seen: print(x) break seen.add(x)
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)
cheapest, best = float("inf"), 0 for p in [7, 6, 4, 3, 1]: cheapest = min(cheapest, p) best = max(best, p - cheapest) print(best)
nums = [0, 1, 2, 3] n = len(nums) print(n * (n + 1) // 2 - sum(nums))
out, digits = [], "" for ch in "12z3a": if ch.isdigit(): digits += ch else: out.append(ch * int(digits)) digits = "" print(len("".join(out)))
One piece of a working solution is replaced by ???. Only one option makes all the tests pass.
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("") → ''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]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]) → 0def 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'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]) → 0Knowing 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.
- Does order matter? "Count the letters" and "count letters in a row" are different tasks.
- What must I remember while walking through the data? A counter, the previous element, the best value so far, what I have already seen.
- When does what I remember change? This moment — "the run broke", "the sum got too big" — is the heart of the algorithm.
- 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 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.
s = "aabbccc"s = "abab"count_chars("aabbccc") → '2a2b3c'count_chars("abab") → '2a2b'- 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. - 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. - 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.
- Build the answer from the dictionary: for every pair write "count + character" and join.
| # | i | ch | counts[ch] = counts.get(ch, 0) + 1 | counts |
|---|---|---|---|---|
| 1 | 0 | a | 1 | {'a': 1} |
| 2 | 1 | b | 1 | {'a': 1, 'b': 1} |
| 3 | 2 | a | 2 | {'a': 2, 'b': 1} |
| 4 | 3 | b | 2 | {'a': 2, 'b': 2} |
| 5 | end | · | · | → "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())
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.
s = "aabbccc"count_chars_sorted(s) → '2a2b3c'count_by_frequency(s) → '3c2a2b'most_frequent_char(s) → 'c'repeated_chars(s) → ['a', 'b', 'c']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()))
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.
s = "abab"s = "aabbcca"count_chars("abab") → '2a2b'compress("abab") → '1a1b1a1b'compress("aabbcca") → '2a2b2c1a'| # | i | s[i] | s[i] == s[i-1]? | count | written |
|---|---|---|---|---|---|
| 1 | 0 | a | — | 1 | "" |
| 2 | 1 | a | True → count += 1 | 2 | "" |
| 3 | 2 | b | False → write "2a" | 1 | "2a" |
| 4 | 3 | b | True → count += 1 | 2 | "2a" |
| 5 | 4 | c | False → write "2b" | 1 | "2a2b" |
| 6 | 5 | c | True → count += 1 | 2 | "2a2b" |
| 7 | 6 | a | False → write "2c" | 1 | "2a2b2c" |
| 8 | end | · | 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.
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.
amounts = [120, 80, 340, 55, 80, 2000]round(mean(amounts), 2) → 445.83median(amounts) → 100.0mode(amounts) → [80]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)
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.
odd = [340, 55, 120, 80, 80]even = [340, 55, 120, 80, 80, 2000]median(odd) → 80median(even) → 100.0n=5 odd → s[2] = 80n=6 even → (s[2] + s[3]) / 2 = (80 + 120) / 2 = 100.0Row 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
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.
amounts = [120, 80, 340, 55, 80, 2000]tie = [1, 1, 2, 2, 3]mode(amounts) → [80]mode(tie) → [1, 2]| # | x | counts[x] | c == top? |
|---|---|---|---|
| 1 | 120 | 1 | — |
| 2 | 80 | 2 | ✓ 2 == 2 |
| 3 | 340 | 1 | — |
| 4 | 55 | 1 | — |
| 5 | 2000 | 1 | — |
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)
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(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
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".
s = "mississippi"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))
| # | char | count | key = (−count, char) | place |
|---|---|---|---|---|
| 1 | i | 4 | (-4, "i") | 1 ✓ |
| 2 | s | 4 | (-4, "s") | 2 |
| 3 | p | 2 | (-2, "p") | 3 |
| 4 | m | 1 | (-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.
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.
nums = [5, 2, 4, 6, 1, 3]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
a[:1] → sorted ✓insert 2 at index 0insert 4 at index 1insert 6 at index 3insert 1 at index 0insert 3 at index 2The shaded cells are the part that is already sorted — it grows by one on every row. ▲ marks where the new element landed.
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.
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.
nums = [1, 3, 4, 6, 8, 11]target = 10two_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 []
1 + 11 = 12 > 10 → R −= 11 + 8 = 9 < 10 → L += 13 + 8 = 11 > 10 → R −= 13 + 6 = 9 < 10 → L += 14 + 6 = 10 = target ✓Each row throws away one number that provably cannot be in the answer. Five steps instead of fifteen pair checks.
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.
nums = [1, 1, 2, 3, 3, 3, 4]remove_duplicates(nums) → 4nums[: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
a[F]=1 == a[S] → skipa[F]=2 != a[S] → S += 1, a[S] = 2a[F]=3 != a[S] → S += 1, a[S] = 3a[F]=3 == a[S] → skipa[F]=3 == a[S] → skipa[F]=4 != a[S] → S += 1, a[S] = 4The 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.
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.
items = [3, 1, 3, 2, 1]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]| # | x | x in seen? | seen | result |
|---|---|---|---|---|
| 1 | 3 | False → take ✓ | [3] | [3] |
| 2 | 1 | False → take ✓ | [1, 3] | [3, 1] |
| 3 | 3 | True → skip | [1, 3] | [3, 1] |
| 4 | 2 | False → take ✓ | [1, 2, 3] | [3, 1, 2] |
| 5 | 1 | True → 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 question | Tool |
|---|---|
| Without duplicates, original order kept | seen + result / list(dict.fromkeys(x)) |
| Without duplicates, sorted | sorted(set(x)) |
| Only values that appear exactly once | counts[x] == 1 |
| Which values repeat | counts[x] > 1 |
| In place, in a sorted array | slow / fast |
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.
s = "abc"t = "ahbgdc"is_subsequence(s, t) → Truedef 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)
| If the task says… | Shape |
|---|---|
| a sorted array and a pair with a target sum; a palindrome; reverse | A |
| "in place", "without extra memory", remove or move elements | B |
| two sorted lists; "merge", "is a subsequence", "common elements" | C |
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.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".
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.
| Complexity | What it looks like in code | Example |
|---|---|---|
O(1) | A fixed number of steps, regardless of size | Reading a dict value by key |
O(log n) | The search space halves at every step | Binary search in a sorted array |
O(n) | One pass over the data | Sum of a list, finding the maximum |
O(n log n) | Sorting, or log n work for each element | sorted() |
O(n²) | A loop inside a loop over the same data | Comparing every pair |
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.
def two_sum_brute(nums, target): for i in range(len(nums)): for j in range(i + 1, len(nums)): # every pair if nums[i] + nums[j] == target: return [i, j] return [] def two_sum(nums, target): seen = {} # value -> index for i, x in enumerate(nums): need = target - x if need in seen: return [seen[need], i] seen[x] = i return []
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… | Try | Typical complexity |
|---|---|---|
| "find a pair / a duplicate / have we seen this?" | Arrays & hashing | O(n) |
| a sorted array, a pair or triple with a target sum, a palindrome | Two pointers | O(n) |
| "longest / shortest contiguous subarray or substring" | Sliding window | O(n) |
| matching brackets, "the next greater element", undo | Stack | O(n) |
| a sorted array and "find", or "the smallest value that works" | Binary search | O(log n) |
| "maximum / minimum", where the best local choice is always safe | Greedy | O(n) |
| "sum of a range", "subarrays that add up to k" | Prefix sums | O(n) |
| meetings, bookings, time ranges that overlap | Intervals | O(n log n) |
| "how many ways", "the best total", choices that build on earlier ones | Dynamic programming | O(n) |
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
Print how many times each character appears in total, as "count + character", in the order characters first appear.
"aabbccc" → "2a2b3c" "abab" → "2a2b" "" → ""- Totals, not neighbours:
ababmust give2a2b. So the order of letters in the string does not matter — only how many of each. - Keep a dictionary
character → countand update it in one pass withcounts.get(ch, 0) + 1. - The dictionary already keeps first-appearance order, so just walk its items and write "count + 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())
abab → 2a2b.count_chars("aabbccc") count_chars("abab") count_chars("")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Counter is a dictionary built for counting: one line instead of the loop. It also keeps the order in which characters first appeared, so the answer is identical.
from collections import Counter def count_chars_counter(s): return "".join(f"{n}{ch}" for ch, n in Counter(s).items())
O(n·k)Short and readable, but s.count() rescans the whole string for every distinct character. Fine for a word, slow for a long text.
def count_chars_count(s): unique = dict.fromkeys(s) # distinct chars, in first-appearance order return "".join(f"{s.count(ch)}{ch}" for ch in unique)
The same totals, but listed in alphabetical order of the characters.
"cabbac" → "2a2b2c" "banana" → "3a1b2n"- The tally is identical to the previous task. Only the output order changes.
- Walk the keys through
sorted(counts)instead of in insertion order.
def count_chars_sorted(s): """'cabbac' -> '2a2b2c': the same totals, but in alphabetical order.""" counts = {} for ch in s: counts[ch] = counts.get(ch, 0) + 1 return "".join(f"{counts[ch]}{ch}" for ch in sorted(counts))
count_chars_sorted("cabbac") count_chars_sorted("banana")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n + k log k)The loop becomes Counter; the sorting step is unchanged — sorted() over a dictionary sorts its keys.
from collections import Counter def count_chars_sorted_counter(s): counts = Counter(s) return "".join(f"{counts[ch]}{ch}" for ch in sorted(counts))
O(n log n)No dictionary at all: after sorting, each character forms one run, and groupby hands you the runs. The same tool as in “runs in a row” below, applied to sorted data.
from itertools import groupby def count_chars_sorted_groupby(s): # sorting puts equal chars side by side; groupby then measures each run return "".join(f"{len(list(run))}{ch}" for ch, run in groupby(sorted(s)))
Print the counts with the most frequent character first. On a tie, keep the order of first appearance.
"aabbccc" → "3c2a2b" "abab" → "2a2b"- Count as usual, then sort the pairs by count, descending:
key=lambda kv: -kv[1]. - Ties sort themselves out: Python's
sorted()is stable — equal elements keep the order they had, which here is first appearance.
def count_by_frequency(s): """'aabbccc' -> '3c2a2b': most frequent first; ties keep first-appearance order.""" counts = {} for ch in s: counts[ch] = counts.get(ch, 0) + 1 ranked = sorted(counts.items(), key=lambda kv: -kv[1]) # sorted() is stable return "".join(f"{n}{ch}" for ch, n in ranked)
count_by_frequency("aabbccc") count_by_frequency("abab")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n + k log k)most_common() is exactly “sort by count, highest first”, and equal counts keep their first-appearance order — the same tie rule as the main solution.
from collections import Counter def count_by_frequency_counter(s): return "".join(f"{n}{ch}" for ch, n in Counter(s).most_common())
O(n·k)The key is a pair: minus the count (so bigger counts come first) and the first position (so ties go to the earlier character). Very explicit, but count() and index() rescan the string.
def count_by_frequency_key(s): # the tie rule is spelled out: more frequent first, then the one seen earlier order = sorted(set(s), key=lambda ch: (-s.count(ch), s.index(ch))) return "".join(f"{s.count(ch)}{ch}" for ch in order)
Count the letters of a phrase, treating upper and lower case as the same and ignoring spaces and punctuation.
"Hello, World!" → "1h1e3l2o1w1r1d"- Normalise first:
text.lower()makes "H" and "h" the same character. - Inside the loop, skip anything that is not a letter with
ch.isalpha(), then count as usual.
def count_letters(text): """'Hello, World!' -> '1h1e3l2o1w1r1d': ignore case, spaces and punctuation.""" counts = {} for ch in text.lower(): if ch.isalpha(): # skip everything that is not a letter counts[ch] = counts.get(ch, 0) + 1 return "".join(f"{n}{ch}" for ch, n in counts.items())
count_letters("Hello, World!")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)The condition moves into a generator expression: Counter only ever sees letters.
from collections import Counter def count_letters_counter(text): letters = Counter(ch for ch in text.lower() if ch.isalpha()) return "".join(f"{n}{ch}" for ch, n in letters.items())
O(n)filter keeps only the characters for which str.isalpha returns True — the same check, without writing the if.
from collections import Counter def count_letters_filter(text): letters = Counter(filter(str.isalpha, text.lower())) return "".join(f"{n}{ch}" for ch, n in letters.items())
Return the character that appears most often. On a tie, return the one that appears first.
"abracadabra" → "a" "aabb" → "a"- Count, then pick the key with the largest value:
max(counts, key=counts.get). - On a tie
maxkeeps the first maximum it meets, and the dictionary is in first-appearance order — so the tie rule is satisfied for free.
def most_frequent_char(s): counts = {} for ch in s: counts[ch] = counts.get(ch, 0) + 1 return max(counts, key=counts.get) # on a tie: the one that appeared first
key=counts.get means "compare the keys by their counts" — a pattern worth remembering.most_frequent_char("abracadabra") most_frequent_char("aabb")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)most_common(1) returns a list with one (char, count) pair; [0][0] takes the character. On a tie it returns the one seen first.
from collections import Counter def most_frequent_char_counter(s): return Counter(s).most_common(1)[0][0]
O(n²)A popular one-liner: “the character whose count is largest”. It gives the same answer, but calls s.count() for every character of s — quadratic. Good to recognise, bad on long strings.
def most_frequent_char_max(s): return max(s, key=s.count)
List every character that appears more than once, each one time, in order of first appearance.
"programming" → ["r", "g", "m"]- Count, then keep only the pairs whose count is above one.
def repeated_chars(s): counts = {} for ch in s: counts[ch] = counts.get(ch, 0) + 1 return [ch for ch, n in counts.items() if n > 1]
repeated_chars("programming")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Counting is delegated to Counter; the filter n > 1 is the same.
from collections import Counter def repeated_chars_counter(s): return [ch for ch, n in Counter(s).items() if n > 1]
O(n·k)dict.fromkeys(s) lists each character once, in first-appearance order; count() then checks each one. No explicit counting, but repeated passes over the string.
def repeated_chars_count(s): return [ch for ch in dict.fromkeys(s) if s.count(ch) > 1]
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}- The counting is identical — only the unit changes. Split the sentence into words with
.split(). - Lower-case the text first, so "The" and "the" count as one word.
def word_frequency(text): counts = {} for word in text.lower().split(): counts[word] = counts.get(word, 0) + 1 return counts
word_frequency("The cat and the hat")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Counter does the counting; dict() only turns it back into a plain dictionary so the printout looks the same.
from collections import Counter def word_frequency_counter(text): return dict(Counter(text.lower().split()))
O(n)defaultdict creates the missing key with a default value (int() is 0), so counts[word] += 1 works without .get(word, 0).
from collections import defaultdict def word_frequency_defaultdict(text): counts = defaultdict(int) # a missing word starts at 0 automatically for word in text.lower().split(): counts[word] += 1 return dict(counts)
Count only neighbouring repeats (run-length encoding). Here abab is four separate runs.
"aabbccc" → "2a2b3c" "abab" → "1a1b1a1b" "aabbcca" → "2a2b2c1a"- Order matters now, so a dictionary does not help. Compare each character with the previous one.
- Same as previous — add one. Different — write the finished run and reset the counter to 1.
- After the loop the last run is still unwritten — write it.
def compress(s): """'aabbccc' -> '2a2b3c': count each run of identical neighbours.""" if not s: return "" parts = [] count = 1 for i in range(1, len(s)): if s[i] == s[i - 1]: # same as the previous char: the run goes on count += 1 else: # the run broke: write it down, start a new one parts.append(f"{count}{s[i - 1]}") count = 1 parts.append(f"{count}{s[-1]}") # the loop never writes the LAST run return "".join(parts)
aabbccc, different for abab. That is why the test on abab matters.compress("aabbccc") compress("abab") compress("aabbcca")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)groupby does by itself what the loop does by hand: it splits the string into runs of equal neighbours. No special case for the last run.
from itertools import groupby def compress_groupby(s): # groupby cuts the string into runs of identical neighbours return "".join(f"{len(list(run))}{ch}" for ch, run in groupby(s))
O(n)The pattern describes a run directly. Compact, but harder to read for most people — use it if your team is comfortable with regex.
import re def compress_regex(s): # (.)\1* = one character followed by any number of copies of itself return "".join(f"{len(m.group())}{m.group(1)}" for m in re.finditer(r"(.)\1*", s, re.S))
Decode a string of "count + character" back. Counts can have more than one digit.
"2a2b3c" → "aabbccc" "12z" → "zzzzzzzzzzzz"- A number can be "12", so collect digits until a letter appears.
- When the letter comes, repeat it as many times as the collected number says, then clear the number.
def decompress(code): """'2a2b3c' -> 'aabbccc'. Counts may have several digits: '12a'.""" out = [] digits = "" for ch in code: if ch.isdigit(): digits += ch # keep collecting the number else: out.append(ch * int(digits)) digits = "" return "".join(out)
decompress("2a2b3c") decompress("12z")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)One pattern splits the code into (count, character) pairs, multi-digit counts included; the rest is string multiplication.
import re def decompress_regex(code): # findall returns (number, letter) pairs; \d+ also catches counts like 12 return "".join(ch * int(n) for n, ch in re.findall(r"(\d+)(\D)", code))
Return the position of the first character that appears exactly once, or −1 if there is none.
"leetcode" → 0 "loveleetcode" → 2 "aabb" → -1- You cannot know a character is unique until you have seen the whole string. So one pass is not enough.
- Pass 1: count everything. Pass 2: walk again in the original order and return the first character with count 1.
def first_uniq_char(s): counts = Counter(s) # pass 1: how many of each for i, ch in enumerate(s): # pass 2: the first one seen exactly once if counts[ch] == 1: return i return -1
first_uniq_char("leetcode") first_uniq_char("loveleetcode") first_uniq_char("aabb")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)The same two passes with a plain dictionary — this is what Counter does under the hood. Useful when an interviewer asks you not to import anything.
def first_uniq_char_dict(s): counts = {} for ch in s: counts[ch] = counts.get(ch, 0) + 1 for i, ch in enumerate(s): if counts[ch] == 1: return i return -1
O(n²)next() returns the first item the generator produces, or the default -1. Elegant, but count() rescans the string for every position.
def first_uniq_char_next(s): return next((i for i, ch in enumerate(s) if s.count(ch) == 1), -1)
Decide whether two strings consist of exactly the same letters, the same number of times.
("anagram", "nagaram") → True ("rat", "car") → False- Different lengths — answer False immediately. A cheap check first.
- Then compare the letter counts. Two equal tallies mean an anagram. Sorting both and comparing also works, at O(n log n).
def is_anagram(s, t): if len(s) != len(t): return False return Counter(s) == Counter(t)
is_anagram("anagram", "nagaram") is_anagram("rat", "car")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n log n)Anagrams sort into the same sequence of letters. No counting and no imports; O(n log n) instead of O(n) — irrelevant for words, noticeable for millions of characters.
def is_anagram_sorted(s, t): return sorted(s) == sorted(t)
O(n)One dictionary instead of two: letters of s add, letters of t subtract. If every balance returns to zero, the letters match.
def is_anagram_balance(s, t): if len(s) != len(t): return False balance = {} for a, b in zip(s, t): balance[a] = balance.get(a, 0) + 1 # +1 for each letter of s balance[b] = balance.get(b, 0) - 1 # -1 for each letter of t return all(v == 0 for v in balance.values())
Reverse the letters of every word, keeping the words in their original order.
"Let's take LeetCode contest" → "s'teL ekat edoCteeL tsetnoc"- Split into words by spaces, reverse each word on its own, glue back with spaces.
- In Python
word[::-1]reverses a string; at an interview they may ask you to do it with two pointers instead.
def reverse_words(s): return " ".join(word[::-1] for word in s.split(" "))
reverse_words("Let's take LeetCode contest")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Reverse the whole sentence — every word is now reversed but the words are in the wrong order — then put the word order back.
def reverse_words_twice(s): # reverse everything: each word comes out reversed, but the word order is wrong — fix the order return " ".join(s[::-1].split(" ")[::-1])
O(n)reversed() reads the word from the end; "".join glues the characters back. Same as [::-1], just more explicit.
def reverse_words_reversed(s): return " ".join("".join(reversed(word)) for word in s.split(" "))
Find the longest beginning shared by all the words.
["flower", "flow", "flight"] → "fl" ["dog", "car"] → ""- The answer can never be longer than the first word, so start with the whole first word as a candidate.
- For each next word, shorten the candidate from the end until the word starts with it. An empty prefix always fits, so the loop always stops.
def longest_common_prefix(words): if not words: return "" prefix = words[0] for word in words[1:]: while not word.startswith(prefix): prefix = prefix[:-1] # shorten until it fits ("" always fits) return prefix
longest_common_prefix(["flower", "flow", "flight"]) longest_common_prefix(["dog", "car"])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n·m)zip(*words) reads all words in parallel, one column of letters at a time, and stops at the shortest word. The prefix ends at the first column with different letters.
def longest_common_prefix_columns(words): prefix = [] for column in zip(*words): # 1st letters of all words, then 2nd letters, … if len(set(column)) != 1: # the letters in this column differ: stop break prefix.append(column[0]) return "".join(prefix)
O(n·m)After alphabetical sorting, the first and the last word differ the most; whatever they share, every word in between shares too. Only two words are compared letter by letter.
def longest_common_prefix_minmax(words): if not words: return "" lo, hi = min(words), max(words) # alphabetically first and last for i, ch in enumerate(lo): if ch != hi[i]: return lo[:i] return lo
O(n·m)os.path.commonprefix already does this — letter by letter, not by folder, despite its name. Worth knowing; in an interview, write the logic yourself.
import os def longest_common_prefix_stdlib(words): return os.path.commonprefix(words) # works letter by letter, despite the name
Duplicates 5
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]list(set(x))removes repeats but loses the order — a set does not remember it. So a set alone is not enough.- Use two structures: a set
seento answer "already taken?" instantly, and a listresultto keep the order. - Walk once: if the value is not in
seen, add it to both.
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
list(dict.fromkeys(x)) — a dictionary keeps insertion order and drops repeated keys.dedup_keep_order([3, 1, 3, 2, 1]) dedup_keep_order(["b", "a", "b"]) dedup_keep_order([])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Dictionary keys are unique and keep insertion order, so dict.fromkeys does the “seen + result” bookkeeping in one call. The standard idiom in modern Python.
def dedup_keep_order_dict(items): return list(dict.fromkeys(items))
O(n²)Keeps a value only if it did not appear earlier. No extra structures, but every check rescans the beginning of the list. Shown so you recognise it — and avoid it on big data.
def dedup_keep_order_slow(items): return [x for i, x in enumerate(items) if x not in items[:i]]
Return the distinct values in ascending order.
[3, 1, 3, 2, 1] → [1, 2, 3]- Here the original order does not matter, so a set is exactly the right tool:
set(x)drops repeats. - Then
sorted()puts them in order and returns a list.
def unique_sorted(items): return sorted(set(items)) # a set drops repeats but forgets the order
unique_sorted([3, 1, 3, 2, 1])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n log n)Sort first, then keep one value from each run of equal neighbours — the way a database can compute DISTINCT together with ORDER BY.
from itertools import groupby def unique_sorted_groupby(items): # after sorting, equal values stand together: keep one from each run return [value for value, _ in groupby(sorted(items))]
Keep only the values that appear once in the whole list; drop everything that has a repeat.
[3, 1, 3, 2, 1] → [2]- Not the same as removing duplicates: 3 and 1 must disappear completely.
- You cannot decide while reading — a value looks unique until its twin shows up. So count first, then filter: keep
xwherecounts[x] == 1.
def only_unique(items): """Values that appear exactly once: [3, 1, 3, 2, 1] -> [2].""" counts = {} for x in items: counts[x] = counts.get(x, 0) + 1 return [x for x in items if counts[x] == 1]
only_unique([3, 1, 3, 2, 1])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Counter replaces the counting loop; the second pass keeps the original order.
from collections import Counter def only_unique_counter(items): counts = Counter(items) return [x for x in items if counts[x] == 1]
O(n²)Reads like the task itself, but items.count(x) walks the whole list for every element. Fine for ten values, not for a million.
def only_unique_count(items): return [x for x in items if items.count(x) == 1]
List every value that appears more than once, each value one time.
[3, 1, 3, 2, 1] → [3, 1]- Count every value, then keep the keys with a count above one.
def find_duplicates(items): """Which values repeat, each listed once: [3, 1, 3, 2, 1] -> [3, 1].""" counts = {} for x in items: counts[x] = counts.get(x, 0) + 1 return [x for x, n in counts.items() if n > 1]
find_duplicates([3, 1, 3, 2, 1]) find_duplicates([1, 2, 3])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Same idea, the counting is done by Counter. Order: by first appearance.
from collections import Counter def find_duplicates_counter(items): return [x for x, n in Counter(items).items() if n > 1]
O(n²)The generator keeps every repeated value (several times); dict.fromkeys removes the extra copies and keeps the order. Short, but quadratic.
def find_duplicates_count(items): return list(dict.fromkeys(x for x in items if items.count(x) > 1))
Keep the first occurrence of every character, in order.
"banana" → "ban" "mississippi" → "misp"- Exactly the list version — a string is a sequence too. Collect kept characters in a list and join at the end.
def dedup_string(s): """'banana' -> 'ban'.""" seen = set() out = [] for ch in s: if ch not in seen: seen.add(ch) out.append(ch) return "".join(out)
dedup_string("banana") dedup_string("mississippi")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)A string is a sequence of characters, so the list idiom works unchanged: unique keys, insertion order kept, then join.
def dedup_string_dict(s): return "".join(dict.fromkeys(s))
Mean, median, mode 3
Return the arithmetic mean of a list, and None for an empty list.
[120, 80, 340, 55, 80, 2000] → 445.83… [] → None- Sum divided by count —
sum(nums) / len(nums). - Guard the empty list first:
len([])is 0 and division by zero crashes.
def mean(nums): if not nums: return None # an empty list has no mean (and avoids / 0) return sum(nums) / len(nums)
mean([120, 80, 340, 55, 80, 2000]) mean([])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)The standard library has it; the only thing left to you is the empty-list rule. fmean always returns a float and is more accurate on long lists of decimals.
from statistics import fmean def mean_stdlib(nums): return fmean(nums) if nums else None # fmean raises an error on an empty list
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- Sort first. The middle index is
n // 2— integer division, because an index cannot be a fraction. - Odd length: return that element. Even length: average it with the one before it,
s[mid - 1].
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
statistics.median on 300 random lists — identical results.median([340, 55, 120, 80, 80]) median([340, 55, 120, 80, 80, 2000])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n log n)Same rule inside: sort, take the middle or the average of the two middles. Write it by hand in an interview, use the library at work.
from statistics import median as stats_median def median_stdlib(nums): return stats_median(nums) if nums else None
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]- It is the character-counting task again, on numbers: tally every value.
- Find the highest count with
max(counts.values()), then keep every value that has it.
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)
statistics.multimode.mode([120, 80, 340, 55, 80, 2000]) mode([1, 1, 2, 2, 3])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Counter does the counting; the rule for ties — return every value with the top count — stays yours.
from collections import Counter def mode_counter(nums): if not nums: return [] counts = Counter(nums) top = max(counts.values()) return sorted(x for x, n in counts.items() if n == top)
O(n)multimode returns all the most frequent values (an empty list for empty input). Not to be confused with statistics.mode, which returns only one.
from statistics import multimode def mode_stdlib(nums): return sorted(multimode(nums)) # multimode already returns every tied value
Two pointers 6
Reverse a list of characters without creating a new list.
["h","e","l","l","o"] → ["o","l","l","e","h"]- Shape A: one pointer at each end. Swap the two characters, then move both inwards.
- Stop when the pointers meet — after that you would swap everything back.
def reverse_string(chars): left, right = 0, len(chars) - 1 while left < right: chars[left], chars[right] = chars[right], chars[left] left += 1 right -= 1 return chars
reverse_string(list("hello"))
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)The built-in does the same pointer swaps in place. In an interview, mention it — then write the two pointers, because that is what is being tested.
def reverse_string_builtin(chars): chars.reverse() # the same swaps, in place, done in C return chars
O(n) · O(n) memorychars[:] = … replaces the contents of the same list object, so callers see the change. The cost is a temporary copy — O(n) extra memory instead of O(1).
def reverse_string_slice(chars): chars[:] = chars[::-1] # build a reversed copy, write it back into the same list return chars
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]- Sorted means duplicates sit next to each other — a new value is simply one that differs from the last kept value.
- Shape B: fast reads everything, slow marks the end of the clean part. New value → advance slow and copy it there.
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
a = [1, 1, 2, 3, 3, 3, 4]; k = remove_duplicates(a); print(k, a[:k])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n) · O(n) memoryCollect the unique values, then write them to the front. Simpler to read, but it uses O(n) extra memory — the task asks for O(1), which is why the main solution uses two pointers.
def remove_duplicates_dict(nums): unique = list(dict.fromkeys(nums)) # the list is sorted, so this stays sorted nums[:len(unique)] = unique return len(unique)
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]- Turn it around: instead of pushing zeroes back, pull non-zeroes forward.
- Slow points to where the next non-zero belongs. Every non-zero found by fast is swapped there, and slow moves on.
def move_zeroes(nums): slow = 0 # where the next non-zero should go for fast in range(len(nums)): if nums[fast] != 0: nums[slow], nums[fast] = nums[fast], nums[slow] slow += 1 return nums
move_zeroes([0, 1, 0, 3, 12])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n log n)The key is True for zeros and False otherwise, and False sorts first. Python’s sort is stable, so non-zero values keep their order. A one-liner, but O(n log n).
def move_zeroes_sort(nums): nums.sort(key=lambda x: x == 0) # False (non-zero) sorts before True (zero) return nums
O(n) · O(n) memoryNon-zero values in order, then the right number of zeros, written back into the same list. Clear, but uses a temporary copy.
def move_zeroes_rebuild(nums): nums[:] = [x for x in nums if x != 0] + [0] * nums.count(0) return nums
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]- Shape C: one pointer on each list. Filling from the front would overwrite values you still need.
- So fill from the end: compare the largest remaining of each list and put the bigger one into the last free slot.
def merge_sorted(nums1, m, nums2, n): """nums1 has m values followed by n empty slots. Fill from the END.""" i, j, k = m - 1, n - 1, m + n - 1 while j >= 0: if i >= 0 and nums1[i] > nums2[j]: nums1[k] = nums1[i] i -= 1 else: nums1[k] = nums2[j] j -= 1 k -= 1 return nums1
merge_sorted([1, 2, 3, 0, 0, 0], 3, [2, 5, 6], 3)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O((m+n) log(m+n))Ignores that both lists are already sorted, so it does more work — but it is short and correct. A good first answer before optimising.
def merge_sorted_sort(nums1, m, nums2, n): nums1[m:] = nums2 # drop nums2 into the empty slots nums1.sort() return nums1
O(m+n) · O(m+n) memoryheapq.merge walks two sorted sequences with two pointers, exactly like the main solution, but front to back — so it needs a copy of nums1[:m].
import heapq def merge_sorted_heapq(nums1, m, nums2, n): nums1[:] = heapq.merge(nums1[:m], nums2) # merges two sorted sequences in one pass return nums1
Given a sorted list that may contain negatives, return the squares in sorted order.
[-4, -1, 0, 3, 10] → [0, 1, 9, 16, 100]- Squaring breaks the order: −4 becomes 16. But the largest square is always at one of the two ends.
- Shape A: compare the two ends, place the bigger square at the back of the result, move that pointer inwards.
def sorted_squares(nums): n = len(nums) result = [0] * n left, right = 0, n - 1 for pos in range(n - 1, -1, -1): # the biggest square is at one of the ends if abs(nums[left]) > abs(nums[right]): result[pos] = nums[left] ** 2 left += 1 else: result[pos] = nums[right] ** 2 right -= 1 return result
sorted_squares([-4, -1, 0, 3, 10])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n log n)The obvious answer: correct, one line, O(n log n). The two-pointer version uses the fact that the input is already sorted to get O(n).
def sorted_squares_sort(nums): return sorted(x * x for x in nums)
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- Shape C: a pointer on the short string marks how much of it is already matched.
- Walk through the long string; whenever its letter equals the next needed letter, advance. At the end, check everything was matched.
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)
is_subsequence("abc", "ahbgdc") is_subsequence("axc", "ahbgdc")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)ch in it consumes t up to and including the first match, so the next search starts where the last one stopped — the pointer is hidden inside the iterator.
def is_subsequence_iter(s, t): it = iter(t) return all(ch in it for ch in s) # "in" moves the iterator forward and never goes back
O(n)The pointer is explicit again, but the scanning is done by find(), which accepts a start position.
def is_subsequence_find(s, t): pos = -1 for ch in s: pos = t.find(ch, pos + 1) # next occurrence after the previous match if pos == -1: return False return True
Sorting 3
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- The obvious guess — the three largest — fails on negatives: two negatives multiply into a large positive.
- After sorting only two candidates remain: the three largest, or the two smallest (most negative) times the largest. Take the bigger one.
def maximum_product(nums): nums = sorted(nums) # either the three largest, or two most negative times the largest return max(nums[-1] * nums[-2] * nums[-3], nums[0] * nums[1] * nums[-1])
maximum_product([1, 2, 3, 4]) maximum_product([-10, -10, 1, 3, 2])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Only five numbers matter: the three largest and the two smallest. heapq finds them in one pass without sorting everything.
import heapq def maximum_product_heapq(nums): a, b, c = heapq.nlargest(3, nums) x, y = heapq.nsmallest(2, nums) return max(a * b * c, x * y * a)
O(n³)Tries every triple. Far too slow for real input, but it cannot be wrong — the perfect reference to test the clever solution against.
from itertools import combinations def maximum_product_brute(nums): return max(a * b * c for a, b, c in combinations(nums, 3))
Count how many positions differ between the list and its sorted version.
[1, 1, 4, 2, 1, 3] → 3- Make the "expected" order with
sorted()— a new list, the original stays as it was. - Walk both side by side with
zipand count the positions where they disagree.
def height_checker(heights): expected = sorted(heights) return sum(1 for a, b in zip(heights, expected) if a != b)
sorted() versus .sort() matters: sorting in place would destroy the list you compare against.height_checker([1, 1, 4, 2, 1, 3])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n log n)In Python True counts as 1 and False as 0, so summing the comparisons counts the mismatches directly.
def height_checker_bools(heights): return sum(a != b for a, b in zip(heights, sorted(heights)))
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- A triangle exists when the longest side is shorter than the sum of the other two.
- Sort from largest to smallest and test neighbouring triples from the top. The first triple that works is the answer.
def largest_perimeter(nums): nums = sorted(nums, reverse=True) for i in range(len(nums) - 2): if nums[i] < nums[i + 1] + nums[i + 2]: # triangle rule for the longest side return nums[i] + nums[i + 1] + nums[i + 2] return 0
largest_perimeter([2, 1, 2]) largest_perimeter([1, 2, 1, 10])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n log n)zip of three shifted copies gives every window of three neighbours. Among valid triangles the one highest in the sorted list has the largest perimeter, so max() finds it.
def largest_perimeter_zip(nums): nums = sorted(nums) triples = zip(nums, nums[1:], nums[2:]) # every three neighbours: a <= b <= c return max((a + b + c for a, b, c in triples if a + b > c), default=0)
O(n³)Checks every triple, not just neighbours. Shows why neighbours are enough: it never finds anything bigger.
from itertools import combinations def largest_perimeter_brute(nums): return max((a + b + c for a, b, c in combinations(sorted(nums), 3) if a + b > c), default=0)
Counting and sets 4
One value appears more than half the time. Find it.
[3, 2, 3] → 3 [2, 2, 1, 1, 1, 2, 2] → 2- Count each value, then take the one with the biggest count.
- A follow-up you may hear: "without extra memory?" — the Boyer–Moore vote keeps one candidate and a counter that goes up and down.
def majority_element(nums): counts = Counter(nums) return max(counts, key=counts.get)
majority_element([3, 2, 3]) majority_element([2, 2, 1, 1, 1, 2, 2])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n) · O(1) memoryEach different value cancels one vote of the candidate. The majority value has more than half the votes, so it survives every cancellation. No dictionary: O(1) memory.
def majority_element_vote(nums): candidate, votes = None, 0 for x in nums: if votes == 0: candidate = x # nobody is ahead: x becomes the candidate votes += 1 if x == candidate else -1 return candidate
O(n log n)A value that fills more than half of the sorted list must cover its middle position.
def majority_element_sorted(nums): return sorted(nums)[len(nums) // 2] # more than half the list covers the middle
Can the note be built from the magazine's letters, using each letter at most once?
("aa", "aab") → True ("aa", "ab") → False- Count the letters available in the magazine.
- Spend them one by one on the note. If a letter runs out, the answer is False.
def can_construct(note, magazine): have = Counter(magazine) for ch in note: if have[ch] == 0: return False have[ch] -= 1 return True
can_construct("aa", "aab") can_construct("aa", "ab")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Counter subtraction drops anything that falls to zero or below, so what remains is exactly the letters the magazine cannot supply. Empty means possible.
from collections import Counter def can_construct_subtract(note, magazine): missing = Counter(note) - Counter(magazine) # subtraction keeps only positive counts return not missing
O(n)Since Python 3.10, <= on two Counters means “every count on the left is at most the count on the right” — the task in one comparison.
from collections import Counter def can_construct_subset(note, magazine): return Counter(note) <= Counter(magazine) # Python 3.10+: every count fits
Every number appears twice except one. Find that one.
[4, 1, 2, 1, 2] → 4- Use a set as a "currently unpaired" list: seen once — add it; seen again — remove it.
- Pairs cancel out, so exactly one number is left in the set.
def single_number(nums): seen = set() for x in nums: if x in seen: seen.remove(x) # second time: the pair cancels out else: seen.add(x) return seen.pop()
a ^ a = 0, so XOR-ing all numbers leaves only the single one. Worth mentioning.single_number([4, 1, 2, 1, 2])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n) · O(1) memoryXOR of a number with itself is 0, and the order does not matter, so every pair cancels and only the single number remains. The classic O(1)-memory answer.
def single_number_xor(nums): result = 0 for x in nums: result ^= x # x ^ x == 0 and 0 ^ y == y: pairs cancel out return result
O(n)If every distinct value appeared twice, the total would be 2 × sum(set). The difference from the real total is the value that appears once.
def single_number_sums(nums): return 2 * sum(set(nums)) - sum(nums) # every value twice, minus what is actually there
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- You know what the full total should be: 0 + 1 + … + n = n·(n+1)/2.
- Subtract the actual total. The difference is exactly the missing number.
def missing_number(nums): n = len(nums) return n * (n + 1) // 2 - sum(nums) # expected total minus actual total
missing_number([3, 0, 1]) missing_number([9, 6, 4, 2, 3, 5, 7, 0, 1])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n) · O(n) memoryAll the numbers that should be there, minus the ones that are. Reads exactly like the task, at the cost of two sets.
def missing_number_set(nums): return (set(range(len(nums) + 1)) - set(nums)).pop()
O(n) · O(1) memoryThe same cancellation trick as in “single number”: XOR every index and every value; each present number meets itself and vanishes.
def missing_number_xor(nums): result = len(nums) for i, x in enumerate(nums): result ^= i ^ x # every number that is present cancels out return result
Loops and arithmetic 3
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"]- The trap is the order of the checks: 15 is divisible by 3, so checking 3 first would print "Fizz".
- Check the strictest rule — divisible by 15 — first, then 3, then 5, then the number itself.
def fizz_buzz(n): out = [] for i in range(1, n + 1): if i % 15 == 0: # check the strictest rule FIRST out.append("FizzBuzz") elif i % 3 == 0: out.append("Fizz") elif i % 5 == 0: out.append("Buzz") else: out.append(str(i)) return out
fizz_buzz(15)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)No separate 15 rule: “Fizz” and “Buzz” are glued together when both apply. Easy to extend — a new rule for 7 is one more term.
def fizz_buzz_concat(n): out = [] for i in range(1, n + 1): word = "Fizz" * (i % 3 == 0) + "Buzz" * (i % 5 == 0) # "" * False == "" out.append(word or str(i)) # an empty word falls back to the number return out
Decide whether a number reads the same backwards — without converting it to a string.
121 → True -121 → False 10 → Falsex % 10gives the last digit,x // 10removes it. Repeat to peel digits off one by one.- Build the reversed number as you go:
reversed_num * 10 + digit. Compare it with the original.
def is_palindrome_number(x): if x < 0: return False original, reversed_num = x, 0 while x > 0: reversed_num = reversed_num * 10 + x % 10 # take the last digit x //= 10 # drop the last digit return original == reversed_num
is_palindrome_number(121) is_palindrome_number(-121) is_palindrome_number(10)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(d)The usual everyday answer. LeetCode’s follow-up asks to solve it without converting to a string — that is what the main solution does.
def is_palindrome_number_str(x): return str(x) == str(x)[::-1] # "-121" reversed is "121-": negatives fail by themselves
Convert a Roman numeral to an integer.
"III" → 3 "IX" → 9 "MCMXCIV" → 1994- Normally you add the value of each symbol. The exception: a smaller symbol before a bigger one (IV, IX, CM) is subtracted.
- So at each symbol, peek at the next one. Smaller than the next — subtract; otherwise — add.
def roman_to_int(s): values = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000} total = 0 for i, ch in enumerate(s): if i + 1 < len(s) and values[ch] < values[s[i + 1]]: total -= values[ch] # IV, IX, XL…: a smaller value before a bigger one else: total += values[ch] return total
roman_to_int("III") roman_to_int("IX") roman_to_int("MCMXCIV")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Reading backwards, you always know the neighbour on the right already, so there is no look-ahead and no index arithmetic.
def roman_to_int_backwards(s): values = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000} total, prev = 0, 0 for ch in reversed(s): # read from the right v = values[ch] total += v if v >= prev else -v # smaller than its right neighbour: subtract prev = v return total
O(n)Turn the six subtractive pairs into plain additions first; after that the answer is just a sum. A nice example of changing the data so the logic becomes trivial.
def roman_to_int_replace(s): for pair, spelled in (("IV", "IIII"), ("IX", "VIIII"), ("XL", "XXXX"), ("XC", "LXXXX"), ("CD", "CCCC"), ("CM", "DCCCC")): s = s.replace(pair, spelled) # remove subtraction: IV -> IIII values = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000} return sum(values[ch] for ch in s)
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.
# 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
Reverse the order of the vowels in a string, leaving every other character where it is.
"hello" → "holle" "leetcode" → "leotcede"- This is "reverse a string" with a filter: shape A, but each pointer first skips characters that are not vowels.
- Only when both pointers stand on vowels do you swap and move both. Copy the string into a list first — strings cannot be changed in place.
def reverse_vowels(s): vowels = set("aeiouAEIOU") chars = list(s) # strings cannot be changed in place left, right = 0, len(chars) - 1 while left < right: if chars[left] not in vowels: left += 1 elif chars[right] not in vowels: right -= 1 else: # both point at vowels: swap them chars[left], chars[right] = chars[right], chars[left] left += 1 right -= 1 return "".join(chars)
reverse_vowels("hello") reverse_vowels("leetcode")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n) · O(n) memoryCollect the vowels, then rebuild the string and take vowels from the end of the stack. Each vowel slot receives the vowels in reverse order — no pointers needed.
def reverse_vowels_stack(s): vowels = set("aeiouAEIOU") stack = [ch for ch in s if ch in vowels] # the vowels, left to right return "".join(stack.pop() if ch in vowels else ch for ch in s)
Decide whether a string can become a palindrome by deleting at most one character.
"aba" → True "abca" → True # − "c" "abc" → False- Check from both ends as usual. While characters match, nothing needs deleting.
- At the first mismatch you have exactly two options: skip the left character or the right one. Check whether either remaining middle is a palindrome.
def valid_palindrome_ii(s): """Can s become a palindrome after deleting at most one character?""" def is_pal(lo, hi): while lo < hi: if s[lo] != s[hi]: return False lo += 1 hi -= 1 return True left, right = 0, len(s) - 1 while left < right: if s[left] != s[right]: # first mismatch: try skipping either side return is_pal(left + 1, right) or is_pal(left, right - 1) left += 1 right -= 1 return True
valid_palindrome_ii("aba") valid_palindrome_ii("abca") valid_palindrome_ii("abc")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Same idea, but the “is the rest a palindrome?” helper becomes a slice compared with its reverse. Shorter; costs a copy of the middle part.
def valid_palindrome_ii_slices(s): left, right = 0, len(s) - 1 while left < right: if s[left] != s[right]: skip_left, skip_right = s[left + 1:right + 1], s[left:right] return skip_left == skip_left[::-1] or skip_right == skip_right[::-1] left += 1 right -= 1 return True
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]- Shape B. Fast reads every element; slow points to where the next element worth keeping goes.
- Not equal to
val→ copy it to the slow position and move slow on. The answer is where slow ended.
def remove_element(nums, val): """Keep every value except val at the front, in place; return how many.""" slow = 0 for fast in range(len(nums)): if nums[fast] != val: nums[slow] = nums[fast] slow += 1 return slow
nums = [3, 2, 2, 3]; k = remove_element(nums, 3); print(k, nums[:k])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n) · O(n) memoryA list comprehension does the filtering; the slice assignment puts the result where the task expects it. Uses a temporary list, unlike the in-place two pointers.
def remove_element_rebuild(nums, val): kept = [x for x in nums if x != val] nums[:len(kept)] = kept # write the kept values to the front return len(kept)
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]- Shape C: one pointer on each list. Compare the two current values.
- The smaller one cannot appear in the other list any more — move its pointer. Equal — it is common: take it and move both.
def intersect_sorted(a, b): """Common values of two sorted lists, with repeats: [1,2,2,3] & [2,2,4] -> [2,2].""" i = j = 0 result = [] while i < len(a) and j < len(b): if a[i] < b[j]: i += 1 # a[i] is too small to appear in b elif a[i] > b[j]: j += 1 else: # equal: it is common, take it, move both result.append(a[i]) i += 1 j += 1 return result
intersect_sorted([1, 2, 2, 3, 5], [2, 2, 4, 5])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n + m)& on two Counters keeps the minimum count of each value — exactly “common, with repeats”. Works on unsorted lists too; the final sorted() restores the order.
from collections import Counter def intersect_sorted_counter(a, b): common = Counter(a) & Counter(b) # & keeps the smaller count of each value return sorted(common.elements())
In each string # deletes the previous character. Decide whether both strings end up the same.
("ab#c", "ad#c") → True ("a#c", "b") → False- Simulate typing: keep a list as the "screen". A letter is appended,
#removes the last one if there is any. - Build both screens and compare. The follow-up "with O(1) memory" walks both strings from the end with two pointers, counting pending deletions.
def backspace_compare(s, t): """'#' deletes the previous character. Compare the typed results.""" def typed(text): out = [] for ch in text: if ch == "#": if out: out.pop() else: out.append(ch) return out return typed(s) == typed(t)
backspace_compare("ab#c", "ad#c") backspace_compare("a#c", "b")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n) · O(1) memoryFrom the right, a “#” simply says how many of the next characters to skip. zip_longest compares both texts lazily and stops at the first difference — no stacks, no copies. This is LeetCode’s O(1)-memory follow-up.
from itertools import zip_longest def backspace_compare_backwards(s, t): def typed_backwards(text): # yields the surviving characters, last to first skip = 0 for ch in reversed(text): if ch == "#": skip += 1 # the next real character will be erased elif skip: skip -= 1 else: yield ch return all(a == b for a, b in zip_longest(typed_backwards(s), typed_backwards(t)))
Count the pairs of different positions whose values add up to less than the target.
nums = [-1, 1, 2, 3, 1], target = 2 → 3- Sort first — pairs do not depend on order, and sorting unlocks shape A.
- If the smallest plus the largest is below the target, then the smallest plus every value in between is too — count
right - leftpairs at once and move left.
def count_pairs_below(nums, target): """How many pairs i < j have nums[i] + nums[j] < target?""" nums = sorted(nums) left, right = 0, len(nums) - 1 count = 0 while left < right: if nums[left] + nums[right] < target: count += right - left # left pairs with EVERY index up to right left += 1 else: right -= 1 return count
count_pairs_below([-1, 1, 2, 3, 1], 2)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n log n)For each number, binary search finds where the too-large partners begin; everything before that point is a valid partner. Same complexity class as sorting, different technique.
from bisect import bisect_left def count_pairs_below_bisect(nums, target): nums = sorted(nums) count = 0 for i, x in enumerate(nums): # partners after i with nums[j] < target - x form a block: count it by position count += bisect_left(nums, target - x, i + 1) - (i + 1) return count
O(n²)Every pair, one by one. The reference that the two clever versions were tested against.
from itertools import combinations def count_pairs_below_brute(nums, target): return sum(1 for a, b in combinations(nums, 2) if a + b < target)
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- This is Two Sum on sorted data, except you look for the best fit instead of an exact match.
- Sum fits → remember it and try a pricier left item. Too expensive → drop the priciest item on the right.
def best_pair_within_budget(prices, budget): """Two products with the largest total that still fits the budget (-1 if none).""" prices = sorted(prices) left, right = 0, len(prices) - 1 best = -1 while left < right: total = prices[left] + prices[right] if total <= budget: best = max(best, total) left += 1 # fits: try a more expensive left item else: right -= 1 # too expensive: drop the priciest item return best
best_pair_within_budget([34, 23, 1, 24, 75, 33, 54, 8], 60) best_pair_within_budget([10, 20, 30], 15)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n²)Tries every pair. default=-1 covers the “nothing fits” case in one place.
from itertools import combinations def best_pair_within_budget_brute(prices, budget): return max((a + b for a, b in combinations(prices, 2) if a + b <= budget), default=-1)
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.
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.
cum[3]=610 < 1000 → L = M + 1cum[5]=1040 >= 1000 → R = Mcum[4]=890 < 1000 → L = M + 1L == 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.
| Question | Through lower bound |
|---|---|
| Where should the target be inserted? | lower_bound(nums, target) |
| First occurrence of the target | i = lower_bound(nums, target); nums[i] == target? |
| Last occurrence of the target | lower_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 goal | lower_bound(cumulative, goal) |
Return the index of the largest value — the first one if it repeats — without using max.
[3, 9, 2, 9, 1] → 1 [] → -1- Assume the first element is the best so far, then walk the rest and replace it whenever you see a larger value.
- Use strictly greater
>: on a tie the earlier position is kept. With>=you would return the last one.
def index_of_max(nums): """Position of the largest value (first one on a tie), without max().""" if not nums: return -1 best_i = 0 for i in range(1, len(nums)): if nums[i] > nums[best_i]: # strictly greater: a tie keeps the first best_i = i return best_i
> versus >= — say out loud which tie rule you chose.index_of_max([3, 9, 2, 9, 1]) index_of_max([])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Two passes, both in C: max() finds the value, index() its first position. The task forbids max() only to test that you can write the loop.
def index_of_max_builtin(nums): return nums.index(max(nums)) if nums else -1
Find the second largest distinct value in one pass, or None if there is none.
[5, 3, 9, 9, 7] → 7 [4, 4] → None- Keep two slots:
firstandsecond. A new leader pushes the old leader down into second place. - A value equal to the leader is not a new second place — that is what "distinct" means. Sorting would also work, but costs O(n log n).
def second_largest(nums): """The second largest DISTINCT value, or None.""" first = second = None for x in nums: if first is None or x > first: first, second = x, first # the old leader drops to second place elif x != first and (second is None or x > second): second = x return second
[9, 9, 7]: a careless solution returns 9.second_largest([5, 3, 9, 9, 7]) second_largest([4, 4])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n log n)set() removes repeats (so [9, 9] does not count twice), sorting does the rest. Easy to get right; the one-pass version avoids the sort.
def second_largest_sorted(nums): distinct = sorted(set(nums)) return distinct[-2] if len(distinct) >= 2 else None
O(n)nlargest(2, …) finds the two biggest distinct values in one pass without sorting everything.
import heapq def second_largest_heapq(nums): top = heapq.nlargest(2, set(nums)) return top[1] if len(top) == 2 else None
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]- The first position is
lower_bound(target). If the value there is not the target, it is absent. - The last position is "one before the first position of anything bigger":
lower_bound(target + 1) - 1.
def lower_bound(nums, target): """The first index whose value is >= target (len(nums) if there is none). Almost every "search in sorted data" question is a variation of this.""" left, right = 0, len(nums) # the answer lies somewhere in [left, right] while left < right: mid = (left + right) // 2 if nums[mid] < target: # mid is too small: the answer is to the right left = mid + 1 else: # mid could be the answer: keep it in range right = mid return left def first_and_last(nums, target): """Positions of the first and last target in a sorted list, or [-1, -1].""" start = lower_bound(nums, target) if start == len(nums) or nums[start] != target: return [-1, -1] end = lower_bound(nums, target + 1) - 1 # first index of the NEXT value, minus one return [start, end]
first_and_last([5, 7, 7, 8, 8, 10], 8) first_and_last([5, 7, 7, 8, 8, 10], 6)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(log n)bisect_left is lower_bound from the standard library; bisect_right is the first position after the last target. Use these at work, write lower_bound in the interview.
from bisect import bisect_left, bisect_right def first_and_last_bisect(nums, target): start = bisect_left(nums, target) # = lower_bound if start == len(nums) or nums[start] != target: return [-1, -1] return [start, bisect_right(nums, target) - 1]
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- All copies of the value sit side by side. Their count is "where the next value starts" minus "where this value starts".
def lower_bound(nums, target): """The first index whose value is >= target (len(nums) if there is none). Almost every "search in sorted data" question is a variation of this.""" left, right = 0, len(nums) # the answer lies somewhere in [left, right] while left < right: mid = (left + right) // 2 if nums[mid] < target: # mid is too small: the answer is to the right left = mid + 1 else: # mid could be the answer: keep it in range right = mid return left def count_in_sorted(nums, target): """How many times target appears in a sorted list — in O(log n).""" return lower_bound(nums, target + 1) - lower_bound(nums, target)
nums.count(x), which is O(n) — on a million sorted rows that is 20 steps instead of a million.count_in_sorted([1, 2, 2, 2, 3], 2) count_in_sorted([1, 2, 2, 2, 3], 4)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(log n)The same formula with the standard library: the block of equal values starts at bisect_left and ends just before bisect_right.
from bisect import bisect_left, bisect_right def count_in_sorted_bisect(nums, target): return bisect_right(nums, target) - bisect_left(nums, target)
O(n)Correct and obvious, but it ignores that the list is sorted and scans all of it. The point of the task is the O(log n) version.
def count_in_sorted_count(nums, target): return nums.count(target)
Return the integer part of the square root of x, without ** or the math module.
8 → 2 16 → 4- There is no list here — you search over the answers 0…x. They are sorted, and "
mid * mid <= x" is true up to a point and false after it. - You want the last value where it is true, so round the middle up — otherwise
left = midnever moves and the loop never ends.
def int_sqrt(x): """Integer part of the square root, without ** or math.""" left, right = 0, x while left < right: mid = (left + right + 1) // 2 # round UP, or the loop never ends if mid * mid <= x: left = mid # mid works: the answer is mid or bigger else: right = mid - 1 return left
int_sqrt(8) int_sqrt(16)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(log x)The exact integer square root for integers of any size. Avoid int(x ** 0.5): floating point can be off by one for very large numbers.
import math def int_sqrt_isqrt(x): return math.isqrt(x)
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- Growing data is sorted data. "Reached the goal" is false, false, …, then true forever — exactly what lower bound finds.
- If lower bound returns the length of the list, the goal was never reached — return −1.
def lower_bound(nums, target): """The first index whose value is >= target (len(nums) if there is none). Almost every "search in sorted data" question is a variation of this.""" left, right = 0, len(nums) # the answer lies somewhere in [left, right] while left < right: mid = (left + right) // 2 if nums[mid] < target: # mid is too small: the answer is to the right left = mid + 1 else: # mid could be the answer: keep it in range right = mid return left def first_day_reaching(cumulative, target): """Cumulative revenue only grows. On which day (index) did it first reach target?""" day = lower_bound(cumulative, target) return day if day < len(cumulative) else -1
first_day_reaching([120, 300, 455, 610, 890, 1040, 1310], 1000) first_day_reaching([120, 300, 455], 1000)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(log n)lower_bound from the standard library; the “-1 if never” rule stays yours.
from bisect import bisect_left def first_day_reaching_bisect(cumulative, target): day = bisect_left(cumulative, target) return day if day < len(cumulative) else -1
O(n)Fine for a month of data. With years of daily data, or the same question asked thousands of times, binary search wins.
def first_day_reaching_scan(cumulative, target): return next((day for day, total in enumerate(cumulative) if total >= target), -1)
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- Lower bound gives the first value ≥ target. The closest value is either that one or the one just before it — nothing else can be closer.
- Handle the edges: if the target is below everything or above everything, only one neighbour exists.
def lower_bound(nums, target): """The first index whose value is >= target (len(nums) if there is none). Almost every "search in sorted data" question is a variation of this.""" left, right = 0, len(nums) # the answer lies somewhere in [left, right] while left < right: mid = (left + right) // 2 if nums[mid] < target: # mid is too small: the answer is to the right left = mid + 1 else: # mid could be the answer: keep it in range right = mid return left def closest_value(nums, target): """The value in a sorted list closest to target (the smaller one on a tie).""" i = lower_bound(nums, target) # first value >= target if i == 0: return nums[0] if i == len(nums): return nums[-1] before, after = nums[i - 1], nums[i] return before if target - before <= after - target else after
<=) decides which one you return.closest_value([1, 3, 8, 10, 15], 12) closest_value([1, 3, 8, 10, 15], 9)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Works on unsorted lists too. The second part of the key encodes the tie rule. O(n) instead of O(log n).
def closest_value_min(nums, target): # the key is a pair: distance first, then the value itself (the smaller wins a tie) return min(nums, key=lambda v: (abs(v - target), v))
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- The list is not sorted, but each position answers a yes/no question: "is the next value bigger?" — yes on the way up, no on the way down.
- Yes, yes, …, no, no — an answer that flips exactly once, so it is again "find the first position where it flips". Still climbing → the peak is to the right; going down → it is here or to the left.
def peak_index(arr): """A list rises then falls. Find the index of the top in O(log n).""" left, right = 0, len(arr) - 1 while left < right: mid = (left + right) // 2 if arr[mid] < arr[mid + 1]: # still climbing: the peak is to the right left = mid + 1 else: # going down: the peak is mid or to the left right = mid return left
peak_index([0, 2, 5, 9, 6, 3, 1])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)The peak is simply the maximum. Correct, but it reads every element; the binary search uses the rise-then-fall shape to read only log n of them.
def peak_index_max(arr): return arr.index(max(arr))
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.
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
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- A fixed window of the last k values, stored in a set so "is it here?" is instant.
- For each new value: if it is already in the window, answer True. Otherwise add it, and drop the value that fell out of range.
def nearby_duplicate(nums, k): """Are there equal values at most k positions apart?""" window = set() # the last k values for i, x in enumerate(nums): if x in window: return True window.add(x) if len(window) > k: window.remove(nums[i - k]) # the value that just left the window return False
nearby_duplicate([1, 2, 3, 1], 3) nearby_duplicate([1, 2, 3, 1, 2, 3], 2)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Instead of keeping a window of values, keep each value’s most recent index and measure the distance. The pattern from Two Sum, with a distance check.
def nearby_duplicate_last_seen(nums, k): last = {} # value -> the last index where we saw it for i, x in enumerate(nums): if x in last and i - last[x] <= k: return True last[x] = i return False
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]- A variable window: grow it on the right until the sum is big enough.
- Then shrink it from the left while it stays big enough, recording the length each time. Each element enters and leaves once — O(n).
def min_subarray_len(target, nums): """Shortest contiguous stretch with a sum >= target (0 if none). Positives only.""" left = 0 window_sum = 0 best = float("inf") for right, x in enumerate(nums): window_sum += x # grow the window to the right while window_sum >= target: # it is big enough: try shrinking from the left best = min(best, right - left + 1) window_sum -= nums[left] left += 1 return 0 if best == float("inf") else best
min_subarray_len(7, [2, 3, 1, 2, 4, 3]) min_subarray_len(100, [1, 2, 3])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n log n)With positive numbers the prefix sums only grow, so for every end you can binary-search the latest start that still gives a big enough sum. Slower than the window, but it combines two patterns you already know.
from bisect import bisect_right from itertools import accumulate def min_subarray_len_prefix(target, nums): prefix = [0, *accumulate(nums)] # sum(nums[i:j]) == prefix[j] - prefix[i] best = float("inf") for j in range(1, len(prefix)): # the latest start i with prefix[i] <= prefix[j] - target i = bisect_right(prefix, prefix[j] - target) - 1 if i >= 0: best = min(best, j - i) return 0 if best == float("inf") else best
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- Compare each day with the previous one — the same move as counting runs in a string.
- Higher → the streak grows. Not higher → the streak restarts at 1. Keep the best length seen.
def longest_growth_streak(values): """Longest run of consecutive days where the value went up.""" if not values: return 0 best = current = 1 for i in range(1, len(values)): if values[i] > values[i - 1]: current += 1 else: current = 1 # the streak broke: start again best = max(best, current) return best
longest_growth_streak([100, 120, 150, 140, 160, 170, 180])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)pairwise turns days into day-to-day changes; groupby finds the runs of “went up”. A streak of n rises covers n + 1 days.
from itertools import groupby, pairwise def longest_growth_streak_groupby(values): if not values: return 0 ups = (b > a for a, b in pairwise(values)) # True for every day that went up runs = [len(list(run)) for up, run in groupby(ups) if up] return max(runs, default=0) + 1 # n rises in a row = n + 1 days
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.
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 + 4Check every pair with two nested loops. It works, but for n numbers that is about n²/2 comparisons — 50 million for 10,000 elements.
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.| # | i | x | need = target − x | seen | need in seen? |
|---|---|---|---|---|---|
| 1 | 0 | 3 | 6 | {} | — |
| 2 | 1 | 8 | 1 | {3:0} | — |
| 3 | 2 | 11 | -2 | {3:0, 8:1} | — |
| 4 | 3 | 5 | 4 | {3:0, 8:1, 11:2} | — |
| 5 | 4 | 4 | 5 | {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.
def two_sum(nums, target): seen = {} # value -> index for i, x in enumerate(nums): need = target - x if need in seen: return [seen[need], i] seen[x] = i return []
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n²)The approach the walkthrough starts from. Write it first if you are stuck — a correct slow answer beats no answer — then say how a dictionary removes the inner loop.
def two_sum_brute(nums, target): for i in range(len(nums)): for j in range(i + 1, len(nums)): if nums[i] + nums[j] == target: return [i, j] return []
O(n log n)Sort the positions by value, then close in from both ends. Slower than the dictionary, but no extra lookup table, and it is the gateway to 3Sum.
def two_sum_two_pointers(nums, target): order = sorted(range(len(nums)), key=nums.__getitem__) # positions, by value left, right = 0, len(order) - 1 while left < right: total = nums[order[left]] + nums[order[right]] if total == target: return sorted([order[left], order[right]]) if total < target: left += 1 else: right -= 1 return []
O(n) time · O(n) memoryseen before checking for its partner. With target 10 and the number 5, the code would pair 5 with itself.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)Try every pair of walls — O(n²). Correct, but slow, and it ignores a property of the problem that lets us discard most pairs.
area=8 best=8 L→area=49 best=49 ←Rarea=18 best=49 ←Rarea=40 best=49 ←Rarea=16 best=49 ←Rarea=15 best=49 ←Rarea=4 best=49 ←Rarea=6 best=49 ←REach 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.
def max_area(height): left, right = 0, len(height) - 1 best = 0 while left < right: h = min(height[left], height[right]) best = max(best, h * (right - left)) if height[left] < height[right]: left += 1 else: right -= 1 return best
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n²)Every pair of walls. It is the reference that proves the greedy “move the shorter wall” rule never skips the best pair.
from itertools import combinations def max_area_brute(height): return max((j - i) * min(height[i], height[j]) for i, j in combinations(range(len(height)), 2))
O(n) time · O(1) memoryFind the length of the longest stretch of a string in which no character appears twice.
s = "abcabcbb" → 3 # "abc"Check every substring and test whether its characters are unique — O(n³), or O(n²) with care.
len=1 best=1len=2 best=2len=3 best=3len=3 best=3 a in window → start=1len=3 best=3 b in window → start=2len=3 best=3 c in window → start=3len=2 best=3 b in window → start=5len=1 best=3 b in window → start=7The 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.
def length_of_longest_substring(s): last_seen = {} # char -> last index start = best = 0 for i, ch in enumerate(s): if ch in last_seen and last_seen[ch] >= start: start = last_seen[ch] + 1 # jump past the repeat last_seen[ch] = i best = max(best, i - start + 1) return best
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)The textbook sliding window: the set holds exactly the characters inside the window. The left edge walks one step at a time instead of jumping — easier to reason about, same O(n).
def length_of_longest_substring_set(s): window = set() left = best = 0 for right, ch in enumerate(s): while ch in window: # shrink from the left until the repeat is gone window.remove(s[left]) left += 1 window.add(ch) best = max(best, right - left + 1) return best
O(n) time · O(k) memorylast_seen[ch] >= start. Without it, a character seen before the window would wrongly pull the left edge backwards.A string contains only brackets of three kinds. Decide whether every bracket is closed by the same kind, in the correct order.
"{[()]}" → True "{[(])}" → False "{[()]}(" → FalseRepeatedly 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²).
| # | char | action | stack |
|---|---|---|---|
| 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.
def is_valid_parentheses(s): pairs = {")": "(", "]": "[", "}": "{"} stack = [] for ch in s: if ch in "([{": stack.append(ch) elif not stack or stack.pop() != pairs[ch]: return False return not stack
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n²)A valid string always has an adjacent pair somewhere; removing it keeps the string valid. Repeat until nothing changes. Very intuitive, but each round rescans the string.
def is_valid_parentheses_replace(s): while "()" in s or "[]" in s or "{}" in s: s = s.replace("()", "").replace("[]", "").replace("{}", "") # peel off innermost pairs return s == ""
O(n) time · O(n) memorypop), and opening brackets left over at the end. Both must return False.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 → 5Scan from the start until you find it — O(n). It ignores the one thing the task gives you for free: the list is sorted.
nums[4]=16 < 23 L = M + 1nums[7]=56 > 23 R = M − 1nums[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.
def binary_search(nums, target): left, right = 0, len(nums) - 1 while left <= right: mid = (left + right) // 2 if nums[mid] == target: return mid if nums[mid] < target: left = mid + 1 else: right = mid - 1 return -1
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(log n)bisect gives the insertion point; one extra check turns it into “found / not found”.
from bisect import bisect_left def binary_search_bisect(nums, target): i = bisect_left(nums, target) return i if i < len(nums) and nums[i] == target else -1
O(log n) time · O(1) memoryleft < right instead of <= skips the last candidate, and left = mid instead of mid + 1 loops forever.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]Sum every possible stretch — O(n²) even with a running total, O(n³) without one.
| # | x | current + x | decision | current | best |
|---|---|---|---|---|---|
| 1 | -2 | — | start | -2 | -2 |
| 2 | 1 | -1 | restart | 1 | 1 ✓ |
| 3 | -3 | -2 | extend | -2 | 1 |
| 4 | 4 | 2 | restart | 4 | 4 ✓ |
| 5 | -1 | 3 | extend | 3 | 4 |
| 6 | 2 | 5 | extend | 5 | 5 ✓ |
| 7 | 1 | 6 | extend | 6 | 6 ✓ |
| 8 | -5 | 1 | extend | 1 | 6 |
| 9 | 4 | 5 | extend | 5 | 6 |
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.
def max_subarray(nums): current = best = nums[0] for x in nums[1:]: current = max(x, current + x) # extend or start over best = max(best, current) return best
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)A subarray sum is the difference of two prefix sums, so the task becomes “Best Time to Buy and Sell Stock” on the running total: subtract the lowest earlier value.
def max_subarray_prefix(nums): best = float("-inf") running = lowest = 0 # running prefix sum and its minimum so far for x in nums: running += x best = max(best, running - lowest) # best sum ending here lowest = min(lowest, running) return best
O(n) time · O(1) memorybest at 0. If every number is negative, the answer is the largest negative number, not 0.For every position, return the product of all the other numbers — without using division.
nums = [1, 2, 3, 4] → [24, 12, 8, 6]For each position, multiply everything else — O(n²). Division would be O(n), but it is forbidden and breaks on zeros anyway.
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.| # | pass | i | nums[i] | multiplier | answer |
|---|---|---|---|---|---|
| 1 | → | 0 | 1 | prefix=1 | [1, 1, 1, 1] |
| 2 | → | 1 | 2 | prefix=1 | [1, 1, 1, 1] |
| 3 | → | 2 | 3 | prefix=2 | [1, 1, 2, 1] |
| 4 | → | 3 | 4 | prefix=6 | [1, 1, 2, 6] |
| 5 | ← | 3 | 4 | suffix=1 | [1, 1, 2, 6] |
| 6 | ← | 2 | 3 | suffix=4 | [1, 1, 8, 6] |
| 7 | ← | 1 | 2 | suffix=12 | [1, 12, 8, 6] |
| 8 | ← | 0 | 1 | suffix=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.
def product_except_self(nums): n = len(nums) answer = [1] * n prefix = 1 for i in range(n): # left-to-right: product of everything before i answer[i] = prefix prefix *= nums[i] suffix = 1 for i in range(n - 1, -1, -1): # right-to-left: times everything after i answer[i] *= suffix suffix *= nums[i] return answer
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n²)Multiplies everything except position i, for every i. The tempting shortcut “total product ÷ nums[i]” breaks on zeros — which is why the task forbids division.
from math import prod def product_except_self_brute(nums): return [prod(nums[:i] + nums[i + 1:]) for i in range(len(nums))]
O(n) time · O(1) extra memoryGiven 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]]Compare every interval with every other and merge repeatedly until nothing changes — O(n²) or worse.
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.
def merge_intervals(intervals): intervals = sorted(intervals, key=lambda iv: iv[0]) merged = [] for start, end in intervals: if merged and start <= merged[-1][1]: merged[-1][1] = max(merged[-1][1], end) else: merged.append([start, end]) return merged
O(n log n) time — the sort dominatesYou 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+1Recursion: ways(n) = ways(n−1) + ways(n−2). Correct, but it recomputes the same values again and again — exponential time, about 1.6ⁿ calls.
| # | step k | how | ways[k] |
|---|---|---|---|
| 1 | 1 | base case | 1 |
| 2 | 2 | base case | 2 |
| 3 | 3 | ways[2] + ways[1] = 2 + 1 | 3 |
| 4 | 4 | ways[3] + ways[2] = 3 + 2 | 5 |
| 5 | 5 | ways[4] + ways[3] = 5 + 3 | 8 |
| 6 | 6 | ways[5] + ways[4] = 8 + 5 | 13 |
| 7 | 7 | ways[6] + ways[5] = 13 + 8 | 21 ✓ |
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.
def climb_stairs(n): if n <= 2: return n prev, curr = 1, 2 for _ in range(3, n + 1): prev, curr = curr, prev + curr return curr
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Says the idea directly: the ways to reach step n are the ways to reach n − 1 plus the ways to reach n − 2. lru_cache stores each answer, turning exponential recursion into O(n).
from functools import lru_cache @lru_cache(maxsize=None) def climb_stairs_memo(n): if n <= 2: return n return climb_stairs_memo(n - 1) + climb_stairs_memo(n - 2) # last step was 1 or 2
O(n) time · O(1) memoryTwo 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.
No problems match this filter.
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] → Falsedef contains_duplicate(nums): return len(set(nums)) != len(nums)
contains_duplicate([1, 2, 3, 1]) contains_duplicate([1, 2, 3, 4]) contains_duplicate([])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Same complexity, but it stops as soon as it finds a repeat, while len(set(...)) always reads the whole list. Matters on a huge list with an early duplicate.
def contains_duplicate_early_exit(nums): seen = set() for x in nums: if x in seen: return True # stop at the first repeat seen.add(x) return False
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"]]def group_anagrams(words): groups = defaultdict(list) for w in words: groups["".join(sorted(w))].append(w) return list(groups.values())
groupby does.group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"]) group_anagrams([""])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n·k log k)The same grouping with a plain dictionary: setdefault returns the existing list or creates an empty one.
def group_anagrams_setdefault(words): groups = {} for w in words: groups.setdefault("".join(sorted(w)), []).append(w) # create the list on first use return list(groups.values())
O(n·k)Instead of sorting each word, count its letters: two anagrams have identical counts. O(k) per word instead of O(k log k) — LeetCode’s optimal answer for lowercase words.
from collections import defaultdict def group_anagrams_counts(words): groups = defaultdict(list) for w in words: key = [0] * 26 # how many a's, b's, … z's for ch in w: key[ord(ch) - ord("a")] += 1 groups[tuple(key)].append(w) # lists cannot be dict keys; tuples can return list(groups.values())
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" → Falsedef is_palindrome(s): left, right = 0, len(s) - 1 while left < right: while left < right and not s[left].isalnum(): left += 1 while left < right and not s[right].isalnum(): right -= 1 if s[left].lower() != s[right].lower(): return False left += 1 right -= 1 return True
is_palindrome("A man, a plan, a canal: Panama") is_palindrome("race a car") is_palindrome(" ")
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n) · O(n) memoryBuild the cleaned version once and compare it with its reverse. Clearer, at the cost of a copy; the two-pointer version needs no extra memory.
def is_palindrome_clean(s): cleaned = [ch.lower() for ch in s if ch.isalnum()] return cleaned == cleaned[::-1]
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]]def three_sum(nums): nums = sorted(nums) res = [] for i in range(len(nums) - 2): if i > 0 and nums[i] == nums[i - 1]: continue # skip duplicate anchors left, right = i + 1, len(nums) - 1 while left < right: s = nums[i] + nums[left] + nums[right] if s < 0: left += 1 elif s > 0: right -= 1 else: res.append([nums[i], nums[left], nums[right]]) left += 1 right -= 1 while left < right and nums[left] == nums[left - 1]: left += 1 return res
three_sum([-1, 0, 1, 2, -1, -4]) three_sum([0, 1, 1]) three_sum([0, 0, 0, 0])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n³)Every triple; sorting each triple and putting it in a set removes duplicates. Hopeless for big input, perfect as a reference for the two-pointer version.
from itertools import combinations def three_sum_brute(nums): found = {tuple(sorted(t)) for t in combinations(nums, 3) if sum(t) == 0} return [list(t) for t in sorted(found)]
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] → 0def max_profit(prices): lowest = float("inf") best = 0 for p in prices: lowest = min(lowest, p) best = max(best, p - lowest) return best
max_profit([7, 1, 5, 3, 6, 4]) max_profit([7, 6, 4, 3, 1])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)accumulate with min produces the running minimum; zip pairs every day’s price with the cheapest earlier price. The same logic, without a hand-written loop.
from itertools import accumulate def max_profit_accumulate(prices): lowest_so_far = accumulate(prices, min) # the cheapest price up to each day return max((p - low for p, low in zip(prices, lowest_so_far)), default=0)
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.75def find_max_average(nums, k): window = sum(nums[:k]) best = window for i in range(k, len(nums)): window += nums[i] - nums[i - k] best = max(best, window) return best / k
find_max_average([1, 12, -5, -6, 50, 3], 4) find_max_average([5], 1)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n·k)Sums every window from scratch. Fine for small k; the sliding window avoids the repeated work.
def find_max_average_brute(nums, k): return max(sum(nums[i:i + k]) for i in range(len(nums) - k + 1)) / k
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]def daily_temperatures(temps): answer = [0] * len(temps) stack = [] # indices of days still waiting for i, t in enumerate(temps): while stack and temps[stack[-1]] < t: j = stack.pop() answer[j] = i - j stack.append(i) return answer
daily_temperatures([73, 74, 75, 71, 69, 72, 76, 73]) daily_temperatures([30, 40, 50, 60])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n²)For each day, scan forward to the first warmer one. Correct and easy to explain; the monotonic stack removes the repeated scanning.
def daily_temperatures_brute(temps): answer = [] for i, t in enumerate(temps): wait = next((j - i for j in range(i + 1, len(temps)) if temps[j] > t), 0) answer.append(wait) return answer
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() = -2class MinStack: def __init__(self): self.items = [] # pairs: (value, min so far) def push(self, x): current_min = min(x, self.items[-1][1]) if self.items else x self.items.append((x, current_min)) def pop(self): self.items.pop() def top(self): return self.items[-1][0] def get_min(self): return self.items[-1][1]
s = MinStack(); s.push(-2); s.push(0); s.push(-3); print(s.get_min()); s.pop(); print(s.top(), s.get_min())
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 → 4def search_insert(nums, target): left, right = 0, len(nums) while left < right: mid = (left + right) // 2 if nums[mid] < target: left = mid + 1 else: right = mid return left
bisect_left does this.search_insert([1, 3, 5, 6], 5) search_insert([1, 3, 5, 6], 2) search_insert([1, 3, 5, 6], 7)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(log n)This task is lower_bound itself, and bisect_left is its standard-library version.
from bisect import bisect_left def search_insert_bisect(nums, target): return bisect_left(nums, target)
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] → 0def find_min_rotated(nums): left, right = 0, len(nums) - 1 while left < right: mid = (left + right) // 2 if nums[mid] > nums[right]: left = mid + 1 # minimum is to the right else: right = mid return nums[left]
find_min_rotated([3, 4, 5, 1, 2]) find_min_rotated([4, 5, 6, 7, 0, 1, 2]) find_min_rotated([11, 13, 15, 17])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Always correct, but reads every element. The task asks for O(log n), which only binary search gives.
def find_min_rotated_min(nums): return min(nums)
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] → Falsedef can_jump(nums): reach = 0 for i, step in enumerate(nums): if i > reach: return False reach = max(reach, i + step) return True
can_jump([2, 3, 1, 1, 4]) can_jump([3, 2, 1, 0, 4])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)Move the goal backwards: whenever a position can jump to the current goal, it becomes the new goal. If the goal reaches the start, the answer is yes.
def can_jump_backwards(nums): goal = len(nums) - 1 # the position we still need to reach for i in range(len(nums) - 2, -1, -1): if i + nums[i] >= goal: goal = i # reaching i is now enough return goal == 0
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)def max_profit_ii(prices): return sum(max(0, prices[i] - prices[i - 1]) for i in range(1, len(prices)))
max_profit_ii([7, 1, 5, 3, 6, 4]) max_profit_ii([1, 2, 3, 4, 5]) max_profit_ii([7, 6, 4, 3, 1])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n)pairwise (Python 3.10+) yields neighbouring pairs, so there is no index arithmetic at all.
from itertools import pairwise def max_profit_ii_pairwise(prices): return sum(max(0, today - yesterday) for yesterday, today in pairwise(prices))
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) → -1class NumArray: def __init__(self, nums): self.prefix = [0] for x in nums: self.prefix.append(self.prefix[-1] + x) def sum_range(self, left, right): return self.prefix[right + 1] - self.prefix[left]
a = NumArray([-2, 0, 3, -5, 2, -1]); print(a.sum_range(0, 2), a.sum_range(2, 5), a.sum_range(0, 5))
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 → 2def subarray_sum(nums, k): count = 0 running = 0 seen = Counter({0: 1}) # prefix sum -> how many times for x in nums: running += x count += seen[running - k] seen[running] += 1 return count
subarray_sum([1, 1, 1], 2) subarray_sum([1, 2, 3], 3) subarray_sum([1, -1, 0], 0)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n²)Grows each subarray from every start. Negative numbers rule out a sliding window, which is why the main solution needs prefix sums with a Counter.
def subarray_sum_brute(nums, k): count = 0 for i in range(len(nums)): total = 0 for j in range(i, len(nums)): total += nums[j] # sum of nums[i..j], grown one step at a time if total == k: count += 1 return count
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]]def insert_interval(intervals, new): res = [] i, n = 0, len(intervals) while i < n and intervals[i][1] < new[0]: res.append(intervals[i]); i += 1 start, end = new while i < n and intervals[i][0] <= end: start = min(start, intervals[i][0]) end = max(end, intervals[i][1]) i += 1 res.append([start, end]) res.extend(intervals[i:]) return res
insert_interval([[1, 3], [6, 9]], [2, 5]) insert_interval([[1, 2], [3, 5], [6, 7], [8, 10], [12, 16]], [4, 8])
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]def erase_overlap_intervals(intervals): intervals = sorted(intervals, key=lambda iv: iv[1]) removed = 0 last_end = float("-inf") for start, end in intervals: if start >= last_end: last_end = end # keep it else: removed += 1 # overlaps: drop it return removed
erase_overlap_intervals([[1, 2], [2, 3], [3, 4], [1, 3]]) erase_overlap_intervals([[1, 2], [1, 2], [1, 2]])
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 + 1def rob(nums): take = skip = 0 for x in nums: take, skip = skip + x, max(take, skip) return max(take, skip)
rob([2, 7, 9, 3, 1]) rob([1, 2, 3, 1]) rob([])
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(n) · O(n) memoryThe same recurrence written as a table. Easier to debug — you can print it — and the main solution is this table with only the last two cells kept.
def rob_table(nums): best = [0] * (len(nums) + 2) # best[i] = the most you can take from house i on for i in range(len(nums) - 1, -1, -1): best[i] = max(nums[i] + best[i + 2], best[i + 1]) # rob i and skip one, or skip i return best[0]
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 + 1def coin_change(coins, amount): INF = amount + 1 dp = [0] + [INF] * amount for a in range(1, amount + 1): for c in coins: if c <= a: dp[a] = min(dp[a], dp[a - c] + 1) return dp[amount] if dp[amount] != INF else -1
coin_change([1, 2, 5], 11) coin_change([2], 3) coin_change([1, 3, 4], 6)
Same answers, different tools. Each version below was run on the same tests and checked against the main solution on 300 random inputs. Knowing two ways — and when each one is better — is exactly what an interviewer listens for.
O(amount · coins)The same recurrence, but asked as a question: “fewest coins for this rest?”. Memoisation makes every rest computed once — the same work as the bottom-up table.
from functools import lru_cache def coin_change_memo(coins, amount): @lru_cache(maxsize=None) def fewest(rest): # fewest coins that make exactly `rest` if rest == 0: return 0 options = [fewest(rest - c) for c in coins if c <= rest] options = [o for o in options if o != -1] return min(options) + 1 if options else -1 return fewest(amount)
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.