Problem Solving
Getting Blocked
When I was in University, we were required to write a program to model a certain algorithm. I was certain my program was doing exactly what it was meant to according to the algorithm, but the result kept coming out wrong. Going through my code line by line, I compared what it was doing to how I thought it should work. I asked my friends whether they could spot a mistake, but they said their solutions looked the same. I googled specific interactions between pieces of my code but found nothing.
Naturally, all this was frustrating. I thought perhaps I was being punished for not being lucky, or smart enough to just "get it". When I eventually did find the issue (I set some variables to 0 instead of 1), it was almost a face to palm moment.
Despite our best efforts, at times we just can't work out how to solve a problem. We are blocked. It can seem like no matter how much thinking we do, we can't get our head around it. That may make us feel stupid or inadequate, but the answer shouldn't be to give up on ourselves.
I realised that it wasn't my fault or anyone else's that I had encountered the problem. I just needed to be a little more careful with the techniques I used to solve it. I knew the math in theory needed variables with initial values of 1, but because it wasn't written in the algorithm, I didn't notice as I checked the algorithm against my code. In other words, I wasn't exploring the full picture in my step-by-step analysis.
Elegant Solution
I used to enter programming competitions with my friends every year for a bit of fun. Often in a competition there are difficult problems and simple problems to solve worth points proportional to difficulty. Normally you get a limited number of attempts at solving a problem, and the less attempts you take, the more points you gain. Most of the time the faster your solution runs, the better.
While I wasn't nearly as good at solving problems as the other guys, I still found ways to contribute. In our team, we decided the really brilliant members would focus all their efforts on the biggest problems, while the rest of us worked through as many simple problems as possible to pad our team's score.
One particular problem I'm proud of solving asked us to read a document, output some information, and then re-read the document to find all the matches to what we had output. This kind of problem normally requires a slow running solution. I was hesitant about being able to solve it in time.
Consciously planning before going about solving a problem can reduce the need to make attempts and then back-track things that just don't work. We can use techniques like pseudocoding to step through a problem mentally before even beginning to write a solution.
I sat down after reading the problem brief and began to think about all of the cases where I would need to output something from my program. Writing these down in a list, I realised they were so similar I wouldn't need to read the document twice. I could just read it once in a very particular fashion. A shortcut! This completely changed how fast our solution would run. I wrote some pseudocode for how I thought it should go and quickly ran the solution by my other teammate. Then I got to work.
Once we submitted our solution, it not only had it passed on the first try, but it was the first pass for that question and eventually was the fastest running solution out of all the teams. I was really proud. My initial hesitation over my ability completely disappeared, and I realised that it was thanks to careful planning that I identified the short-cut and an extra pair of eyes to confirm I hadn't made any mistakes. It might have been a simple problem, but to me it affirmed my value to the team.
Techniques and Processes
There are many different ways to go about solving problems. Some involve thinking about the logic of a program in a specific way, others help you get unstuck when you hit a block. For me, it is useful to think of the scenario I might use a given technique, so for this section, I will use the tags logic
and debug
to note that.
Useful methods (in the order I like to use them):
Pseudocode
logic
Trying something
logic
debug
Console.logging
debug
Reading error messages
debug
Googling
logic
debug
Rubber Ducky Method
logic
debug
Asking your peers for help
logic
debug
Asking your coaches for help
logic
debug
Improving your process with reflection
logic
Pseudocode logic
Many people naturally begin to plan the steps they will take when they encounter a problem. The aim with Pseudocode is to break a problem down into much smaller steps in plain English. It allows us to solidify those steps into actionable snippets as well as provide us with a reminder of what we are trying to do whenever we step away, or even get lost in the details.
Something like this:
Bake a cake:
1. Get a bowl
2. Place all the ingredients in the bowl
3. Mix the ingredients
4. Get a cake tray
5. Pour the mixture into the tray
6. Bake the cake at 180 Degrees Celsius
7. Let the cake cool
Enjoy!
Commenting your code before you start can help keep each piece of code nicely segmented and simple. For example, these are some pseudocode comments for my Fizzbuzz solution in JavaScript:
// FizzBuzz (Super Edition)
// Fizzbuzzes on an individual number
// fizzBuzz function
// Prepare replacement string
// Check if number is multiple of 3
// Add Fizz to replacement string
// Check if number is a multiple of 5
// Add Buzz to replacement string
// Multiples of 15 are already FizzBuzzed
// If number is not multiple of 3 or 5
// Return number
// Else
// Return replacement string
// Fizzbuzzes an entire array
// superFizzBuzz function
// loop over array
// call fizzbuzz to replace each element
// Return modified array
Keeping a pseudocode block for reference can remind you of the overall flow of your program.
Not far from the final solution:
// FizzBuzz (Super Edition)
// Fizzbuzzes on an individual number
// fizzBuzz function
function fizzbuzz(num) {
// Prepare replacement string
let replace = ''
// Check if number is multiple of 3
if(num%3 == 0){
// Add Fizz
replace += 'Fizz'
}
// Check if number is a multiple of 5
if(num%5 == 0){
// Add Buzz
replace += 'Buzz'
}
// Multiples of 15 are already FizzBuzzed
// Return replacement string
// If number is not multiple of 3 or 5
// Return number
return (replace!='' ? replace : num)
}
// Fizzbuzzes an entire array
// superFizzBuzz function
function super_fizzbuzz(arr) {
// map fizzbuzz to the number array!
return arr.map(num => fizzbuzz(num))
}
Trying something logic
debug
Often issues with a solution can be preemptively solved by just trying something out ahead of time. If I wanted to write a piece of code which averaged a set of test scores from all the students in a class, usually I would make sure I had first tried and tested an averaging function. Perhaps I’d also test accessing the score of an individual student. Knowing what is possible and works well can inform both our program’s logic as well as identify problems.
Console.logging debug
Often when programming, we manage information that can’t be seen by our eyes while our program is running. Logging variables to the console allows us to see what information our program has and therefore catch inconsistencies we might not have been aware of.
Javascript makes it very simple with console.log()
For example, in my FizzBuzz function above I could console.log('Fizz')
every time the number was a multiple of 3, to make sure it was happening. I could even console.log(replace)
before returning to make sure the replacement string had the right FizzBuzz.
Reading error messages debug
When your code is doing something wrong, chances are when you go to run it an error message gets spit out. These are helpful for pinpointing the specific point at which your code goes wrong, and therefore offering a chance to resolve the issue. Most error messages can be broken down into a couple of helpful pieces.
The error message :
![An error log from repl.it](images/errorLog1.png)
This can be the type of error, what value was expected and what was observed. Often it gives a file name and a line. You can think of these as the apparent culprits for the error. In the example above, it says SyntaxError: Invalid or unexpected token
and tells me the error is on line 13 of index.js. Looking at line 13 closely, I've missed a closing apostrophe!
The stack trace :
![An error log from repl.it](images/errorLog2.png)
A list of all the processes your program was in the middle of. Remember, a function can be called from a function, so while following regular control flow, chances are that an error occurs mid-way through executing multiple functions. The stack trace tells you which order the functions were called in and also where they come from. Some might be written by you, others could be from external libraries or built in functions.
Let's explore the example above. There are three functions: average, Object.<anonymous> and Module._compile. We can ignore the latter two because those aren't functions we wrote and chances are the problem isn't there. The stacktrace tells us the error is at index.js:27:12. Looking to line 27 of our code, we see it is the average function. Our ReferenceError tells us arrr is not defined
. Hang on, we clearly wrote arrr[num]
on line 27. Oh! there's an extra 'r' there. It should be arr[num]
Googling logic
debug
Perhaps a tool of infinite power. Sometimes we might have no idea what a specific error message means, but chances are someone else has had the same error message in the past. Googling allows us to find out how they solved it, as well as potentially stumble into more useful explanations or cleaner alternative solutions.
Other times, it can be hard to know what the best way of going about something might be. Googling a set of terms relevant to your problem can unlock unique ways to go about solving it. In other words, you could identify new pieces of logic to use in your solution.
Googling also becomes important for checking how built-in functions work. Because many people around the world use them, how they work is well documented. A quick search for the inbuilt function name and the language will net you a handy guide!
Rubber Ducky Method logic
debug
Sometimes sharing a problem helps bring forth the insight we need to solve it. Have you ever explained a problem to a friend and in the process realised exactly what you need to do, without your friend even responding? That’s what we try to emulate with the rubber ducky method.
Imagine, or have, a rubber ducky in your hand while you explain your code to it. Start with general goals, move to the details (line by line) and then on to program flow. Remember, a rubber duck can’t ask questions or give you feedback, so the aim is to expand upon each aspect of your code in detail to the duck.
A good duck need not ask and a good programmer omits no details.
![Rubber Duck](images/duckling-2542277_640.png)
Eventually you’ll find that explaining your code offers you insights you might not have had, or perhaps just forgot, and allow you to identify your mistakes. This method is also really powerful for looking at someone else’s code, because it requires you to understand each part as you go.
Asking your peers for help logic
debug
At times we find ourselves lost and completely unsure of how to best proceed. These are the times where asking peers for help is the most beneficial. A fresh set of eyes might quickly spot the issue our own eyes have glazed over, or a peer might be able to explain key logical steps we hadn’t thought of.
The key to best utilising peer help, is to have a clear idea of what it was you were trying to do and what it is you have done so that they can quickly get up to speed. Screenshots of your code, or a copy of the error message you are receiving can help get the message across if you aren't in person.
Asking your coaches for help logic
debug
Similar to asking your peers, in a study programme you usually have coaches who are avaliable to help you when you get stuck. They probably know the ins and outs of the tasks you are trying to complete, and therefore have a good idea of how to help you. Remember that coaches can be busy helping others, so make sure to make a decent effort to solve problems yourself before going to them.
Improving your process with reflection logic
The final technique of problem solving is arguably more important than all the rest. Reflecting is about thinking back on what you have done in your attempts to solve the problem, what worked and what didn't. Then, thinking about how you might approach the problem differently next time, for a more efficient result.
Reflecting applies to all kinds of problems, not just those in programming, however, one unique tool to programming is the ability to refactor code. When your initial solution isn't very elegant, you have the power to go back and clean it up. This can help solidify your learning and helps others understand what's going on.