Against the Clock: Succinct, Clever, and Elegant vs. Longer, Manual, and Simpler

Original date: Nov 16, 2017

Many times there are multiple approaches for solving a particular coding challenge. When time is limited, how do you know which approach you should take? The more elegant solution that might require a little more mastery and familiarity of the problem but result in a more impressive and clever-looking answer? The more manual, labor intensive process that allows you to attack the problem one layer at a time but omits demonstrating your prowess over specific, tailor-made methods? When the clock is ticking in an interview, it can be hard to decide, especially if you encounter a problem outside of your comfort zone.
During timed coding challenges, whether for a job interview, a Launch School assessment, or any situation in which you code for an audience, our LS instructors have reminded us frequently that our problem-solving ability will be working at less than peak capacity. Especially in the beginning, coding in front of others adds another dimension to the challenge. Nerves combined with the unknown nature and content of the forthcoming problem heighten the tension beyond our routine coding practice, and naturally this adds a layer of increased difficulty to the thought process.
In light of this, we face the choice between an approach that might result in a more optimal solution that demonstrates better knowledge over specific methods that enable us to solve a particular problem as succinctly and elegantly as possible, which could impress our audience and/or evaluators, or the alternate approach in which we use more manual methods that allow us to dissect the problem piece by piece and give us greater flexibility if we decide to change direction if the results aren’t what we expected.
Like many students at Launch School, I worked through the Small Problems exercises more than once — three to four times each, in fact. Looking back on and comparing the approaches I took, I see how some of these solutions could work better than others during a timed interview, especially under pressure.

Case 1: Triangle Sides
In the Triangle Sides problem, we are given three integers that represent the length of the sides of the triangle. Our goal is to return a symbol object of the appropriate category of triangle: equilateral, isosceles, or scalene.
Triangle: Solution 1
def triangle(s1, s2, s3)
  sides = [s1, s2, s3].sort
  return :invalid if sides.include?(0) || sides.min + s2 <= sides.max
  if sides.uniq.size == 1
    :equilateral
  elsif sides.uniq.size == 2
    :isosceles
  elsif sides.uniq.size == 3
    :scalene
  end
end
Triangle: Solution 2
def triangle(s1, s2, s3)
  sides = [s1, s2, s3].sort!
  
  return :invalid if (sides.min + s2) <= sides.max
  case
  when sides.include?(0)                then :invalid 
  when s1 == s2 && s2 == s3 && s1 == s3 then :equilateral
  when s1 == s2 || s1 == s3 || s2 == s3 then :isosceles
  when sides.uniq == sides              then :scalene
  else :invalid
  end
end
In the example of the triangles, I formulated the more elegant Solution 1 on a night when I felt in the zone, at the peak of my skill. The more complicated and less elegant solution actually resulted from a subsequent pass through the practice problems. Not wanting to peek back at my previous solution from a week or two earlier but unable to remember exactly how I’d solved it, I fell back to working with what I knew would work — a solution in which I compared sides.
In Solution 1 I first defined the sides variable and set it to reference an array containing each of the three sides of the triangle. I wrote a guard clause to return :invalid if either of two conditions were present that would result in an impossible triangle: a side equal to zero or if the sum of the two shorter sides was not greater than the longest. After that, my solution calls the Array#uniq on the sides array, and then calls the Array#size method on that result and compares that integer to the appropriate integer that corresponds to the :equilateral, :isosceles, or :scalene return value.
As mentioned previously, days later on a subsequent workthrough of the problems I could not recall that solution which seemed to work so well on the earlier attempt. I knew that I needed to compare the sides of the triangle but couldn’t remember the particular methods I’d used or the exact approach. I remembered watching the videos on watching others code and how when in doubt, go back to basics when solving a problem.
With that in mind, in Solution 2 I also initialized a sides variable and set it to an array containing the sides of the triangle, and I wrote a case statement to evaluate the different possible scenarios. The big difference from Solution 1lies in the methods used to handle each case. Whereas I’d called Array#uniq and Array#size and compared with an integer matching the number of unique sides in the triangle categories, in Solution 2 I applied one of the simplest — if repetitive comparisons possible: I manually compared all three sides using the == comparison and the logical && and || operators in order to determine to which category the triangle belonged. Certainly less glamorous, but in absence of the more clever solution, it got the job done.
Which solution would I recommend for a timed interview? Again, the first solution looks better, and if I finished quickly enough to refactor or if I was able to instantly think of the sides.uniq.size comparison, I might attempt the more impressive Solution 1. However, in the more likely scenario where this question was only the first of multiple interview questions, I would recommend the solution that required writing more lines of code but seem much more obvious while thinking the problem through.
Case 2: Rotate Array
In our second case of the Rotate Array problem, the choice between the refined approach vs. back to basics is even more pronounced. In this problem we want to return a new array in which the first element of the original array is removed from the front of the new array and added to the end. So the array [7, 3, 5, 2, 9, 1] would become [3, 5, 2, 9, 1, 7].
Rotate Array: Solution 1:
def rotate_array(array)
  first_element = array.delete_at(0)
  new_array = array.clone.push(first_element)
