Sometime last year I started to solve hackerrank problems at my pace and here is my progress after about 108 days ago.

Funnily , I used to think there was no need to practice interview questions unless you were looking for a job or interviewing job applicants. This earlier perception might be wrong; I now think engineers should solve a few questions weekly to keep their skills sharp . It is amazing how fast problem-solving and modelling skills atrophy if not honed.

Here are a few reasons to consider solving problems every now and then.

1. Improved Problem solving

My problem modelling and solving skills have grown sharper. Just like chess masters can see two or three moves ahead, I now have a similar feeling when I start new problems. I can sometimes ‘feel’ an approach is going to fail because I have made the same mistake before.

Practice makes perfect or once bitten, twice shy?

2. Deeper understanding of languages and tools

I know a lot of JavaScript but knowing or reading about some concept is not the same as experiencing it first hand. Solving the challenges quickly exposed me to the painful reality of JavaScript’s limits.

For example, I read somewhere a long time ago that JavaScript functions have an arity limit (a function’s arity is the number of parameters it accepts). This value varies from platform to platform (for my Chrome on Ubuntu, it is around 217).

I soon experienced this first hand when I wrote a recursive solution that leveraged partial application to bind the elements of an array to another function. This solution worked pretty well for small values but immediately broke on some problem sets.

For some of the problem sets, these sparse arrays were very large (> 130k slots). Attempting to spread or invoke [].push.apply(target, largeArray) would throw the RangeError: Maximum call stack size exceeded.

let target = []; target.concat(...new Array(Math.pow(2,17)));

The dots connected only after I debugged the code.

Another learning opportunity was when I wrongly assumed that ~~ would always cast to valid integer values. I quickly found out that the ~~ trick was only guaranteed for 32-bit values. The explanation of that is documented in this post.

3. Freedom to experiment with unorthodox approaches

My goal was to solve the challenges as quickly as possible – this freed me up to try new things and experiment with different code styles. I tried out things that I would probably never use in production.

I discovered some gems and always learnt more; understanding why a particular trick works can devolve into poking into the underlying language design principles or even the mathematical roots and that is always good in my opinion.

Ultimately I started to use some of the neater ES6-style syntax (I intend to adopt some of these in production code). Another gain is that I now have a good test ground for things I would be too busy to try at work. Concepts like generators, iterators, symbols, sets etc.

4. More robust assumptions and modelling

I was extremely surprised when I got only one question right out of 8 on a particular challenge. My algorithm was provably correct so what could be wrong?

Well, I purchased the test set and found out the cause immediately. My algorithm was right but my modelling assumptions were wrong! I had made the implicit assumption that all input could be validly represented as integers; the test set had input strings that were over 500 characters long.

The biggest eye-opener was the realization that despite always sanitizing input, I still unconsciously assumed input would be in a specific range. As engineers, we should question our biases and assumptions to get more robust code. What guarantees exist that you’ll get sane values if you are not enforcing extra checks?

How did I solve the problem? I switched to strings and then carried out integer operations on each character. That’s the only way to get around extremely large values.

What was the real issue?

2500 is about 3.273390607896142e+150. Floating point representations of huge numbers suffer from a loss in precision; consequently, some operations would return invalid responses.

5. Make it work first

I have learnt to take a step back if an abstraction doesn’t work cleanly; it nearly always means that the proposed model is not the best solution for the model. Forced models cause problems downstream i.e. your solution won’t pass all the tests.

Also, after solving a problem, I take a step back, examine the code and then clean it up. Most times I find more efficient routes to the solution or at least I am able to eliminate some intermediate steps. Eventually, I end up with the crispest code possible.

Finally, I look at how others have solved the same problem. I don’t need to do this but exploring new ways of solving the same problem or understanding other viewpoints is a great learning experience. At the very least, I get to widen my perspective and improve my solution toolbox.

Conclusion

What do you think of honing your skills via algorithmic challenges?