Why so many Leetcode solutions are terrible

As the 2019 recruiting season is in full swing, I’ve found myself spending a lot of time on Leetcode to keep my algorithms and data structures knowledge sharp, like many of my peers. Unfortunately though, I see so many examples of solutions that, in my opinion, really miss the point of what good code is. In this post, I’m going to look at the relatively simple Add Two Numbers problem to illustrate my point.

The Problem

“You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.”

This problem is one of the simpler ones on Leetcode, but that actually suits this blog post quite well since a harder problem would detract from the main take-aways of the post. To make sure we’re all on the same page, consider this graphic that Leetcode provides under the Solution tab:

Already in this graphic can we see the beginnings of the Leetcode-provided solution that I have an issue with.

Already in this graphic can we see the beginnings of the Leetcode-provided solution that I have an issue with.

In an interview setting, if the interviewee can reach this understanding of the problem posed, the solution is actually relatively straight-forward. The three-step process is basically:

(1) Decode the linked-list representations into integers.

(2) Add the integers.

(3) Encode the sum into the linked-list representation and return it.

So, I implemented my solution and then I took a look at the Discussion tab to compare my own solution with others’ (don’t worry, we’ll look at my solution a bit farther down).

Discussion solutions

The most popular Python 3 answer that I came across reads as follows:

01 # Definition for singly-linked list.
02 # class ListNode:
03 #     def __init__(self, x):
04 #         self.val = x
05 #         self.next = None
06 
07 class Solution:
08  # @return a ListNode
09  def addTwoNumbers(self, l1, l2):
10      carry = 0
11      root = n = ListNode(0)
12      while l1 or l2 or carry:
13          v1 = v2 = 0
14          if l1:
15              v1 = l1.val
16              l1 = l1.next
17          if l2:
18              v2 = l2.val
19              l2 = l2.next
20          carry, val = divmod(v1+v2+carry, 10)
21          n.next = ListNode(val)
22          n = n.next
23      return root.next

This solution, by the way, is titled “Clear python code, straight forward.” Now, this blog post is not about shaming Leetcode contributors, so I do want to point out that I think this solution is really cool (it is impressive how it solves the solution with a single pass over both of the given lists). The solution, however, is neither clear nor straight-forward, because it is largely unreadable. I should mention too, that this is not a one-off in the discussions page but that this theme is present throughout Leetcode. For the curious reader, see C++ solution, easy to understand (spoiler alert: it is not particularly easy to understand), and here is the discussion tab of the problem. Even the solution provided by Leetcode is completely void of comments and use the same anti-modular design.

Anyway, in order to get back to the problem, let’s decode the solution a little:

01 # Definition for singly-linked list.
02 # class ListNode:
03 #     def __init__(self, x):
04 #         self.val = x
05 #         self.next = None
06 
07 class Solution:
08  # @return a ListNode
09  def addTwoNumbers(self, l1, l2):
10     
11      carry = 0
12      root = n = ListNode(0)
13         
14         # Iterate forward so long as at least one given list is still going and carry is non-zero
15      while l1 != None or l2 != None or carry != 0:
16         
17          # Let's grab the values (and iterate forward) of each node that is non None
18          v1 = v2 = 0
19          if l1:
20              v1 = l1.val
21              l1 = l1.next
22          if l2:
23              v2 = l2.val
24              l2 = l2.next
25                 
26          # Sum the values of each node with the carry
27             sum = v1 + v2 + carry
28             
29             # Get the new carry and the value to store in the node of the sum list
30             # (dividing by 10 reduces the value to a single digit)
31             # (and gets a carry for the next iteration/digit)
32             carry, val = divmod(sum, 10)
33             
34             # Store value and iterate forward
35          n.next = ListNode(val)
36          n = n.next
37      return root.next

Hopefully, my comments have illuminated the solution enough that it is understandable. Even with this, however, the solution is still hard to read. Questions that I had to consider for quite a while include: How exactly is the result list created and iterated (lines 12, 35-36)? How is the while loop terminated (line 15)? What is happening on line 32? Why is it happening?

