You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tech-interview-handbook/contents/coding-interview-techniques.md

18 KiB

id title description keywords sidebar_label
coding-interview-techniques Top techniques to approach and solve coding interview questions Learn methods to find solutions for coding interview problems and optimize their time and space complexity
how to approach a coding interview question
how to solve any coding interview question
coding interview practice
coding interview questions
optimize time complexity
optimize space complexity
optimize time and space complexity
Techniques to solve coding interview questions

The biggest fear most candidates will have during a coding interview is: what if I get stuck on the question and don't know how to do it? Fortunately, there are structured ways to approach coding interview questions that will increase your chances of solving them. From how to find a solution or approach, to optimizing time and space complexity, here are some of the top tips and best practices that will help you solve coding interview questions.

How to find solutions to coding interview problems

When given a coding interview question, candidates should start by asking clarifying questions and discussing a few possible approaches with their interviewers. However, this is where most candidates tend to get stuck. Thankfully, there are ways to do this in a structured manner.

Note that not all techniques will apply to every coding interview problem. As you apply these techniques during your practice, you will develop the intuition for which technique will be useful for the problem at hand.

1. Visualize the problem by drawing it out

Ever wondered why coding interviews are traditionally done on whiteboards and videos explaining answers to coding questions tend to use diagrams? Whiteboards make it easy to draw diagrams which helps with problem solving! A huge part of coding is understanding how the internal state of a program changes and diagrams are super useful tools for representing the internal data structures state. If you are having a hard time understanding how the solution is obtained, come up with a visual representation of the problem and if necessary, the internal states at each step.

This technique is especially useful if the input involves trees, graphs, matrices, linked lists.

Example

How would you return all elements of a matrix in spiral order? Drawing out the matrix and the path your iterator needs to take will help tremendously by allowing you to see the pattern.

2. Think about how you would solve the problem by hand

Solving the problem by hand is about solving the problem without writing any code, like how a non-programmer would. This already happens naturally most of the time when you are trying to understand the example given to you.

What some people don't realize is that sometimes a working solution is simply a code version of the manual approach. If you can come up with a concrete set of rules around the approach that works for every example, you can write the code for it. While you might not arrive at the most efficient solution by doing this, it's a start which will give you some credit.

Example

How do you validate if a tree is a valid Binary Search Tree without writing any code? You first check if the left subtree contains only values less than the root, then check that the right subtree contains only values bigger than the root, then repeat for each node. This process seems feasible. Now you just have to turn this process into code.

3. Come up with more examples

Coming up with more examples is something useful you can do regardless of whether you are stuck or not. It helps you to reinforce your understanding of the question, prevents you from prematurely jumping into coding, and having multiple examples is helpful when verifying your solution later. Coming up with more examples and then solving them by hand also helps you to identify a pattern which can be generalized to any input, which is the solution!

4. Break the question down into smaller independent parts

If the problem is large, start with a high-level function and break it down into smaller constituting functions, solving each one separately. This prevents you from getting overwhelmed with the details of doing everything at once and keeps your thinking structured.

Doing so also makes it clear to the interviewer that you have an approach, even if you don't manage to finish coding all of the smaller functions.

Example

The Group Anagrams problem can be broken down into two parts - hashing a string, grouping the strings together. Each part can be solved separately with independent implementation details. You could start off with this code:

def group_anagrams(strings):
  def hash(string):
    pass

  def group_strings(strings_hashes):
    pass

  strings_hashes = [(string, hash(string)) for string in strings]
  return group_strings(strings_hashes)

And proceed to fill in the implementation of each function. However, do note that sometimes the most efficient solutions will require you to break some abstractions and do multiple operations in one pass of the input. If your interviewer asks you to optimize based on your well-abstracted solution, that is one possible path forward.

5. Apply common data structures and algorithms at the problem

Unlike real-world software engineering where the problems are usually open-ended and might not have clear solutions, coding interview problems tend to be smaller in nature and are designed to be solvable within the duration of the interview. You can also expect that the knowledge required to solve the problem is not out of this world and they would have been taught during college. Thankfully, the number of common data structures and algorithms is finite and a hacky approach which works from my experience is to try going through all the common data structures and applying them to the problem.

