• Back

My Mixed Feelings About Coding Interviews

intro

These days, coding interviews are like security checks before getting into big companies, and they keep getting more competitive. It is not uncommon to hear someone fail coding rounds multiple times before eventually landing a decent job. LeetCode used to have only a few hundred questions; now it is enormous. I got dinged twice myself at the beginning of 2021, which is why I started rethinking what I should do about this trend, whether I like it or not.

I have heard many people, usually mid-senior level and often the losers of this coding game, argue that coding interviews are not reflective of a person's actual capability. There is truth to that. However, in a market-driven economy, coding interviews are still a proven and efficient way to screen people. Back when I was interviewing candidates at Bosch, I understood the pain of choosing among many qualified people. A coding round can act like an efficient weeder for the less prepared candidates. It is imperfect, but it is also practical.

The real question is how we should navigate a market that already works like this. Coding interview skills are not exactly the same as actual coding skills because the game is heavily algorithmic. In my years of doing ML research, I almost never needed a heap or fancy data structures to process my day-to-day work. Yet on a coding prep site, there is always a decent chance you will run into a heap question. Should I complain? Should I just keep doing research, publish papers, and stay in academia? Or should I get on the train and do LeetCode as well?

I am more of a realist, so my answer is to tackle LeetCode questions with a clear structure and to use that process to rebuild a stronger algorithmic foundation. ML, stats, or math skills are like the "devil fruit" power, but coding skills are still the basic combat power you rely on in a rapidly evolving tech world.

I am taking my baby steps.

P.S. updated 8-22-2021, 2-18-2022, 4-28-2022, 10-22-2022, 4-22-2023, 1-17-2024, 3-24-2026:

