← View series: problem solving with dsa
~/blog
Flowcharts and Pseudocode
The Problem
You write a function to check if a string is a palindrome:
def is_palindrome(s):
return s == s[::-1]It passes for "racecar". You run it on "A man, a plan, a canal: Panama". It fails. You add s.lower(). It fails again — spaces and punctuation break the comparison. You add filter logic. Then you notice the empty string isn't handled. Then single characters. Between the preprocessing, the edge case guards, and the loop logic, the function that started at six lines has tripled in size, and you have lost confidence that all the paths are correct.
This is the pattern for most problems beyond trivial scope. The instinct to start coding immediately pays off only when the problem fits in your head in one pass. When it involves branching, loops, or multiple edge cases, writing code first produces logic that is correct by coincidence and fragile by construction.
Flowcharts and pseudocode replace reactive patching with deliberate design. They force you to see every branch, every edge case, and every loop exit before you write a line of code.
Flowchart
A flowchart is a diagram of control flow. Each shape has a specific meaning:
- Oval — start or end. Exactly one start per diagram, at least one end.
- Rectangle — a process or operation. "max = list[0]", "increment i by 1".
- Diamond — a decision with exactly two outgoing paths (yes/no, true/false).
- Parallelogram — input or output. Reading data, printing results.
- Arrow — direction of flow. Conventionally top-to-bottom or left-to-right.
The diagram for "find the largest number in a list":
flowchart TD
Start([Start]) --> Input[Read list]
Input --> Init[max = list[0], i = 1]
Init --> Check{i < len(list)?}
Check -->|Yes| Compare{list[i] > max?}
Compare -->|Yes| Update[max = list[i]]
Update --> Increment[i = i + 1]
Compare -->|No| Increment
Increment --> Check
Check -->|No| Output[Print max]
Output --> End([End])Every path is drawn explicitly. The loop back is a visible arrow, not a hidden while keyword. You can trace the three interesting cases — empty list, single element, all equal — by following the arrows.
For a problem with more branching, the flowchart's value becomes more obvious. Here is "check if a number is prime":
flowchart TD
Start([Start]) --> Input[Input n]
Input --> EdgeCheck{n ≤ 1?}
EdgeCheck -->|Yes| NotPrime[Print "Not prime"]
EdgeCheck -->|No| Init[i = 2]
Init --> LoopCheck{i × i ≤ n?}
LoopCheck -->|No| Prime[Print "Prime"]
LoopCheck -->|Yes| DivCheck{n % i == 0?}
DivCheck -->|Yes| NotPrime2[Print "Not prime"]
DivCheck -->|No| Increment[i = i + 1]
Increment --> LoopCheck
NotPrime --> End([End])
NotPrime2 --> End
Prime --> EndThree decision diamonds, two possible "not prime" exits (edge case and divisibility), and a loop bounded by i × i ≤ n. The structure is visible at a glance — no hidden else clauses or break statements.
Tracing Through the Diagram
The real test of a flowchart is running edge cases through it manually. Take the find-max diagram against an empty list:
Read list— the list is[].max = list[0], i = 1— crash. Accessing index 0 of an empty list is undefined.
The flowchart revealed a missing guard before a single line of implementation. The fix: add a diamond — "Is the list empty?" with a "Return None" exit.
Now trace [5] (single element):
max = 5, i = 11 < 1?— NoPrint max— prints 5
The loop condition naturally handles this case. No special branch needed.
Trace [5, 3, 9, 2]:
| Step | i | max | list[i] | Decision |
|---|---|---|---|---|
| Init | 1 | 5 | — | — |
| Check | 1 | — | — | 1 < 4 → Yes |
| Compare | — | 5 | 3 | 3 > 5 → No |
| Increment | 2 | — | — | — |
| Check | 2 | — | — | 2 < 4 → Yes |
| Compare | — | 5 | 9 | 9 > 5 → Yes |
| Update | — | 9 | — | — |
| Increment | 3 | — | — | — |
| Check | 3 | — | — | 3 < 4 → Yes |
| Compare | — | 9 | 2 | 2 > 9 → No |
| Increment | 4 | — | — | — |
| Check | 4 | — | — | 4 < 4 → No |
| Output | — | 9 | — | Print max |
The trace is mechanical. Each row follows one arrow. No runtime, no debugger.
The same exercise on the prime checker flowchart reveals a different insight. Trace n = 1:
n ≤ 1?— YesPrint "Not prime"— correct. 1 is not prime by definition.
Trace n = 2:
n ≤ 1?— Noi = 2i × i ≤ n?—2 × 2 ≤ 2→ NoPrint "Prime"— correct.
Trace n = 17:
| Step | i | n % i | Decision |
|---|---|---|---|
| Init | 2 | — | — |
| LoopCheck | 2 | — | 4 ≤ 17 → Yes |
| DivCheck | 2 | 1 | 1 ≠ 0 → No |
| Increment | 3 | — | — |
| LoopCheck | 3 | — | 9 ≤ 17 → Yes |
| DivCheck | 3 | 2 | 2 ≠ 0 → No |
| Increment | 4 | — | — |
| LoopCheck | 4 | — | 16 ≤ 17 → Yes |
| DivCheck | 4 | 1 | 1 ≠ 0 → No |
| Increment | 5 | — | — |
| LoopCheck | 5 | — | 25 ≤ 17 → No |
| — | — | "Prime" |
The loop runs for i = 2, 3, 4 and stops when i = 5 violates the condition. The algorithm touches four divisors for a number that could have been checked by trial division up to n-1 (16 iterations). The flowchart makes the optimization visible — the i × i ≤ n bound is the reason.
Pseudocode
Pseudocode sits between a flowchart and real code. It borrows the structure of programming languages — conditionals, loops, functions — but uses natural language for the expressions.
The find-max in pseudocode:
FUNCTION find_max(list)
IF list is empty THEN
RETURN None
max = list[0]
FOR i = 1 TO list.length - 1
IF list[i] > max THEN
max = list[i]
RETURN max
END FUNCTION
Pseudocode is language-agnostic. The same logic maps to Python, Java, C++, or JavaScript with mechanical changes.
There is a spectrum of how close pseudocode looks to real code. At one end is almost-English, useful when communicating with non-programmers:
Begin
Input FirstNumber
Input SecondNumber
SUM = FirstNumber + SecondNumber
Print SUM
End
At the other end is almost-code, useful when designing for a specific environment but not yet committed to syntax:
function find_max(list):
if list is empty:
return None
max = list[0]
for i in range(1, len(list)):
if list[i] > max:
max = list[i]
return max
The right level depends on the audience. For a teammate who knows Python and wants to validate the algorithm, the almost-code form is efficient. For a stakeholder who does not read code, the flowchart or almost-English pseudocode is clearer.
Flowchart vs Pseudocode
| Aspect | Flowchart | Pseudocode |
|---|---|---|
| When to use | Understanding branching logic, communicating with non-programmers | Designing algorithms before writing code |
| Strengths | Visual, easy to spot missing paths | Compact, closer to actual code |
| Weakness | Bulky for large problems | Less visual — easy to miss branches |
| Best for | Problems with complex conditionals | Problems with sequential logic |
They complement each other. For a binary search or a tree traversal, start with a flowchart — the branching structure is the hard part. For a merge, a factorial, or a string transformation, start with pseudocode — the logic is linear and a diagram adds overhead.
Translating to Python
The find-max pseudocode translates directly:
def find_max(lst):
if not lst:
return None
max_val = lst[0]
for i in range(1, len(lst)):
if lst[i] > max_val:
max_val = lst[i]
return max_valThe translation is mechanical because the algorithm's structure was already decided in pseudocode. The Python-specific choices are None for absence, range() for iteration, and not lst for the empty check.
The same mechanical mapping works for the prime checker:
def is_prime(n):
if n <= 1:
return False
i = 2
while i * i <= n:
if n % i == 0:
return False
i += 1
return TrueThe pseudocode used × for multiplication; Python uses *. The pseudocode used %; Python uses the same operator. Every structural decision — the early return on n ≤ 1, the loop bound, the default True at the end — was made before writing Python. The code writes itself once the design is done.
Common Mistakes
Mistake 1 — A decision diamond with an undefined branch. Every diamond must have exactly two outgoing paths. In the prime checker flowchart, both n % i == 0 cases — true and false — are drawn. If only the true branch were drawn and the false branch implicitly fell through to the next diamond, the diagram would be incomplete and the logic ambiguous. When a branch should do nothing, draw it explicitly: a short arrow to the next node or a box labeled "continue".
Mistake 2 — Pseudocode that depends on language-specific features. Negative indexing (list[-1]), implicit truthiness (if list), and mutable default arguments are Python-isms. Writing them in pseudocode assumes the reader and implementer share those conventions. Keep pseudocode to operations available in every language: indexing by position, explicit comparisons, explicit null checks.
Mistake 3 — Pseudocode detailed enough to be real code. If your pseudocode reads like Python with different variable names, you skipped the design step. The purpose is to think about the algorithm, not to pre-write the implementation. Write "for every second element" instead of for i in range(0, len(arr), 2). Save the exact syntax for when you are writing real code.
Mistake 4 — Not updating the diagram when the algorithm changes. A flowchart or pseudocode that contradicts the implementation is worse than none at all — it misleads the next reader. If you discover a missing edge case during implementation, update the diagram. If the diagram lives in a doc or a whiteboard, the cost of keeping it in sync is negligible compared to the cost of debugging against stale documentation.
When You Can Skip This
If you sketch on paper or in your head before typing, you are already doing what flowcharts and pseudocode formalize. The notation itself is overhead — it pays off when the problem has more than three decision points or when the loop structure has multiple exit conditions.
The useful habit is not drawing perfect diagrams. It is separating algorithm design from code writing. Most bugs come from doing both at the same time. The diagram is just the tool that forces the separation.
For a two-sum problem, jump straight to code. For a Red-Black tree insertion, draw the flowchart first — it is the difference between a correct implementation and an all-night debugging session.
The notation does not matter: paper, whiteboard, a mermaid diagram in a Markdown file. What matters is the habit of resolving the structure before committing to the code. Every hour spent designing is three hours saved debugging. The return rate only improves as the problems get harder.