Home > Reflections | ⏮️ ⏭️
2024-06-05
🪞 Practice Reflections
Yesterday, I observed a problem with my programming problem solving strategy.
This prompted the question: 🤔 How can I generate higher quality algorithms without relying on testing?
🧠⛈️ Brain Storming
1. Become familiar with more solutions
- Maybe this is a signal that the current bottleneck in my coding interview performance isn’t coding.
- At least while practicing, when planning a solution to a problem, if I can’t identify an algorithmic approach I’m confident in, it may be more fruitful to stop and do some research than to continue down a likely dead end path.
2. Develop a systematic approach to generating ideas
When stuck in the planning phase, maybe I start asking myself a series of questions that may highlight an approach I’m familiar with, but that is currently not front of mind. Examples:
- What would a brute force approach look like? Typically, this involves generating every possible solution, testing each one, and returning a successful candidate.
- Is there a natural recursive solution? What is the base case? What do the sub problems look like?
- Is there a simpler version of this problem that I know how to solve? Sometimes a solution to a simpler problem can be modified to solve a more complicated problem.
- Is there a preprocessing step that would make this easier to solve? For example, sometimes sorting the input enables a tractable solution.
- Can I break the problem down into sub problems whos solutions can be combined to form a solution to the problem at hand?
- Is there a data structure that would make this problem easier to solve? Arrays, lists, stacks, queues, trees, graphs, tries, maps…
3. Practice writing algorithmic solutions in a shorthand pseudo code
- I’m kind of just winging it in the planning phase when it comes to writing out an algorithmic approach.
- It’s easy to have a fuzzy idea in mind, but writing that idea out explicitly forces a degree of clarity. It also frees up cognitive resources to focus on the sticking points.
- Having a systematic and familiar approach to writing down a solution can save a lot of time and help structure our thoughts in a way that aids analysis and elaboration.
- Of course, to be useful, this short-hand pseudo code would need to be much faster to write than a real programming language.
- What kinds of primitives would empower such an algorithmic pseudo code to be expressive, flexible, and precise enough to formulate a solution to any programming problem?
- Does this already exist? Can I copy someone else’s approach rather than invent my own?
- Perhaps I can practice writing in such a language backward: by translating real solutions into this higher level pseudo code. This certainly seems like a good way to test the language for practicality. How much shorter is the pseudo code than the real solution? How much faster is it to write? What details can be omitted completely while maintaining a useful representation of the algorithm?
4. Practice planning solutions to programming problems without actually implementing or testing them.
- If I’m already pretty good at translating plans to code, I could save a lot of time and quickly develop a sense for how many and what kinds of problems I’m not prepared to solve.
- This could inform a study plan: ah, of a dozen LeetCode problems I wrote out plans for today, I couldn’t confidently identify solutions to those involving e.g. binary search or tree traversal, etc
🏋️ Practice
Problem Statement
Given a string array
words
, return an array of all characters that show up in all strings within thewords
(including duplicates). You may return the answer in any order.
🪞 Reflections
This took about 3x as long (46m) as my target time for an interview (15m)
Why did planning take 21 minutes?
- It took a bit of time to come up with a strategy to accurately count duplicate characters across words
- The initial plan didn’t come out smoothly on the first try
- After analyzing the runtime complexity of the initial plan, I decided to optimize to eliminate a W^2 term, which seems significant given the problem constraints
Initial implementation took ~8m
Not bad.
Why did testing take ~14m?
I split the problem into 3 helper functions and tested each helper function independently, then I tested the top level function.
I feel like this worked out well.
I don’t think any particular function took very long to test.
Testing was pretty easy; there wasn’t much state to track and I didn’t need many test cases to gain confidence.
While testing the top level function, I was confident that each helper was correct.
Testing effectiveness
- I don’t think I actually caught any bugs during testing
- Yet there were 2 syntax errors and 1 simple return type error I found while attempting submission
- I’m not particularly worried about any of these errors.
- They weren’t logic errors
- I don’t think an interviewer would care
The uncaught errors
- syntax error in frequencyMap return type (⇒ instead of :)
- I probably annotated the return type after initially writing the function. This mixup isn’t a big deal.
- syntax error in frequencyMap for of (no const in c of word)
- I don’t actually use the for-of syntax very often, but wanted to reduce a bit of complexity by not dealing with indices
- A
.forEach
method call would have worked just as well and probably would have avoided the syntax error
- returned a string instead of string[]
- I simply forgot (or didn’t initially pay attention to) the fact that they wanted an array of character strings instead of a single string
- not really worried about this
Are my rationalizations justified? Useful?
While i’m not particularly worried about these minor classes of errors, it’s possible I’m rationalizing poor performance due to a self-serving bias and a desire to perform well.
To control for bias, perhaps it’s still worth considering how to improve.
- For the careless return type syntax error
- Perhaps a habit of one final scan of a function implementation (focusing specifically on syntax) before moving on could help catch these.
- That said, in a real interview, speed is likely to be the major challenge. I’d rather include a few minor syntax errors in my final solution and increase my odds of finishing on time than risk missing the timeline but have perfect syntax.
- Compromise: save the syntax scan as an optional last step, time permitting.
- Prefer familiar syntax (e.g.
xs.forEach(x => ...)
) over less familiar syntax (e.g.for (const x of xs) ...
) - Consider another optional, time-permitting step at the end: re-read the problem statement and double check that I’m returning the right thing.
Further Considerations
I don’t think my analysis above indicates a path to reducing time from 46 → ~15 minutes.
Some thoughts on compromises and further improvement:
- When writing a solution involving helper functions, define the helper functions last. Perhaps just write the function signature and initially assume that it’ll work. It’s probably better to finish a high level solution and not have time to implement (and test) all of the helper functions rather than run out of time during helper function implementation before having the full high-level implementation in place.
- Since my testing is happening at the end, it’s always possible to stop testing early. Perhaps it’s good enough to see that I have a systematic approach to testing that could catch bugs if I had enough time to apply it. In this case, it’s probably worth writing out a few example cases to test before running all of the tests.
- Still, my manual testing process tends to be slow. It’s worth considering how I might be able to speed it up.
- I can hear people talking in a nearby room. This distraction likely slowed me down a bit. Definitely prepare to have a quiet environment during a real interview.
- Again, what about this 21 minute planning phase? That already blows the entire budget and then some.
- In a real interview, I’ll need to be sure to think aloud and discuss all of my decisions with my interviewer before following through on them. For example, it may have been good enough to note that my first plan had a sub-optimal runtime, rather than actually re-planning my solution. I’m not 100% sure if I should be monitoring the clock while practicing and deciding to cut things short, or if it’s better to focus on accuracy initially and assume that accurate practice will improve time. Perhaps I should keep an eye on the clock so as to make practice more realistic. In this case, a quick glance at the clock after my first implementation would likely have resulted in a decision to skip the optimization. Or at least, if I have a good idea of how much time each phase should take…
- Identify a budget. Monitor the clock aiming to stay within the budget. For example: 5 minutes to plan, 5 minutes to implement, 5 minutes to test. Having these milestones in mind in advance may support better decision making during an interview. Counterpoint: it’s important not to let the stress of time pressure encourage bad decisions. So maybe it’s worth considering in advance what kinds of short cuts are worth taking, and where I absolutely should not cut corners to save time. Examples:
- Good short cuts to save time:
- Note the likely existence of an optimization up front, but don’t re-plan or re-implement a working solution unless the interviewer insists.
- Assume the existence of helper functions when initially implementing a high level solution. Write the helper functions if there’s extra time at the end and the interviewer wants to see them.
- Situations when short cuts should not be used to save time:
- I’m not confident that my planned solution will work. My intuition seems to be pretty good. If I’m not confident in my plan, the solution likely won’t work. Stick it out in the planning phase even if this means we don’t ultimately have time to implement and test. If we identify a solution we’re confident in with only a few minutes remaining, pull out all of the time saving tricks at our disposal to attempt to get some form of implementation in place. Don’t implement helper functions. Don’t fret about exact syntax or perfect use of built-in functions. Ideally, we also at least have a tiny bit of time to demonstrate some form manual testing. Even just mentioning a test case and talking through the simplest test case can demonstrate that the skill exists, even if we don’t have time to fully show it off.
- Good short cuts to save time:
My Solution
🏋️ More Practice
59. Spiral Matrix II
Given a positive integer
n
, generate ann x n
matrix
filled with elements from1
ton2
in spiral order.
🪞 Reflections
- Ugh. This was painful. Death by a thousand little bugs. Or, I guess, 10 little bugs.
- That said, while the overall time to completion was unacceptable (~84m), my plan did ultimately work.
- Yes, there were a lot of errors I had to work through to get a working implementation, but they were:
- a syntax error nobody will care about
- a syntax error due to the (apparently old) version of TypeScript LeetCode is using
- a syntax error nobody will care about… basically a typo
- a type error that required me to give TS more information… it would have worked at runtime
- a type error that I had to ignore to appease the TS
- a typo
- a typo
- maybe a bad poorly thought through short-cut or maybe just the result of a yet-to-be-fixed typo
- a typo
- a bad copy/paste (basically a typo)
- Yes, it feels like a lot of errors, but none of them indicate a lack of understanding of the problem or a lack of programming ability… just a lack of compiler-level code perfection. I don’t think they’d be held against me in an interview.
- I’m happy with the order I chose to work in.
- 17 minutes may be a long time to plan, but I was (rightfully) confident in my plan when I moved to implementation.
- I spent ~9 minutes implementing the top-level solution, putting off implementation of the helper functions.
- I spent the next 10 minutes thoroughly testing my high-level solution before moving on to implement and test the helper functions.
- In an interview, I likely would have been stopped mid-testing to move on to the next problem. At that point, I’d demonstrated a well-planned solution, awareness of the possibility of maybe-easier alternate approaches, a high-level implementation, and an ability to thoroughly, manually test my code. This is what they’re looking for.
- I’m definitely happy with my decision to implement the steering approach rather than trying to come up with a calculation for the indices. While there may be a nice calculation, I don’t think I’d have come up with it in a reasonable amount of time (if at all).
- I’m happy with my initial high-level solution. All the bugs were found in a helper function.
- I’m pretty happy with my decomposition into helper functions.