end
Rotate Array: Solution 2:
def rotate_array(array)
  new_array = []
  
  array.each do |num|
    new_array << num
  end
  first_item = array.first
  new_array.shift
  new_array << first_item
end
In Rotate Array Solution 1, the shorter solution is a mere two lines long. I initialized the first_element variable which references the array argument calling Array#delete_at, to which I pass the 0 index. I then define a new_arrayvariable pointing to Array#clone in order to preserve the original array object and return the new_array variable to which I push the first_element variable, resulting in the rotated array without modifying the original. A lot is happening in just these two lines of code!
However, Solution 2 demonstrates how we can use the most basic approach to accomplish the same task, albeit with many more lines of code. As part of the requirements for the Rotation exercise, the original array must not be modified, so the first step I took in Solution 2 is to initiate a new_arrayvariable referencing an empty array. Because I wanted to avoid any methods that would mutate the original array object, I decided the easiest way to populate the new array would be to iterate through the original array and push each element into the new array. Because the new_array variable did not reference the original array, the original would not reflect the changes I made to the elements of new_array.
Solution 2 results in a whopping seven lines of code compared to Solution 1’s two. However, if I were given the Rotation problem in a timed interview, I’d strongly recommend the second approach, especially for newer programmers. Although much longer, the backbone of this solution relies on two of the first methods that we learn here in Launch School: Array#each and Array#<<.
Case 3: Rotation (Part 2)
In our final example, Rotation (Part 2), our goal is to write a method that rotates the last specified number of digits in an integer similar to how we rotated elements of an array in Rotation (Part 1). The method rotate_rightmost_digits on the arguments (735291, 3) would result in the rotated integer 735912.
In this case we can extend the comparison of our two approaches to a solution with not only twice as many lines of code, but is also broken down into two methods.
Rotation (Part 2): Solution 1:
def rotate_rightmost_digits(num, spaces)
  num_array = num.to_s.chars
  digit_to_move = num_array.delete_at(-spaces)
  num_array.push(digit_to_move)
  num_array.join.to_i
end
Rotation (Part 2): Solution 2:
def rotate_rightmost_digits(num, digits)
  rotate_array(num.to_s.chars, digits)
end
def rotate_array(array, digits)
  new_array = []
  array.each do |num|
    new_array << num
  end
  new_array.delete(array[-digits])
  new_array << array[-digits]
  new_array.join.to_i
end
The first solution is a modest four lines long and is fairly straightforward. I initialize a variable for an array which we populate by calling Integer#to_s on the num argument and chaining String#chars to that return value. I then create a second variable digit_to_move assigned to the num_array on which I call Array#delete_at and pass in a negative sign in front of the spacesargument. Then I push the digit_to_move variable into num_array and on the last line call Array#join on num_array and then call String#to_i to return the desired integer.
Solution 2 doesn’t hide any of its complexity. It’s repetitive, extremely convoluted, and at first (and maybe second or third) glance, doesn’t seem to warrant any consideration as a worthy approach in a timed interview.
Or does it? Many practice exercises at Launch School task us with creating a method or solving a problem and then introducing us to a more complex variation of that problem in a subsequent exercise. This helps us focus on solving one part of a problem and then expanding the application of that solution to a larger problem. However, can we always assume that an interviewer will start off with the most basic version of an interview question? I certainly won’t assume this will always be the case.
Solution 2 splits the task into two methods. rotate_rightmost_digits calls Integer#to_s and String#chars to convert the number into an array of string objects representing the digits of the number, and calls the rotate_arraymethod, which does the heavy lifting of rotating the elements around. The rotate_array method iterates through the array with the Array#each method and pushes each element into num_array, ensuring that original object is not modified. Then I delete the element at the appropriate negative index and push that element to the end of the array. Finally, I join the array and convert it to the desired integer return value.
Although long and maybe even unwieldy, Solution 2 demonstrates how we can break down a complex problem into smaller chunks and use simpler methods to build up part of the solution as we go and get closer and closer to the target result.Part of the elegance of the Ruby language lies in its ability to do so much in the background — behind the scenes, resulting in clean, yet expressive code. However, during a timed interview or in front of an audience, times when we have to improvise code under less than ideal circumstances, we sometimes have to sacrifice the aesthetic in order to maintain our ability to problem solve under pressure. Concise code is sometimes a luxury that we can’t afford when time is a factor and we need to work toward a solution as quickly as possible in order not to dwell too long on one problem. If the short and more “Ruby-ish” solution springs to mind? By all means, go with that. However, when in doubt, an approach that utilizes more basic methods in incremental steps that solve a piece of the problem often yields longer code but can result in a faster path to the solution and help keep us focused on precisely what we are trying to accomplish with each step of the solution. By manually coding through steps that more sophisticated methods would execute automatically “by magic” in the background, using the longer form solutions can help us code more mindfully, especially as we’re focusing on learning and reinforcing the basics. This is extremely significant in an interview when working against the clock and/or facing a more complex task.

Comments

Popular posts from this blog

Our Story So Far

Studying for First Launch School Assessment