Most of us have been given the same advice: to become a better problem solver, you need to solve more problems. But this advice is too simplistic. Getting good at anything requires more than just reps. It’s also how you go about it.
Besides, when programmers hear the advice “solve more problems,” they often think that “more” means faster. It’s a mistake I made when starting out.
The problem-solving treadmill can be detrimental to learning and improving. Early on I’d speed through one problem and head to the next. But the reality of that approach soon set in. By focusing on quantity, I compromised quality and missed key learnings along the way.
It’s not to say that repetition doesn’t matter; it does. However, repetition alone doesn’t get to the heart of the matter. The process does.
I have a vested interest in this topic: I want to get better at solving problems in order to improve as a programmer. So here I offer my plan of attack. It involves reps to be sure—and a whole lot more.
“I was obsessed with HackerRank when I began learning to code,” said an instructor of a Python course I was taking. Although it’s fine to have a favorite platform, don’t limit yourself to a single one. Here’s why: you need to be ready for anything.
One goal of mine is to toggle smoothly among different problem types and across different platforms. The problems on Interview Cake are different than those in Reuven Lerner’s book, Python Workout. Likewise I find the problem statements in Lerner’s Weekly Python Exercise different from those on HackerRank.
I have my preferences, to be sure. However, I need to be able to solve problems of all types. So I’m using a variety of platforms and resources to get practice.
Here are some examples:
- Interview Cake
- Cracking the Coding Interview by Gayle Laakmann McDowell
- Python Workout by Reuven Lerner
- Weekly Python Exercise by Reuven Lerner
On Sundays, when I make my plan for the week ahead, I select a few problems from the above resources. I’m deliberate about it. For example, I’ll pick a problem that focuses on binary search from LeetCode. Then, one that focuses on data structures from one of Reuven Lerner’s sources.
This practice prevents me from getting too comfortable. I can’t rely on the same data structure or technique. I need to be able to pick the best tool for the job. I’ve got to be able to pivot.
It also challenges me. That’s because I select problems that push me to the edge of my limits, a feature of what psychologist Anders Ericsson calls “deliberate practice.”
Deliberate practice is all about skill development. It’s a fully-focused, conscious effort that takes you out of your “comfort zone,” centers on a specific goal, and “demands near-maximal effort,” Ericsson explains in his excellent book, Peak.
In other words, you’re not going through the motions doing something that comes easy or natural. “The hallmark of . . . deliberate practice,” Ericsson writes, “is that you try to do something you cannot do.”
You often hear about athletes who incorporate cross-training into their routine. For example, a runner completes a bike workout or two each week. It makes a lot of sense: by cross-training athletes push their cardiovascular fitness from a different angle.
Cross-training for programmers is a topic I’ve written about before. Yet, more needs to be said about the benefits of this approach. Just like an athlete cross-trains to improve their cardiovascular fitness, I’m incorporating cross-training to improve my mental fitness.
My cross-training involves solving math problems. I apply the same tactics I’d use to solve a programming problem but in mathematics. Same process, different context.
My current math book provides the cross-training benefits mentioned above. But it’s doing something more: it’s helping me build my intuition for solving problems.
That’s an important point. I don’t want to memorize a math equation or process. I want to intuitively know when to use it. The same is true when solving programming problems.
All of us get stuck and all of us get errors while problem-solving. We use these failures to help us figure out what to do differently.
Oftentimes that’s where the learning stops. Once we’ve solved the problem we’re anxious to move along. But don’t.
Mathematician Richard Hamming suggests that’s when the learning should begin.
“I regard the study of successes as being basically more important than the study of failures...there are so many ways of being wrong and so few of being right, studying successes is more efficient,” Hamming writes in his book, The Art of Doing Science and Engineering.
This idea of "looking back" and studying your successes is a topic I've written about previously, and it's worth noting again.
You always hear about how we need to "learn from our failures." But Hamming is right: there's so much to learn from our successes.
So one of the biggest changes I’ve made to my problem-solving process is putting Hamming’s words to practice. I vividly recall solving my first problem that involved a linked list, a data structure that was new to me at the time. After I solved the problem, I studied it intently. I copied my solution into a Google Doc file and used comments to explain the code to myself.
I wanted to ensure I clearly understood what I did and why for this critical reason: so I could do it again.
Awareness can be a wonderful teacher.
Don’t assume your solutions are great. Find out for yourself by getting feedback. Feedback is a critical factor when it comes to getting better. It’s also a component of deliberate practice.
There are two ways I’m getting feedback.
First, after I complete a problem, I study the solutions of others. Sometimes resources, like LeetCode and Cracking the Coding Interview, provide the solution. Other times I find the solution of a programmer who’s solved the same problem. Sometimes use both.
Either way, the next step is the same: I start at the top of the solution and explain each line in my own words. I speculate why the programmer chose this particular data structure, for example. Then, I write a short summary of the program to solidify the core concepts or new approaches that I just learned about.
Putting something complicated, like a line of code, into your own words is a great test to see if you really understand it. Plus, you get practice at another core skill: reading code.
If there’s something that’s new to me, I research it. I compare their code to my own. I study. I evaluate. I learn. It’s an arduous process, but I get so much from it.
The second way to get feedback is to simply ask another programmer for it. Video calls are helpful to step through code line by line in real time. Although, pull requests are useful, too.
There’s a point to the feedback: apply it! Put your learnings into practice.
Now let’s talk about repetition. Problem-solving is a skill. Like any skill, it takes time to build. It simply doesn’t happen overnight or over a weekend. That’s why I devote time each day to problem-solving.
There’s a common reaction to this daily practice: “There’s too much to learn. I can’t spend time each day problem-solving.”
My response is two-fold. First, there’s always going to be something more to learn, always.
Second, in large part programming is problem-solving. It's fundamental to our craft. It’s worthy of your time and attention each day. Here’s the caveat: make your daily practice intentional.
Many people regard problem-solving as an art. I happen to agree with that sentiment. The same can be said for the process itself.
The ideas outlined in this article are my approach for getting better at problem-solving. Your approach may look very different. The point is this: be intentional about your practice. Quality matters; make each problem count.
Originally published on amymhaddad.com.