Algorithms and Problem Solving Tips
Algorithms and Problem Solving Tips
ONE_
hard problem on an interview. The reason for this is something called an
Amygdala Hijack, which is many of the “non technical” roadblocks we help
cover during our course.
In short, this is when the fear center of the brain takes over higher functions
of the brain and basically prevents people from being able to think clearly
and effectively problem-solve.
STEP
Most engineers have experienced this at some point during the interview
process and we’ve found that having a set starting point of what to do
during an interview is crucial to prevent this from happening.
You can start with time and space constraints. Knowing this will help you
rule out solutions that run too inefficiently, or it will give you a hint of how
many auxiliary data structures or loops you need.
Be sure to ask about edge cases, like empty strings, empty arrays, very large
or very small inputs etc.
It’s all about putting constraints on your thinking and finding parameters
that will focus your attention towards a smaller solution space.
Keep in mind time and space complexities might not always be given. They
might just say ‘do the best you can.’
This shouldn’t deter you, but it also means there could be several ways to
solve this problem, and some are better than others.
The point is that there are a ton of conditions and constraints that will help
focus your thinking and can hint at the direction you need to go in. At the
most basic level you are gathering data.
The discs are stacked with the largest ones on the bottom and
the smallest on top.
Each time a disc is moved from one peg to another peg, counts
as one step, regardless of if the new peg is adjacent to the
old one.
The goal of the puzzle is to move the whole tower from one peg
to another peg in the smallest number of steps.
Output: Integer
You should also be asking if you actually have to simulate the motion of the
discs themselves.
This isn’t an immediately obvious question to ask, but it’s a very important
one. A lot of students start devising data structures and disc swapping
algorithms they’ll need to solve this problem, when in reality they’ve already
failed the interview.
The solution only asks for the NUMBER OF MOVES, not for you to actually
create the SEQUENCE OF MOVES. This is a BIG difference, one which
simplifies the problem tremendously. This is something all interviewees
should be on the lookout for when they are asking questions.
You should also write down everything that comes to mind: patterns you
notice, constraints you are given or things you observe. Any hunches, ideas
or thoughts should all go somewhere.
These are the building blocks of your solution. Think of this as a brain dump.
Again, the goal here is to simply get everything written down somewhere. It
doesn’t need to be super organized, but it should be legible and you should
be able to make sense of it. This step is meant to be a tool for you to use later
one when you are solving your problem.
/*
Here are the constraints:
Only move one disc at a time
Only smaller discs on larger discs
No max number of discs
Number of discso won’t be negative
Do the best you can on space and time
No need to simulate moves, just output total number
*/
Try more
Examples
An analogy for this is an imagine in which you must connect the dots. As you
add more dots and make more connections, the pattern starts to emerge.
If I give you a few numbers and ask you what sequence is this
a part of, it would be impossible for you to tell.
[1, 2]
Now if I give you the next number 3, you know a little more but
you still can’t be certain what sequence it is exactly.
[1, 2, 3]
[1, 2, 3, 5]
[1, 2, 3, 5, 8, 13]
Going back to the tower of Hanoi example, it’s a good idea to see what the
output for a given number of discs would.
Do this by manually moving discs and tallying up how many moves it takes
to get them from one peg to another.
This can be something you confirm with the interviewer, and it won’t come
across as you asking for hints, but solidifying your understanding.
Sometimes the output is obvious from the input, like sorting a list, but often
it isn’t.
What you are doing is establishing the endpoints of the problem. The start
and the finish. Your algorithm is the route the data takes between those two
points. And oftentimes there are many ways to get there.
Your function at this point is a black box. You don’t know how it works on
the inside, or what routes the data takes as it moves through the algorithm,
but you know what the inputs and outputs SHOULD look like.
It’s like knowing you are starting a road trip in San Francisco and ending up
in New York. There are different routes you can take; some are faster, safer
or more scenic. Which you choose depends on your needs, but ultimately,
all routes need to lead you to the same destination.
This is also the foundation of Test Driven Development. You have some
expectations for outputs given some inputs, and now your job is to write a
function that fulfills those different test cases.
Be rigorous and if your hypothesis is wrong, change it and try again. This is
what scientists do. They sample data, they survey the space and they try to
draw conclusions.
PRO TIP
This is where a solution you try only works for a more limited set of inputs
Trying a lot of than required.
examples also
reduces the risk of Maybe it only works for sorted inputs, or positive inputs, or lowercase letters.
overfitting In some cases, like creating a binary search algorithm in a sorted array,
these are constraints on the problem itself. But other times it means you’ve
overlooked an edge case or a set of inputs that would break your reasoning.
We see this all the time with candidates who haven’t solved a lot of problems.
Sometimes they hard code in their solutions things related to that specific
example. Like they think solving that example is the whole challenge.
Luckily in interviews the solution space tends to be small. Often you’ll only
have to try a handful of examples to get the gist, or even one might suffice.
It can be a bit of an art to know how many or what kinds of inputs to try.
Given enough different inputs this will likely give you a good sense of the
different possible cases your function will have to handle. This is kind of
how machine learning in your head works; by trying random permutations
and shuffling things around until an optimal solution just kind of “clicks”.
Another thing you can do is to be methodical with the examples you try.
Start specific and then slowly generalize your function. Make sure it works
in a specific case and then slowly expand it to handle more and more cases.
The bottom line is that trying examples will really help you understand
the problem better, and there’s a whole layer of unconscious activity that is
working in your mind under the hood that is getting you closer to the solution.
PRO TIP
Become
The
Algorithm
0 : 2 * Han
discs - 1)ion
/Iterative
ution Runconst
discs
Hanoi=
=== 0?
Linear Tim
O(N)&C odiscs - 1) + 1
/Iterative Sol
Linear TimeO
ant Space O(
unction Hano
== 0 ? 0 : et result =0
cs - 1) + 1 or(let
ve Solut resultsi == 0;
2
i<
*r
ar TimeO
nt Space
nH anoi(d
l0
If you were living during a time before personal computers and had to solve
this problem with a pen and paper how would you do it?
Forget about figuring out the concrete steps you would have to follow, or
how you would implement your solution.
This might feel weird at first, but it involves exercising something called
lateral thinking.
For example:
What if your algorithm “knew” more or less about the input data?
What would happen if you focused on solving only a part of
the problem?
What if you assumed you already had something that could
solve part of problem?
These are things present in children when they are young and learning
about the world that we as adults tend to forget. Tap into that innate sense
of discovery and lose yourself a little in the algorithm.
We can’t stress enough how much solving algorithm problems comes down
to having the correct mindset and focus, and being able to adopt them in a
controlled setting will increase your likelihood of doing so when it matters
in an interview.
Josh Waitzkin talks a lot about this in a book called “The Art of Learning”,
where he chronicles his journey of how he became a chess and martial
arts champion. It’s a fascinating read where he goes deep into the inner-
workings of the learning process.
This is also where diagramming comes in. Our monkey brains are designed
for visual input so this is another step to help bridge the gap between what
The Art of we understand intuitively and what machines understand programmatically.
Learning: An
Inner Journey If you have arrays, for example, draw them out with pointers:
to Optimal
Performance What could you do if you had one pointer?
What about 2? Or 3? Or more?
What if you don’t start both pointers at the beginning?
One could go to the end, another might be in the middle, or could serve as
some kind of tally.
These are the common patterns for when you have O(1) (constant) space
complexity constraints.
Think about what the algorithm knows and when. If you only have two
pointers, looping from the front and back, the algorithm won’t have any
sense of what’s in the middle. If you have a dictionary of key-value pairs
with all your values, you have O(1) lookup for everything. This is a common
pattern for when you have O(N) space.
For trees, graphs, matrices etc. draw out an example with all the connections.
PRO TIP You can think of this being the “View”, you are the “Controller” and the input
data/ auxiliary data structures would be the “Model” in an MVC framework.
The code for multiple Updating the view (changing the drawing), updates the model (or data) and
recursion and depth- you are the controller (the one making it all happen).
first binary tree
traversals often looks Trees are also a great way of visualizing multiple recursion functions, where
very similar. the recursive cases are represented in the branchings.
Sometimes you get problems that produce trees with unusual branching
factors, like for balanced parens permutations or for string permutations.
This will give you some insight into how you should structure a variable
number of recursive calls depending on different input cases.
3. Then think about how the call stack would treat the problem.
Because at the end of the day computers follow instructions in a linear manner even
if it makes more sense for us to conceptualize things in branched data structures.
Be the algorithm. Behave how it would behave. Think how it would ‘think’.
Distill
the Problem
To its
Essence
The goal of all this playing around, writing observations and acting as the
algorithm is to ultimately boil down the solution into one or a few sentences.
This represents the most compressed way of expressing your solution, and
being able to find the hidden gem that holds the key to solving the problem.
The best way to start to understand and practice this part of the process is
to do it with really simple algorithms.
“Iterate over the array, keeping track of the largest number you’ve seen so
far, updating it when you see a larger one.”
That’s it.
That sentence contains everything you need for solving that problem,
and being able to come up with it is the true indicator of whether you
understand how to solve the problem.
I can’t tell you how many times I’ve asked people whether they know how to
solve some algorithm they are given, and they have been unable to express
it in one sentence. Or when they’ve tried, it turned into a long, garbled run-
on sentence.
You really can’t trust your intuition here, you have to rely on your ability to
produce this sentence to check yourself.
You could write a distinct function that takes in a string of letters and then
outputs a dictionary that just displayed the count of each letter. That would
solve the first piece.
The second piece would take that dictionary as an input and have some
kind of tally of the number of letters the occur an odd number of times, and
return whether that tally was less than 2.
But all this relies on knowing some kind of keystone piece of information.
In the anagram palindrome example, that’s the observation that all
palindromes have at most one letter that occurs an odd number of times.
This one sentence, or insight is the most condensed piece to solving this
problem. Before this point, you are dealing with examples, and diagrams,
and test cases, and auxiliary variables, and writing notes/observations.
And after this you will expand your few sentences out into many lines of
code that handle looping, reassigning variables, mutating data…
And then to write this down somewhere so you can refer back to it if you
ever get lost. I’ve seen people come up with the key insight and then forget
it as they got into implementing their solution.
The first step is to create a max heap from the unsorted array. Then you
remove elements from the peak of the heap and placing them at the end,
while maintaining the max heap condition. That’s it. Two steps.
The main trick here is trying to answer “what” your function is going to do,
and to resist the urge to express “how” it’s going to do it. The how comes right
As the saying goes, “when you’re a hammer, every problem starts to look
like a nail.”
But if you pre-commit to using an array, all your LRU cache methods will be
seen through the lens of making them work with an array.
To summarize, the goal of this step is to condense all your observations and work into
only a few sentences in english, that express WHAT your algorithm will do, not HOW
it will do it. For that, we will need the next step.
Pseudocoding
A lot of people skip the pseudocode step, which in our opinion is a big mistake.
The main reason you may do this is that you think it saves time, but that’s
not how things tend to play out.
Let’s say you skip writing your pseudocode. Best case scenario, you’ve
saved yourself 5 minutes or so on your solution and you get done with the
problem a little faster.
It’s unlikely that amount of time saved will actually increase your chances of
landing the job, though saving time is always beneficial.
I’ve seen a number of people on the right track to solving a problem end
up spending 4 times longer than necessary because they got confused and
didn’t have a “source of truth” to refer back to.
They end up erasing and rewriting a lot of code, and forgetting what their
original plan was in the first place. Sometimes they end up not even solving
the problem at all when I know they were capable.
From that you will derive the second type of pseudocode, which is probably
closer to what most people think of when they imagine pseudocoding.
This is the “HOW” of the problem. It’s far more granular and it actually lays
out the steps needed to be taken to implement a solution.
Here you’ll actually be spelling out any additional variables, auxiliary data
structures or actions your algorithm will perform, like adding, swapping,
traversing, instantiating etc.
In some languages with very minimal syntax like Python, this pseudocode
will look almost like code, and often interviewers won’t even ask you to code
out your solution if you do this part well. That’s because these step-by-step
instructions should be sufficient to implement a solution in any language,
which is why many tech interviews are language-agnostic: if you can get to
this point, implementing the next part should relatively trivial.
Remember that this part is still meant to be a valuable tool for you. It should
be written in such a way that you can code out each line one at a time and
not have to hold too much information about the rest of your solution in
your head.
For example, if you are iterating through an array of numbers, where you
are removing repeats, you can think of any given number falling into one of
two categories: either you’ve seen it before, or you haven’t.
Those are all things you can do at the diagramming stage. Once you know those
pieces, you can go about figuring out how you to systematize your solution.
Let’s look at a problem like sorting an array of 0s and 1s in linear time and constant space.
You can do this with two pointers starting from the front and end. If you do that there
are only four possibilities for what the value of the array is at those pointers:
Both point to 0s
That’s it, there are not other cases to handle. In the first one you want to increment
increment the lower pointer, and leave the upper pointer where it is. In the second
case you want to decrement the second pointer, and leave the first.
In the third case you’ll increment the first and decrement the second, and in the final
case you want to swap the two values.
So think about the cases you can encounter and try to capture all of them. The goal is
to not have to think about syntax while you are solving your problem and writing out
the steps. It also lets you be less precise and able to back track if you make a mistake.
Locking yourself in with code can mean you end up erasing a lot of time consuming
work, which you want to avoid at all costs.
Time
To Do
Some
Coding
SIX_
super difficult. In fact, it should almost be trivial.
That said, it pays to be clear but concise when you’re writing your
pseudocode so that when you or your interviewer refers back to it, it’s
legible and understandable. And make sure that when you start writing
code that it’s clean and DRY. Style points are a big part of this section.
Here is the solution to Anagram Palindrome with what some notes, examples
and pseudocode to mimic what real world interview notes might look like:
// Anagram Palindrome
// Given a word, return whether some anagram exists that is a
palindrome
// Palindromes: mom, racecar, dad, bbaabb, asdffdsa
// Anagrams: abc, cba, acb,
// “mmo” => true
// “carrace” => true
// “cutoo” (outco) => false
// Brute force: Create all anagrams, see if any are palindromes
// a
// ab, ba
// abc, acb, cab, bac, bca, cba
// Runs in O(N!) which is very bad
// Key insight all palindromes have at most one character that
occurs an odd number of times
/*
Diagram:
i
Input: “carrace”
{
c:2,
a:2,
r:2,
e:1
}
*/
// 1) Count the frequencies of each letter
// 2) Make sure that no more than one letter occurs an odd
number of times
PRO TIP Code clarity here is more important than saving a few lines on the
whiteboard. Below are two different implementations of the same solution
to the above problem of sorting an array of 0s and 1s. One uses three “if”
On code DRYness statements while the other just uses one.
- it’s okay to start
with a solution that They both work, but it’s not immediately clear why the more “clever”
doesn’t use the solution works. It makes it harder to understand and judge for yourself
fewest number of whether you have the right answer, and it also introduces the possibility of
if/else statements bugs and other mistakes.
or the most clever
optimizations
start++;
end--;
}
else if(arr[start] === 1 && arr[end] === 0) {
[arr[start], arr[end]] = [arr[end], arr[start]];
}
else if(arr[start] === 1 && arr[end] === 1) {
end--;
}
}
return arr;
}
}
return arr;
}
// VERY clever solution, with only one if case
function sortBits(arr) {
One important thing you should think about at this point is syntax.
In fact, syntax is the main reason for separating the pseudocode from the
code in the first place: so you don’t have to worry about syntax while you’re
reasoning through the solution.
Every bit of your brain’s CPU is valuable and while it’s focused on solving
the problem it shouldn’t have to be worrying about language-specific
idiosyncrasies.
And if there is a bug, test the different components of your solution with
strategic print statements. The point is to minimize the surface area the bug
might be in, but checking to see if the output of each part of the algorithm
is correct. If you structured your solution correctly, this should be easy to do.
It’s kind of like performing a binary search for finding the bug: successively
cutting your search space in half is the fastest way to find it.
The rules are simple, if you see a function, add a block the stack and the
state of all the variables at that call-time. If you see a return statement, pop
off a block, replacing the function call in the previous block on the stack
with the value you are returning, if you are returning one at all.
And then try edge cases to make sure your final solution is robust. What
happens if your input is 0? Or negative? Or an empty array/object? What if
every value is the same, or if they are very large etc. Stress test your solution.
This part is all about being precise and getting into the nuts and bolts of
your solution to make sure everything is working properly.
If you wrote a function that produces all the combinations of strings of 0s and
PRO TIP 1s of a certain length, write a function that only outputs the combinations
that don’t have consecutive ones.
You can also visualize Or if you wrote the code for a min heap, try writing the code for a max heap.
the call stack, If you know quicksort, try mergesort. It’s kind of like “muscle confusion”
which is especially training in the gym. You’re trying to reduce overfitting to a particular problem.
important for
recursive problems.
Also look at other solutions to the same problem. Studying other people’s
code will help you progress faster than if you just stick to solving problems
yourself. Everyone has a different way of thinking, and you’d be surprised how
much you can learn and optimize your own code by looking at other people’s.
There are a limited number of patterns that cover a good chunk of all the
problems that get asked. If you internalize those, you’ll be able to decide
what tools are best for the job.
This will do far more good than a few days of cramming every once in a
while, for the same reason that one really hard workout won’t get you in
shape as well as a lot of consistent sustainable workouts. But don’t spend
more than 30min to an hour spinning your wheels.
If you are completely stuck, look at the solution, learn it and then come back
to the problem a few days later. Over time, you will learn. And often you’ll be
95% of the way there, and there is a small subtle change that needs fixing.
So don’t stress; this is a marathon, not a sprint and burning out will not get
you anywhere. Step away from the problem and come back, you might be
surprised how much your unconscious does; it really does work, try it.
Confidence is also a huge part of doing well on interviews, but that is tied
to how competent you feel. And this just gets built over time with practice.
As Ben Horowitz said in his book, ‘The Hard thing about Hard Things’:
“sometimes there are no silver bullets, only lead bullets”.
PRO TIP
And finally, going back to “The Art of Learning”, you are trying to learn “form
to leave form.”
We do not expect you to think about each of these steps exactly when you
see a new algorithm, you should naturally internalize this line of thinking.
Test things, move forward and if you hit a dead end you can move back to
a previous step, write pseudocode, then diagram, then ask questions and
so fourth.
Instead of a rigid structure, it should become more organic with time and
practice, personalized to your style of thinking.
Similarly, a computer (and even humans) can’t really understand the inner
workings of our brains. It just kind of pattern matches and comes up with
a solution.
The amazing thing is that we are able to bridge that gap, by constructing
programming languages and specific ways of thinking and interacting with
computers that allow us to encode the solutions to abstract problems in a
step-by-step format.