After doing LeetCode for 2+ years, I have several important epiphanies:

  • In the LLM era, the bar is shifting. Memorizing answers matters less than before; explaining tradeoffs, spotting edge cases, and showing real understanding matter more.
  • Interview coding is different from research coding; do it or not do it, stop whining since there's not much use complaining;
  • Doing questions by categories is way more efficient than doing them randomly. Remember common patterns!
  • Need to take notes and Review! we human beings eventually succumb to the Ebbinghaus forgetting curve
  • Discipline, discipline, and discipline! Laziness is the biggest enemy.
  • Write succinct code. Short code, but readable. Try to cut to the chase ASAP instead of trying brute force first!
  • Try to explain every major implementation step or motivation. Carve out the data structure, pseudo code, and try to list out several edge cases.
  • Try to talk like a likeable person during interview: positive, smart, good technical habits, and willingness to listen and debug.
  • Eventually, muscle memory works faster than the brain. There are patterns in each category, and some of them simply need repetition.
  • Thought process is more important than actual coding, need to let people see why you are doing what.
  • In the GPT-4 / LLM era, I do not see much value in merely memorizing answers, since these tools can outperform that kind of preparation very easily. What still matters is demonstrating your understanding of the problem and the quality of your thought process.
  • I managed to keep doing Leetcode's daily challenge after I found where I want to work at. It simply became a habit, which does not take much mental power to execute every day. In this way, you can keep at least 70-80% of your peak coding performance, and still think independently without too much help from GPT tools.
  • Array:

    Array Questions are prevalent but could be combined with other categories Array questions I practiced

  • Two Pointers are actually large enough of a topic that worth its own page.
  • Hashmap(2 sums)
  • Graph problem hidden in words
  • Stack:

    Stack questions on leetcode is not very intuitive. The questions, if not thought through carefully, can easily fool you into O(n^2) solutions. The correct answers are most of times O(n). Stack questions I practiced

  • Remove K digits Trick: find the first node of descending orer
  • One trick seems to be in the while loop within the for loop that goes through the input.
  • The other trick seems to be using 2-dimensional stack instead of one
  • Next time, if I see anything that requires to keep track of the previous elements, I should think about using the LIFO strategy to host that many elements in the stack and count them through popping them out of the stack.

    Heap:

    Heap questions are intrinsically more challenging. Leetcode lists almost all the heap questions above medium difficulty. But once you get used to them, everything would look very similar to you. Especially, once you are familiar with the idea of keeping priority queue in 2D heap and how to use 2 heaps to tackle a moving stream of data, all other heap questions would be similar derivative or variant questions. Heap questions I practiced

  • The core idea about heaps is to leverage its natural sorting capability. Top K xxx for xxx...
  • This is a great resource for Heap
  • How to heapify, why heap is better than sorted list, merge many already-sorted input streams into a single sorted output stream.

    Tree:

    Tree questions are mostly revolving around BST(Binary Search Trees), and to grasp BST well, I really think the best way is to go back and read CLRS thoroughly and understand every bit they say about BSTs. Tree questions I practiced

  • Search O(height)
  • Find min, max, successor, predecessor O(height)
  • Insert O(height)
  • Delete: This one is tricky since it can involve 2 different cases. still O(height) thoTransplant:replaces one subtree as a child of its parent with another tree.
  • Rotation: This operation is needed for red-black tree or more advanced tree structures.
  • Tree questions involve BFS, DFS, Dijkstra's algorithm, and lots lots of recursions. This topic is one of the most important topic and definite worth spending more time on it. I took 2 weeks going through many tree-based questions, and I still feel I am not mastering tree 100%. There are couple of important notes:
  • Remember sub-category of tries, treaps.
  • Be sure to get the recursion base case/ terminate case first. e.g. (#543 diameter of the tree, #199 rightside)
  • Get comfortable to use Stack for DFS and queue for BFS, they run faster and more intuitive to understand
  • Serialize/deserialize , construct tree from preorder+inorder list, postorder+inorder list
  • Get comfortable with recursion.
  • Matrix related:

    Matrix questions are somehow related and they share lots of similarities. Matrix questions I practiced

    Graph:

    Graph questions are involved, but I feel once I know the pattern, it's a lot more within control

  • Disjoint Set, Union Find: Union function and find function, quick union, quick find, or union with rank
                           
                           def isBipartite(self, graph: List[List[int]]) -> bool:
                                length = len(graph)
                                parent = [i for i in range(length)]
    
                                def find(x):
                                    if x!=parent[x]:
                                        parent[x]=find(parent[x]) #rank compression
                                    return parent[x]
    
                                def union(x,y): # union by rank can balance the tree
                                #whichever is taller tree will dominate
                                    px,py=find(x),find(y)
                                    if px!=py:
                                        parent[px]=py
    
                                for i in range(length):
                                    pari=find(i)
                                    for j in graph[i]:
                                        #check connected or not
                                        if find(j)==pari:
                                            return False
                                        union(graph[i][0],j)
                                return True
                           
                        
  • BFS Template(queue, priority queue)
  •                        
                           def cloneGraph(self, node: 'Node') -> 'Node':
                                if not node:
                                    return node
                                queue = [node]
                                seen = {}
                                seen[node] = Node(node.val, [])
                                while queue:
                                    curr_node = queue.pop(0)
                                    for neighbor in curr_node.neighbors:
                                        if neighbor not in seen:
                                            seen[neighbor] = Node(neighbor.val, [])
                                            queue.append(neighbor)
                                        seen[curr_node].neighbors.append(seen[neighbor])
                                return seen[node]
                           
                        
  • DFS Template
  •                        
                           class Solution:
                            def isValidBST(self, root: Optional[TreeNode]) -> bool:
    
                                if root is None:
                                    return True
    
                                dfs_stack = [root]
                                self._add_all_left_nodes(root, dfs_stack)
    
                                prev_val = dfs_stack[-1].val - 1 # could use math.min as well
    
                                while len(dfs_stack) > 0:
                                    node = dfs_stack.pop()
                                    if prev_val < node.val:
                                        prev_val = node.val
                                    else:
                                        return False
                                    if node.right is not None:
                                        dfs_stack.append(node.right)
                                        self._add_all_left_nodes(node.right, dfs_stack)
    
                                return True
    
    
                            def _add_all_left_nodes(self, node, s):
                                while node.left is not None:
                                    s.append(node.left)
                                    node = node.left
                           
                    
  • Dijkstra's, A-Star
  • A good start is to build adjacency list
  • Lots of word, string, char type of questions are implicit Graph questions, like word ladder and etc.
  • Dynamic Programming:

    Base Case; Transition function; How to think about breaking a problem down to subproblems. After doing quite a few of them, I found that starting with recursion is usually the easiest, then adding memoization is not difficult, and finally coming up with tabular dp will be natural. The core is to pin down the state-transition function as early as possible. DP questions I practiced

    Backtrack:

    Backtracking is a tricky categrory, but could be solved using a template, 1.Subsets 2. Subsets II 3.Permutations 4. Permutations II 5. Combinations 6. Combination Sum II 7. Combination Sum III 8. Palindrome Partition Backtrack questions I practiced

  • The core idea about backtracking is it incrementally builds candidates to the solutions, and abandons a candidate ("backtrack") as soon as it determines that this candidate cannot lead to a final solution.
                               
                               def backtrack(candidate):
                                    if find_solution(candidate):
                                        output(candidate)
                                        return
    
                                    # iterate all possible candidates.
                                    for next_candidate in list_of_candidates:
                                        if is_valid(next_candidate):
                                            # try this partial candidate solution
                                            place(next_candidate)
                                            # given the candidate, explore further.
                                            backtrack(next_candidate)
                                            # backtrack
                                            remove(next_candidate)
                               
                        
  • This is a great resource for Backtracing
  • Still this type of questions are very challenging even after practise, since they would usually involve O(2^n) complexity.


    Comments

    Want to leave a comment? Visit this post's issue page on GitHub (you'll need a GitHub account).