How to Think Algorithmically
If you have seen me help someone with their code, you may have noticed my unusual approach. I do not have them start by looking at their code. Instead, I ask them to explain their logic. What are they trying to accomplish? How do they think it should work?
This is not because I am lazy or trying to make things difficult. It is because I have learned, through years of teaching and debugging, that most coding problems are not coding problems. They are logic problems.
The Problem with Starting with Code
When you sit down at your keyboard and immediately start typing, you are trying to solve two problems at once. You are working out what you want to do, and working out how to make the computer do it.
This is similar to learning English by writing Shakespearean poetry. It produces a mess.
Say you are trying to write a FizzBuzz algorithm. If you start with code instead of logic, you might write something like this:
for (let i = 1; i <= 100; i++) {
if (i % 3 === 0) {
console.log("Fizz");
}
if (i % 5 === 0) {
console.log("Buzz");
}
// ... wait, what about FizzBuzz?
}
You have already hit a problem, and you have not finished writing the code. If you had worked out the logic first, you would have identified this problem without dealing with the syntax.
The Algorithmic Thinking Approach
Instead of starting with code, start with plain language. Write out your instructions one step at a time, as if you were explaining them to Naomi, who will do exactly what you say and nothing more.
I will use FizzBuzz as an example. Here is how I would break it down:
- First, I look at the number.
- Is the number divisible by three?
- If it is, I say "Fizz".
- Is the number divisible by five?
- If it is, I say "Buzz".
- Is the number divisible by three and five?
- If it is, I say "FizzBuzz".
- If I have said nothing at all, I say the number.
The crucial part is that I test this logic manually before I write any code. What happens if the number is 3? What should happen? What about 2? 5? 7? 15?
- First, I look at the number. The number is 3.
- Is the number divisible by three? Yes, 3 is divisible by three.
- If it is, I say "Fizz". So I say "Fizz".
Saying a word ends my logical flow. I have said what I need to say, so I am done.
We are expected to say "Fizz" when we see 3, so this is correct. What about 5?
- First, I look at the number. The number is 5.
- Is the number divisible by three? No, 5 is not divisible by three.
- If it is, I say "Fizz". It is not, so I do not say anything.
- Is the number divisible by five? Yes, 5 is divisible by five.
- If it is, I say "Buzz". So I say "Buzz".
Again, the logic is correct. When I see 5, I should say "Buzz". What about 7?
- First, I look at the number. The number is 7.
- Is the number divisible by three? No, 7 is not divisible by three.
- If it is, I say "Fizz". It is not, so I do not say anything.
- Is the number divisible by five? No, 7 is not divisible by five.
- If it is, I say "Buzz". It is not, so I do not say anything.
- Is the number divisible by three and five? No, 7 is not divisible by three or five.
- If it is, I say "FizzBuzz". It is not, so I do not say anything.
- If I have said nothing at all, I say the number. So I say 7.
This is also correct. When I see 7, I should say 7. What about 15?
- First, I look at the number. The number is 15.
- Is the number divisible by three? Yes, 15 is divisible by three.
- If it is, I say "Fizz". So I say "Fizz".
This is an issue. When I see 15, I should say "FizzBuzz".
Because saying something ends my logic, I need to check for three and five before I check them individually. In the current order, the "three and five" condition can never be reached.
So I fix my logic:
- First, I look at the number.
- Is the number divisible by three and five?
- If it is, I say "FizzBuzz".
- Otherwise, is the number divisible by three?
- If it is, I say "Fizz".
- Otherwise, is the number divisible by five?
- If it is, I say "Buzz".
- Otherwise, I say the number.
Now I test again. For 3, the result is "Fizz", which is correct. For 5, the result is "Buzz", which is correct. For 15, the result is "FizzBuzz", which is correct. For 2, the result is 2, which is correct.
Once the logic is sound and all of my manual tests work, I write code.
This approach serves two purposes:
- I work out the logic independently from the code, so I am not handling both logic and syntax at once.
- If my app does not work, I can look for an error in the code, because I know the logic is sound. I tested that before writing any code.
The Importance of Precision
Computers do only and exactly what you say. They cannot infer your intent. They cannot read between the lines. They follow your instructions to the letter, even when those instructions lead to nonsense.
I learned this recently while helping someone solve a pyramid-building problem. They were trying to build a pyramid out of a character, with a specified number of rows. Their initial instructions were something like:
- Look at the string.
- Look at the integer.
- Write the string.
- Go to the next line.
- Write the same string from before again.
- Add two more of the same single string beside it.
- Repeat until you have the same number of rows as the integer.
When I followed these instructions precisely, I got:
x
xxx
xxxxx
xxxxxxx
xxxxxxxxx
This is not a pyramid. A pyramid should be centred, like this:
x
xxx
xxxxx
xxxxxxx
xxxxxxxxx
The instructions were missing a crucial detail: the spacing. They were also imprecise. The instruction "Add two more of the same single string beside it" does not say what "it" refers to or where the characters go. The instructions assumed I would understand the intent, but a computer, or a very literal Naomi following instructions, would not.
After several iterations, we refined the instructions to be precise:
- Look at the string. It is what we want the pyramid to be built by.
- Look at the integer. It is the amount of rows we want.
- Add a number of spaces equal to the integer, and then write the string.
- Go to the next line.
- Write the same spaces string from before again, but get rid of one space, and write the string.
- Add two more of the same single string beside it.
- Write the whole space and string again from the last row, get rid of one space again, and add two more of the same string.
- Repeat step 7 until you have the same number of rows as the integer.
Now, when I follow these instructions precisely, I get the correct pyramid. The logic is sound. Only then do we start thinking about how to translate this into code.
Translating Logic to Code
Once your logic is sound and tested, translating it to code is much simpler. You are no longer working out what to do. You already know that. You are only working out how to express it.
Return to the pyramid example. We have our logic:
- Look at the string and integer.
- For each row, calculate the number of spaces and the number of characters.
- Print the spaces, then the characters.
- Move to the next row.
Now we can think about the code structure.
First, we define our function:
function pyramid(characterToBuildPyramidWith, numOfRows) {
}
These are our steps:
- We need a loop to iterate through rows.
- For each row, we must know how many spaces to use. We use fewer spaces in each row.
- For each row, we must know how many characters to use. We use more characters in each row.
- Finally, we print our result.
First, we create a loop to iterate through the number of rows. A standard for loop is suitable for this.
function pyramid(characterToBuildPyramidWith, numOfRows) {
for (let row = 0; row < numOfRows; row++) {
}
}
We use row instead of i because it is less confusing.
For each row (so each iteration), we must know how many spaces to use. We have already worked that out in our logic:
Add a number of spaces equal to the integer Write the whole space and string again from the last row, get rid of one space again
So we start with numOfRows spaces, and for each iteration we decrement the number of spaces by one. This means we can use numOfRows - row. We put that in a variable so we do not lose track.
function pyramid(characterToBuildPyramidWith, numOfRows) {
for (let row = 0; row < numOfRows; row++) {
const spaces = " ".repeat(numOfRows - row);
}
}
Next is the number of characters. Again, we refer back to our logic on paper:
Write the same spaces string from before again, but get rid of one space, and write the string. Write the whole space and string again from the last row, get rid of one space again, and add two more of the same string.
We start with one character and add two more for every iteration. That is row times two, plus one to account for the initial character.
function pyramid(characterToBuildPyramidWith, numOfRows) {
for (let row = 0; row < numOfRows; row++) {
const spaces = " ".repeat(numOfRows - row);
const characters = characterToBuildPyramidWith.repeat(row * 2 + 1);
}
}
Our last step is:
Finally, print our result.
We can print with a console.log statement. We print our spaces followed by our characters.
function pyramid(characterToBuildPyramidWith, numOfRows) {
for (let row = 0; row < numOfRows; row++) {
const spaces = " ".repeat(numOfRows - row);
const characters = characterToBuildPyramidWith.repeat(row * 2 + 1);
console.log(spaces + characters);
}
}
Now we check our work by calling the function:
function pyramid(characterToBuildPyramidWith, numOfRows) {
for (let row = 0; row < numOfRows; row++) {
const spaces = " ".repeat(numOfRows - row);
const characters = characterToBuildPyramidWith.repeat(row * 2 + 1);
console.log(spaces + characters);
}
}
pyramid("x", 5);
Checking the console, I see:
x
xxx
xxxxx
xxxxxxx
xxxxxxxxx
This looks like a pyramid, but it is shifted one space too far. The logic is off, so we go back to our logic on paper and do not touch the code yet.
- Look at the string. It is what we want the pyramid to be built by.
- Look at the integer. It is the amount of rows we want.
- Add a number of spaces equal to the integer, and then write the string.
- Go to the next line.
- Write the same spaces string from before again, but get rid of one space, and write the string.
- Add two more of the same single string beside it.
- Write the whole space and string again from the last row, get rid of one space again, and add two more of the same string.
- Repeat step 7 until you have the same number of rows as the integer.
The issue is in step 3.
- Add a number of spaces equal to the integer, and then write the string.
By adding a number of spaces equal to the integer, we failed to account for the initial pyramid character. We need to add one less space.
So we update our logic first:
- Look at the string. It is what we want the pyramid to be built by.
- Look at the integer. It is the amount of rows we want.
- Add a number of spaces equal to the integer less one, and then write the string.
- Go to the next line.
- Write the same spaces string from before again, but get rid of one space, and write the string.
- Add two more of the same single string beside it.
- Write the whole space and string again from the last row, get rid of one space again, and add two more of the same string.
- Repeat step 7 until you have the same number of rows as the integer.
When Logic Breaks During Coding
This happens often. You have worked out your logic, tested it manually, and it looks correct. You start translating it to code, and halfway through, you realise it is not correct.
Maybe you cannot work out how to achieve a specific step. Maybe you discover an edge case you did not consider. Maybe the logic does not work the way you thought it would.
Stop coding immediately.
The instinct is to keep typing, to try to fix the problem in code, or to work around it. Do not do this. If your logic needs revision, that is a logic problem, not a code problem. Logic problems should be solved on paper, not in your editor.
Step away from your keyboard. Go back to your plain English instructions. Walk through them again manually. Where does the logic break? What did you miss? Which assumption turned out to be wrong?
Say you are working on the pyramid problem, and while coding you realise: "How do I know how many spaces to remove each time? Do I start with the full number of spaces, or one less?"
Instead of working this out in code, go back to your logic:
- Add a number of spaces equal to the integer, and then write the string.
- Go to the next line.
- Write the same spaces string from before again, but get rid of one space, and write the string.
Test it manually. If the integer is 5, I start with 5 spaces. Then I go to the next line and have 4 spaces. Then 3 spaces. Then 2 spaces. Then 1 space. Then 0 spaces. This is consistent.
Now you can go back to your code with clarity. You know that for row 0, you need numOfRows - 0 - 1 spaces (which is 4 for 5 rows). For row 1, you need numOfRows - 1 - 1 spaces (which is 3). The pattern is clear because you worked it out in logic first.
Assume we have tested our change manually and it works. I am omitting that process here. Now we can update our code to reflect our logic change:
function pyramid(characterToBuildPyramidWith, numOfRows) {
for (let row = 0; row < numOfRows; row++) {
// Notice how this is the ONLY change I made. We aren't changing anything that isn't updated in the logic on paper.
const spaces = " ".repeat(numOfRows - row - 1);
const characters = characterToBuildPyramidWith.repeat(row * 2 + 1);
console.log(spaces + characters);
}
}
pyramid("x", 5);
The output is:
x
xxx
xxxxx
xxxxxxx
xxxxxxxxx
The pyramid is now correct.
The key principle is this: if you discover a logic problem while coding, you have not discovered a coding problem. You have discovered that your logic was incomplete. Fix the logic first, test it manually, and then return to the code.
This may feel inefficient. You might think that you are close and should fix the one remaining issue in code. Fixing logic in code does not work, and it makes the problem larger.
The Benefits of This Approach
When you think algorithmically first, you gain several advantages.
You catch logic errors early. Instead of debugging why your code does not work, you debug why your logic does not work. Logic is much easier to debug than code.
You separate concerns. Logic and syntax are two different problems. Solve them separately, and you will solve them more effectively.
You build confidence. When your code does not work, you know it is a syntax issue and not a logic issue. That narrows down your debugging significantly.
You communicate better. When you can explain your logic in plain English, you can explain it to teammates, ask for help more effectively, and document your code more clearly.
You learn faster. By focusing on logic first, you develop stronger problem-solving skills that transfer across languages and technologies.
The Takeaway
Before you write code, do the following:
- Write out your logic in plain English, step by step.
- Test that logic by hand, using multiple test cases.
- Ideally, ensure you have tested every branch in your logical breakdown.
- Edit and adjust the logic when test cases fail.
- Once all of your manual test cases pass your hand-written logic, begin translating it into code.
You will find that your code is cleaner, your bugs are fewer, and your problem-solving skills are stronger. If you can explain your logic to me in plain English and it works, then translating it to code is a matter of syntax. Syntax is the easy part.
Remember: computers do only and exactly what you say. Be precise, be thorough, and test your logic before you write your code.
