Approaching Problems like a Software Engineer

Approaching Problems like a Software Engineer

During a recent, mock technical interview, I was given the following problem to solve.

Write a function that takes in two inputs, an array, and a number. Remove the first item in the array. Then, move it to the last place in the array. Repeat this the number of times as given in the second argument and return the array.

For example, if the given array is [1,2,3], and the number of times it should shuffle is 1, the function should return [2,3,1].

Break Down the Problem

Given the problem, I thought about the main components (or problems) I would need to make things work.

  • Remove the first item in the area (the item at place array[0])
  • Take the removed item and place is at the end of the array (it becomes the item at place array[array.length -1].
  • Repeat the above for each time the second argument specified
  • Return the shuffled array

Knowing that the act of removing an item and placing it into the array would have to happen x numbers of times, a for loop seemed like a good place to start. Within that loop I could remove the first item in the array with .shift() then add it back onto the end of the array with .push().

Putting that together into a function, our solution could look like this:

function moveToEnd(array, number) {

    for ( let i = 0; i < number; i++ ) {
        let itemToMove = array.shift()
        array.push(itemToMove)
    }

    return array    
}

Given the following inputs:

moveToEnd([1,2,3], 1)
moveToEnd([1,2,3], 2)
moveToEnd([1,2,3], 3)

We get the following outputs:

[2,3,1]
[3,1,2]
[1,2,3]

It works!

While this solution does work, it may not be the best way to approach the problem. Let’s think about some possible edge cases that we have not accounted for.

Run Time

In the sample code, I have three simple examples of what the inputs and outputs could look like. The array is relatively small, so have been the number of times the for loop needs to run.

In Javascript, an array can be any size. Shuffling through an array of 1000 items would take longer than shuffling through an array of 3 items. An array of 1,000,000 items would take even longer. Alternatively, if a 3 item array needed to be shuffled 1,000,000 times, it would take longer than removing an item from the front and placing it on the end 3 times.

Time complexity, runtime, and Big O notation are some of the concepts in computer science surrounding the idea of how fast, slow, or performant a program or function is. These concepts are too complex to cover in this blog post, however, we can keep the idea of runtime in mind as we refactor the moveToEnd function.

Noticing Patterns

Focusing on one aspect of the function, the user inputs, we can improve our solution a little bit.

Going back to our earlier examples, let’s shuffle the [1,2,3] array a few more times.

moveToEnd([1,2,3], 1) // expected return [2,3,1]
moveToEnd([1,2,3], 2) // expected return [3,1,2]
moveToEnd([1,2,3], 3) // expected return [1,2,3]
moveToEnd([1,2,3], 4) // expected return [2,3,1]
moveToEnd([1,2,3], 5) // expected return [3,1,2]
moveToEnd([1,2,3], 6) // expected return [1,2,3]

You may see a pattern here.

If the array is shuffled 1 time or 4 times, the resulting array ([2,3,1]) is the same. Each time number is incremented by 1, the resulting array is the same as every 3rd instance. Another interesting thing is that when the array is shuffled 3 times (or a multiple of 3 with 0 as a remainder) the resulting array is the same as the original array.

This is interesting to think about.

If the return of a function is the same when we shuffle the array 1 time, 3 times or 6,000,000 times, do we really need our for loop to run 6,000,000 times?

No.

We can make our code a little more performant by simplifying our number to be as small as possible in comparison to the array then running the for loop that many times.

Here are some scenarios to think about:

  • If number is less than then the length of the array run the for loop
  • If number is greater than length of the array, divide it by the length of the array, then use the remainder to run the for loop

There are a few ways to tackle this, but for the purpose of this example, I’ll use an if statement to evaluate the value of the number argument.

    if (number > array.length) {
        number = number % array.length
    }

On the second line, we use the modulus operator to return the remainder and assign it to the number variable, that is then used in the for loop.

Our updated function now looks like this.

function moveToEnd(array, number) {

    if (number > array.length) {
        number = number % array.length
    }    

    for ( let i = 0; i < number; i++ ) {

        let itemToMove = array.shift()
        array.push(itemToMove)

    }

        return (array)

}

With this addition, our for loop will run a relatively small number of times compared to what the number argument could be.

What else?

The moveToEnd problem is relatively easy to solve on purpose. Solving problems as an engineer is part of getting to a solution that works. And part getting to a solution that scales, accounts for edge cases or has minimal trade-offs or side effects.

In technical interviews, you may not have all the answers to what inputs could look like, what outputs should be, or other factors to consider. But, that’s on purpose. It’s on you, as the developer, to ask clarifying questions to help guide you to an optimal solution.

For example, when I was posed with this problem, I asked if it was ok to mutate the original array, or if I should return a new, shuffled array, leaving the original intact. If I was approaching this problem now, I may think about if my solution works with a nested array. Or, to account for if the number argument is not an integer or less is less than 0. Going a little deeper, I may revisit the for loop to see if there is a better option.

Conclusion

Again, moving items in an array may not be a scenario you will encounter in the real world. But, thinking about making your code efficient is something you will encounter. Challenge yourself to revisit code you have written and refactor it to account for less-than-ideal user inputs, time complexity any other edge cases you can think of.

Resources

Cover photo by Olav Ahrens Røtne on Unsplash