Beyond legibility, there is a much bigger problem with the code: modularity. In an interview setting, follow-up questions are not uncommon. Consider the amount of work required to accommodate the following new criteria:

(1) What if the given linked lists were instead provided with the digits in the correct order?

(2) What if an arbitrary number of numbers were given instead of just two?

(3) What if instead of adding the numbers, you had to subtract one number from the other?

I’m definitely not claiming that the code couldn’t be adjusted to deal with any of these new critera (or, even harder, a combination of (1) and (2) or of (1) and (3)). It would, however, be tedious and prone to lots of errors. Let’s take a look at the solution I provided, also in Python 3.

01 # Definition for singly-linked list.
02 # class ListNode:
03 #     def __init__(self, x):
04 #         self.val = x
05 #         self.next = None
06 from collections import deque
07 
08 class Solution:
09     # Takes ListNode integer representation and returns an integer
10     def decodeNumber(node: ListNode) -> int:
11             d = deque()
12         
13             # For each digit, append on the left of the deque to build the number
14             # (appening on left to account for digits arriving in reverse order)
15             while node != None:
16                 d.appendleft(str(node.val))
17                 node = node.next
18         
19             # Join deque into integer and return
20             num = ''.join(list(d))
21             return int(num)
22     
23     # Takes an integer and returns a ListNode integer representation             
24     def encodeNumber(num: int) -> ListNode:
25         
26         # Let's build a deque from the given integer
27         d = deque(str(num))
28     
29         # Deepest value is the most significant digit
30         node = ListNode(int(d.popleft()))
31     
32         # As long as there are digits, add them (from left) as a new node
33         while len(d) > 0:
34             newNode = ListNode(int(d.popleft()))
35             newNode.next = node
36             node = newNode
37     
38         return node
39     
40     def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
41         
42         # Decode the two given numbers for a resulting integer
43         resultNum = Solution.decodeNumber(l1) + Solution.decodeNumber(l2)
44         
45         # Encode the resulting integer to the given format
46         return Solution.encodeNumber(resultNum)

The first thing you’ll notice is how I defined two new functions: encodeNumber and decodeNumber, where the bulk of the work is taking place. In terms of legibility, the immediate benefit is that reading the main function takes no more than a minute. Additionally, to anyone familiar with queues and linked lists, the en- and decoding functions are also very legible since their purposes are narrow and well-defined (no confusing divmod’s in the middle of the linked list operations).

In terms of configurability, consider the follow-up questions posed earlier:

(1) What if the given linked lists were instead provided with the digits in the correct order?

Replace each instance of popleft and appendleft with their analogues pop and append, respectively. This simply flips the order of the digits in the queue. This, by the way, is what I did for Add Two Numbers II on Leetcode, solving it in less than a minute.

(2) What if an arbitrary number of numbers were given instead of just two?

Simply call decodeNumber for each given number and return the encoded sum.

(3) What if instead of adding the numbers, you had to subtract one number from the other?

Simply replace the plus operator in the main function with the minus operator.

As you can hopefully see, the difference that taking a couple of minutes to think about how I could separate the different aspects of my code made a huge difference.

Seeing solutions like the example I brought up can be really dissuading (it definitely makes me feel stupid when I don’t understand a short snippet of a language I am supposedly familiar with). I hope I have gotten my point across though, that code legibility lands more-so on the shoulders of the author than the reader. So, keep that and modularity in mind as you keep practicing on Leetcode and best of luck with the 2019 recruiting season!

Disclaimer: I am not a recruiter. I am not an algorithms and data structures expert. I do not hold an advanced degree in Computer Science. Hell, I’m not even a software engineer; I do security and spend comparatively little time programming. These are just my personal observations and I am sure there are those who disagree. Your mileage may vary.

Recruiters: If you have an exciting information security internship that you think I might be right for, please contact me.

Sten SjöbergComment