These are the data structures to keep in mind and try, in order of frequency they appear in coding interview questions:

  • Hash Maps - Useful for making lookup efficient. This is the most common data structure used in interviews and you are guaranteed to have to use it
  • Graphs - If the data is presented to you as associations between entities, you might be able to model the question as a graph and use some common graph algorithm to solve the problem
  • Stack and Queue
  • Heap - Question involves scheduling/ordering based on some priority. Also useful for finding the max K/min K/median elements in a set
  • Tree/Trie - Do you need to store strings in a space-efficient manner and look for the existence of strings (or at least part of them) very quickly?

Routines

  • Sorting
  • Binary search - Useful if the input array is sorted and you need to do faster than O(n) searches
  • Sliding window
  • Two pointers
  • Union find
  • BFS/DFS
  • Traverse from the back
  • Topological Sorting

In future we will add tips on how to better identify the most relevant data structures and routines based on the problem.

How to optimize your approach or solution

After you've come up with an initial solution to the coding interview problem, your interviewer would most likely prompt you to optimize the solution by asking "Can we do better". The following techniques help you further optimize the time and space complexity of your solution:

How to optimize time complexity

1. Identify the Best Theoretical Time Complexity of the solution

The Best Theoretical Time Complexity (BTTC) of a solution is a time complexity you know that you cannot beat.

Some simplified examples:

  • The BTTC of finding the sum of numbers in array is O(n) because you have to look at every value in the array at least once
  • The BTTC of finding the number of groups of anagrams is O(nk) where n is the number of words and k is the maximum number of letters in a word because you have to look at each word at least once and look at each character in each word at least once
  • The BTTC of finding the number of islands in a matrix is O(nm) where n is the number of rows and m is the number of columns because you have to look at each cell in the matrix at least once

Why is it important to know the BTTC? So that you don't go down the rabbit hole of trying to find a solution that is faster than the BTTC. The fastest practical solution can only ever be as fast as the BTTC, not faster than the BTTC. The BTTC is not necessarily achievable in practice (hence theoretical), it just means you can never find a real solution that is faster than it. If your initial solution is slower than the BTTC, there could be opportunities to improve such that you can attain the BTTC (but not always the case). It wouldn't hurt to mention the BTTC to your interviewer, which will be taken as a positive signal and also to remind yourself that you should not try to come up with something faster than the BTTC.

Some people might think that the BTTC is simply the total number of elements in a data structure, because you need to go through each element once. This is not always true. The most famous example would be finding a number in a sorted array of numbers. The sorted property changes things a whole lot:

  • Finding a number would be O(log(n)) because you can use a binary search.
  • Finding the largest number would be O(1) because it is the last value in the array.

This is why it is important to pay attention to every detail given about the question. Be careful not to determine the incorrect BTTC due to lack of attention to the question details!

With the correct BTTC determined, you now know the time complexity of the optimal solution lies between your initial solution and the BTTC and can work your way towards it. If your solution already has the BTTC and the interviewer is asking you to optimize further, there are usually two things they are looking out for:

  • Do even less work. Your solution could be O(n) but making two passes of the array and the interviewer is looking for the solution that uses a single pass.
  • Use less space. Refer to the section below on optimizing space complexity.

2. Identify overlapping and repeated computation

A naive/brute force solution often executes the same operation over and over again. When the code is doing an expensive operation that has been done before, take a moment to step back and consider if you can reuse results from previous computations. Dynamic programming (DP) is the most obvious type of questions you can entirely leverage past computations. There are non-DP questions that can leverage this technique too, although not as straightforward and might require a preprocessing step.

Example

The Product of Array Except Self question is a good example of a problem which contains overlapping/repeated work. To get the value for an index, you need to multiply the values at all other positions. Doing this for every value in the array would take O(n2) time. However, see that:

  • result[n]: Product(nums[0] … nums[n-1]) * Product(nums[n + 1] … nums[N - 1])
  • result[n + 1]: Product(nums[0] … nums[n]) * Product(num[n + 2] … nums[N - 1])

There's a ton of duplicated work in computing the result[n] vs result[n + 1]! This is an opportunity to reuse earlier computations made while computing result[n] to compute result[n + 1]. Indeed, we can make use of a prefix array to help us arrive at the final solution in O(n) time at the cost of more space.

