Algorithmic thinking is the ability to turn a vague problem into a precise sequence of steps that always works and is fast enough for the input size. It has less to do with knowing named algorithms than with a habit: define the problem exactly, solve a small case by hand, notice what you did, generalise it, and check its cost. You build it with practice on that habit, and the rest of this post shows the habit applied to a real problem.
The five steps
- Restate the problem precisely. What are the inputs, their types and size limits? What exactly is the output? What counts as valid? Ask about empty input, duplicates, negative numbers and ties.
- Solve small examples by hand. Pick two or three inputs, including a tricky one, and work out the answers without thinking about code. Pay attention to what you actually do.
- Write down the simplest correct method. Usually brute force. It gives you a correctness baseline and a complexity to beat.
- Find the waste and remove it. Where does the simple method repeat work, or look at data it could have ruled out? That question leads to a better data structure or technique.
- Verify. Trace your steps on the examples and edge cases, state the time and space complexity, and compare against the brute force on random inputs if you can.
Decomposition (breaking the problem into smaller ones) and abstraction (ignoring details that do not matter, such as treating a map as a graph) run through all five steps. Our post on breaking down complex problems covers decomposition in more detail.
Worked example: longest substring without repeating characters
1. Restate. Input: a string s. Output: the length of the longest contiguous substring in which no character appears twice. For "abcabcbb" the answer is 3 ("abc"). For "" it is 0.
2. By hand. In "pwwkew" I scan left to right. "pw" is fine; the next w repeats, so I must drop everything up to and including the first w. Continue from the second w: "wke" is length 3, then the next w repeats again. The answer is 3. Note what I did: I kept a stretch of characters, and when a repeat appeared I moved the start of the stretch forward past the earlier copy.
3. Brute force. Try every start index and extend until a repeat appears:
def brute(s):
best = 0
for i in range(len(s)):
seen = set()
for j in range(i, len(s)):
if s[j] in seen:
break
seen.add(s[j])
best = max(best, j - i + 1)
return best
This is correct. Its time is O(n²) in the worst case, and it also re-reads the same characters from many different start positions.
4. Remove the waste. When the inner loop restarts at i + 1, it re-scans characters it already knew were distinct. My hand method never went backwards. It kept a start index and, for each character, the last position where it appeared. On a repeat inside the current stretch, jump the start to just after that earlier position.
def window(s):
last = {} # char -> most recent index
start = best = 0
for i, ch in enumerate(s):
if ch in last and last[ch] >= start:
start = last[ch] + 1
last[ch] = i
best = max(best, i - start + 1)
return best
Each character is visited once, so this is O(n) time, with extra space bounded by the number of distinct characters.
5. Verify. Run both functions on examples, including "abba", which exposes the need for the check last[ch] >= start. Without it, when the last a is read, the code would jump the start backwards to index 1. Output from the code above:
'abcabcbb' 3 3
'bbbbb' 1 1
'pwwkew' 3 3
'' 0 0
'a' 1 1
'abba' 2 2
Each row is the input followed by the results of brute and window. The two also agreed on 2,000 random strings over a three-letter alphabet. Comparing a fast solution against a slow, obviously correct one is the cheapest strong test you have. This is the sliding window pattern; the Longest Subarray Without Repeating lessons on AlgoCademy take the same problem from brute force to the optimised version in steps.
The mental moves behind the steps
- Pattern matching: “this looks like finding a pair in a sorted array” lets you reuse a known technique. This comes from having solved many problems; see how to identify patterns in coding challenges.
- Invariants: state something that stays true throughout your loop (“
startis the smallest index such thats[start..i]has no repeats”). If you can state it, you can usually prove the loop is correct. - Cost awareness: estimate operations from the input limits. About 108 simple operations is a rough ceiling for a few seconds in most languages, so n = 105 rules out O(n²) but allows O(n log n).
- Trade-offs: time versus memory, simplicity versus speed. The best algorithm on paper is not always the right one: for 50 items, a clear O(n²) loop is fine.
Mistakes that slow people down
- Coding before understanding. If you cannot solve three small examples on paper, you cannot write the code yet.
- Skipping the brute force. It feels like wasted time but it defines correctness and shows you where the waste is.
- Chasing the clever trick. Most problems need a standard technique applied carefully, not a flash of insight.
- Never testing edge cases. Empty input, a single element, all equal values, and the largest allowed input.
- Reading solutions too early. Try for a fixed time (say 30 minutes), then read, then re-implement from scratch the next day.
A simple practice plan
- Learn one technique at a time (hash tables, two pointers, sliding window, binary search, recursion, BFS and DFS) and do 5 to 10 problems on it.
- For every problem, write the five steps in a notebook, even if briefly. The act of stating the brute force and its waste is the training.
- Re-solve problems you needed help with after a week.
- Move to mixed sets, choosing the technique yourself.
- Explain solutions out loud, as in an interview. Explaining exposes gaps that silent solving hides.
Pseudocode is a good middle step between the idea and the code: it forces the steps to be exact without the syntax. Visualisers such as VisuAlgo can help you see what an algorithm does to the data, but only if you predict each step before you watch it.
