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]
.Given the problem, I thought about the main components (or problems) I would need to make things work.
array[0]
)array[array.length -1]
.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.
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.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:
array
run the for
looparray
, divide it by the length of the array, then use the remainder to run the for
loopThere 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.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.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.
Cover photo by Olav Ahrens Røtne on Unsplash
Previously published at https://shannoncrabill.com/blog/approaching-problems-like-a-software-engineer/