3. Try different data structures

Choice of data structures is key to coding interviews. It can help you to reach a solution for the problem, it can also help you to optimize your existing solution. Sometimes it's worth going through the exercise of iterating through the data structures you know once again.

Is lookup time slowing your algorithm down? In general, most lookup operations should be O(1) with the help of a hashmap. If the lookup operation in your solution is the bottleneck to your solution's time complexity, more often than not, you can use a hashmap to optimize the lookup.

Example

The K Closest Points to Origin question can be solved in a naive manner by calculating the distance of each point, sorting them and then taking the K smallest values. This takes O(nlog(n)) time because of the sorting. However, by using a Heap data structure, the time complexity can be reduced to O(nlog(k)) as adding/removing from the heap only takes O(log(k)) time when the size of the heap is capped at K elements. Changing the data structure made a whole ton of difference to the efficiency of the algorithm!

4. Identify redundant work

Here are a few examples of code which is doing redundant work. Although making these mistakes might not change the overall time complexity of your code, you are also evaluated on coding abilities, so it is important to write as efficient code as possible.

Example

Don't check conditions unnecessarily. These are examples where the second check is redundant.

  • if not arr and len(arr) == 0
  • x < 5 and x < 10

Don't invoke methods unnecessarily

  • len(arr) in many parts of the function. If the len doesn't change, declare a variable at the start

Minimize work inside loops / Don't redo work you have already done

  • Transforming a string within a for loop. Transform the string outside the for loop!

Be lazy. Use lazy evaluation - only evaluate expressions when you need it

  • slow() or fast()
  • likely() and unlikely()

Lazy initialization - only create objects when you know you're going to need them

Do early termination. Stop after you already know the answer.

Consider this basic question "Determine if an array contains an even number" and the code for it:

def has_even(nums):
  has_even = False
  for num in nums:
    if num % 2 == 0:
      has_even = True
  return has_even

Does this code work? Definitely. Is this code as efficient as it can be? Nope. We only need to know if an even value exists in the array. We can stop iterating as soon as we know that there exists an even value.

def has_even(nums):
  for num in nums:
    if num % 2 == 0:
       return True
  return False

Most people already know this and already do this outside of an interview. However, in a stressful interview environment, people tend to forget the most obvious things. Terminate early from loops where you can.

How to optimize space complexity

Most of the time, time complexity is more important than space complexity. But when you have already reached the optimal time complexity, the interviewer might ask you to optimize the space your solution is using (if it is using extra space). Here are some techniques you can use to improve the space complexity of your code.

1. Changing data in-place/overwriting input data

If your solution contains code to create new data structures to do intermediate processing/caching, memory space is being allocated and can sometimes be seen as a negative. A trick to get around this is by overwriting values in the original input array so that you are not allocating any new space in your code. However, be careful not to destroy the input data in irreversible ways if you need to use it in subsequent parts of your code.

A possible way which works (but you should never use outside of coding interviews) is to mutate the original array and use it as a hashmap to store intermediate data. Refer to the example below.

Note that in Software Engineering, mutating input data is generally frowned upon and makes your code harder to read and maintain, so changing data in-place is mostly something you should do only in coding interviews.

Example

The Dutch National Flag problem could be easily solved with O(n) time and O(n) space by creating a new array and filling it up with the respective values in a sorted fashion. As an added challenge and space optimization, the interviewer will usually ask for an O(n) time and O(1) space solution which involves sorting the input array in-place.

An example of using the original array as a hash table is the First Missing Positive question. After the first for loop, all the values in the array are positive, and you can indicate presence of a number by negating the value at the index corresponding to the number. To indicate 4 is present, negate nums[4].

2. Change a data structure

Data structures again!? Yes, data structures again! Data structures are so fundamental to coding interviews and mastery of it makes or breaks your interview performance. Are you using the best data structure possible for the problem?

Example

You're given a list of strings and want to find how many of these strings start with a certain prefix. What's an efficient way to store the strings so that you can compute your answer quickly? A Trie is a tree-like data structure that is very efficient for storing strings and also allows you to quickly compute how many strings start with a prefix.

Next Steps

If you haven't already, I recommend you check out my free structured guide for coding interviews, which contains step by step guidance such as: