Home > Reflections | ā®ļø āļø
2024-06-13
šļø Solution Planning Practice
I want to try something a bit different today.
Rather than spending 15-90 minutes going deep by planning, implementing, and testing a single problem, Iām going to take a shallow, broad pass by reading and planning several problems.
Iāll try to limit each problem to 2-10 minutes of planning, focusing on identifying the key algorithms, data structures, or insights that will be critical in solving the problem, and that also imply runtime complexity.
For each problem, I should also rate my confidence in my proposed solution.
Some problems, I wonāt know how to do at all.
Some problems, Iāll be confident that I can implement the optimal solution in reasonable time.
And many will likely fall somewhere in between.
After briefly reviewing many problems, maybe Iāll identify some patterns that can be useful in guiding my continued study and practice.
It could also be interesting to later return to these plans and practice implementing a solution based on a previously derived plan.
Separating the steps in time may help me understand how valuable the plans actually are and how easily I can turn my own plan into a working solution.
Here we go.
6.Ā Zigzag Conversion
The stringĀ
"PAYPALISHIRING"
Ā is written in a zigzag pattern on a given number of rows like this: (you may want to display this pattern in a fixed font for better legibility)
P A H N
A P L S I I G
Y I R
And then read line by line:Ā"PAHNAPLSIIGYIR"
Write the code that will take a string and make this conversion given a number of rows
- This is not a traditional algorithmic problem.
- Itās really just about sequencing.
- Iāll need to fill n rows in the correct sequence, then print the rows with the proper format.
- Two approaches could be taken here:
- Compute the indices that should go on each row (this doesnāt seem like an obvious calculation)
- Iterate between the N rows in an up and down pattern, filling each one character at a time until weāre out of characters.
- The second options seems easier to implement quickly.
- We could track the direction weāre moving between the rows (up or down) and change directions when reaching the top or bottom.
This will take O(N) time in the size of the input string.
Filling the arrays and then printing will take an additional O(N) space that wouldnāt be necessary if we came up with a computation for the position of each character in the sequence.
This should be fairly straight-forward to implement. Getting the details with the direction exactly right and avoiding out of bounds and null pointer errors could be a bit fiddly.
Planning Time: 6:35
š Reminds me of 59. Spiral Matrix II
Reviewing Official Solution
ā
The official solution describes 2 similar approaches to what I described above, also preferring the local, steering approach
Note: They point out that Time Complexity is actually O(N * numRows) due to the extra time taken to process all the spaces
7.Ā Reverse Integer
Given a signed 32-bit integerĀ
x
, returnĀx
Ā with its digits reversed. If reversingĀx
Ā causes the value to go outside the signed 32-bit integer rangeĀ[-231, 231Ā - 1]
, then returnĀ0
.
Assume the environment does not allow you to store 64-bit integers (signed or unsigned).
- Reversing the digits is easy: toString ā toArray ā Reverse ā join ā parseInt
- The trick, however, is in the constraint that we canāt store a number thatās out of range.
- So weāll have to be able to identify when a number would be out of the 32 bit signed range before converting the string of digits back to a number.
- Alternatively, they might want us to detect overflow (i.e. we attempt to store a number bigger than can fit in 32 bits and the result is simply the wrong 32 bit number).
- If we simply had to detect overflow, we could just convert the reversed number back to a string after parsing the integer and check if itās the same string as before parsing.
- If we have to proactively decide if the (reversed) number will be too large before converting it to an integer, we can try several approaches
- How many digits is the biggest (or smallest if negative) number we can fit in 32 bits? If our string is fewer digits, weāre fine.
- If our string has the max number of digits we can fit in 32 bits, we can check digit-by digit, from largest to smallest: if our reversed numberās ith digit is smaller or equal to the max (min) able to fit in that place, weāre fine. As soon as we find a digit that is strictly smaller, the remaining digits donāt matter.
There are log10 digits in a base-10 integer, and the amount of work weāre doing is proportional to the number of digits, so:
Runtime complexity: O(log(N)) in the passed in integer.
Storing the digits will take O(log(N)) space.
If the first approach is acceptable (I think weād have to simulate this overflow effect in JavaScript), this implementation seems pretty easy.
Implementing the second approach is quite doable, but could get a bit messy in tracking all of the details.
Planning Time: 11:44
Reviewing Official Solution
ā
They describe a similar approach to what I described above.
ā
Same runtime (though I used extra space and they didnāt)
11.Ā Container With Most Water
You are given an integer arrayĀ
height
Ā of lengthĀn
. There areĀn
Ā vertical lines drawn such that the two endpoints of theĀith
Ā line areĀ(i, 0)
Ā andĀ(i, height[i])
.
Find two lines that together with the x-axis form a container, such that the container contains the most water.
ReturnĀ the maximum amount of water a container can store.
NoticeĀ that you may not slant the container.
- We want to find 2 lines that maximize the area of the rectangle formed between them
- Thereās an obvious, brute-force, O(N^2) solution that simply compares every line with every other line and computes the area for each
- How can we reduce our search space?
- Maybe we can compute a sort of rolling maximum while scanning left to right
- Itās never necessary to track a shorter line that comes after a taller line as the starting point
- Since we know how long the width is in advance, we can drop from consideration short starting lines with max values that are smaller than an option weāre already tracking
- Maybe we always keep track of the tallest starting and ending lines that weāve seen
We can make a single forward pass and filter the list to all candidate starting lines.
Algorithm: keep all lines that are strictly taller than the previously kept line
We can do the same thing in reverse, keeping for consideration ending lines in increasing height (from end to start)
Then we can compare each start line with each end line.
In the worst case, we have a sort of pyramid where the lines grow to the middle, then shrink to the end
Then we still have N/2 start lines to compare to N/2 end lines, which is still O(N^2)
Example:
values : 1 2 3 3 2 1
indices: 0 1 2 3 4 5
In this case, we donāt need to make all comparisons, we only need to compare a start line with the furthest end line that is equal or shorter in height.
So weād compare the following pairs of indices:
0,5 = 1 x 5 = 5
1,4 = 2 x 3 = 6
1,5 = 1 x 4 = 4 ā didnāt need because 0,5 already used 5, thus is taller because itās wider
2,3 = 3 x 1 = 3
2,4 = 2 x 2 = 4 ā didnāt need because 1,4 already used 4, thus is taller because itās wider
2,5 = 1 x 3 = 3 ā didnāt need because 1,5 already used 5, thus is taller because itās wider
So maybe we can do this:
- O(N) scan left to right finding candidate starting positions
- O(N) scan right to left finding candidate ending positions
- O(N) scan left to right, tracking a rolling max, comparing to the furthest end line that is equal or shorter in height AND hasnāt already been compared earlier
runtime complexity: O(N)
space complexity: O(N)
Planning Time: 40:52
Iām not 100% confident that this is correct, but very close to it.
I think I could implement this without too much trouble.
Reviewing Official Solution
ā They have a 2 pointer solution that runs in linear time and looks quite a bit different than mine.
I believe that these algorithms amount to the same thing, but just looked at from different perspectives.
In my algorithm, I used extra space, where they didnāt have to.
- To be sure, I should implement my solution and see if it works.
Note: this is yet another application of the 2 pointer pattern, which seems to turn O(N^2) brute-force solutions into clever O(N) solutions.
12.Ā Integer to Roman
Seven different symbols represent Roman numerals with the following values:
Symbol | Value |
---|---|
I | 1 |
V | 5 |
X | 10 |
L | 50 |
C | 100 |
D | 500 |
M | 1000 |
Roman numerals are formed by appendingĀ the conversions ofĀ decimal place valuesĀ from highest to lowest. Converting a decimal place value into a Roman numeral has the following rules:
- If the value does not start with 4 orĀ 9, select the symbol of the maximal value that can be subtracted from the input, append that symbol to the result, subtract its value, and convert the remainder to a Roman numeral.
- If the value starts with 4 or 9 use theĀ subtractive formĀ representingĀ one symbol subtracted from the following symbol, for example,Ā 4 is 1 (
I
) less than 5 (V
):ĀIV
Ā and 9 is 1 (I
) less than 10 (X
):ĀIX
.Ā Only the following subtractive forms are used: 4 (IV
), 9 (IX
),Ā 40 (XL
), 90 (XC
), 400 (CD
) and 900 (CM
).- Only powers of 10 (
I
,ĀX
,ĀC
,ĀM
) can be appended consecutively at most 3 times to represent multiples of 10. You cannot append 5Ā (V
), 50 (L
), or 500 (D
) multiple times. If you need to append a symbolĀ 4 timesĀ use theĀ subtractive form.
Given an integer, convert it to a Roman numeral.
- This problem seems amenable to recursion:
- decide the first symbol to use, based on the rules
- subtract the value of that symbol from the input value
- make a recursive call using the remainder of the input
- base cases: 0 (empty string) and any value in a lookup table of numbers to their roman numeral symbols
Time complexity: roman numerals follow a kind of logarithmic pattern, similar to Arabic numerals, so this feels like O(log(N)) in the number provided for conversion.
Space complexity is the same, and only needed for storing the output string.
Iām pretty confident I could code this up without much trouble.
Planning Time: 7:56
Reviewing Official Solution
ā
They propose an iterative version of the recursive solution I describe above.
Note: they claim O(1) time complexity due to the fixed upper bound on the roman numeral size. I guess thatās technically correct, but itās a pretty big constant. I think the logarithmic argument applies equally well, but donāt think itās particularly useful to argue the point.
15.Ā 3Sum
Given an integer array nums, return all the tripletsĀ
[nums[i], nums[j], nums[k]]
Ā such thatĀi != j
,Āi != k
, andĀj != k
, andĀnums[i] + nums[j] + nums[k] == 0
.
Notice that the solution set must not contain duplicate triplets.
- The obvious brute force solution is to assemble all triplets and filter them by which of them sum to 0
- O( N choose 3 ) = something like O( N! * K! * / (N - K)! ) ⦠roughly exponential, which is unacceptable
- Iāve worked on 2Sum and 3Sum somewhat recently, and recall that there are a couple of tricks:
- For 2Sum O(N * log(N)) time complexity / O(1) extra space complexity:
- sort the input, small to large
- use 2 pointers, starting at either end of the list
- if the sum is too small, increment left pointer
- if the sum is too big, decrement right pointer
- if the sum is equal, add the pair to the result set
- continue as long as the pointers arenāt equal
- Alternatively O(N) time complexity / O(N) extra space complexity:
- O(N) build a hash map from values to (lists of) indices
- O(N) for each value, check for target - value in the hash map, with different index
- For 2Sum O(N * log(N)) time complexity / O(1) extra space complexity:
- O(N^2) For 3 Sum, we can
- For each element E, fix 1 pointer at this element
- Apply 2Sum with the target = -E on the remainder
I believe I could implement either of these without too much difficulty.
Iām not sure I could explain exactly why the 2 pointers method works.
The hash map approach seems straight-forward to explain.
Planning Time: 16:09
Reviewing Official Solution
ā
They describe the same approaches Iāve described above, with some extra tips to avoid duplicates.
ā
Same O(N^2) runtime
šŖ Reflections
- 5 problems planned, ranging from ~6 ā ~40 minutes of planning time each.
- Okay, so thatās still quite a bit of time spent on each problem. This isnāt necessarily a bad thing. I thought about each problem carefully and believe I came up with plans for workable solutions.
- Even though it took about 40 minutes, Iām pretty happy with having come up with (what I believe to be) a linear time solution to 11.Ā Container With Most Water
- I want to try covering more problems to an even shallower depth, perhaps just reading the problem, coming to understand it, and jotting down any ideas that immediately come to mind. Maybe I time box this to time to read and understand the problem + 2 minutes.
- However, before I move on, I think I should look at posted solutions for these problems to determine if my plans should actually work. Iād hate to convince myself that Iāve come up with a correct solution that turns out to be incorrect.
- After reviewing the official solutions, Iām confident in all of my plans with a possible exception in 11.Ā Container With Most Water. I think they amount to the same thing (but canāt know for sure without implementing it), the the official solution is more elegant.
ā” Rapid Fire Solution Planning Practice
16.Ā 3Sum Closest
- Modify 3Sum to keep track of the triple with the value closest to the target
Planning Time: 0:54
17.Ā Letter Combinations of a Phone Number
- The inputs are incredibly short (up to 4 digits) so we could just hard code 4 nested for-loops, only use the ones we need, and loop over the possible possible characters creating the output array.
- Runtime complexity is O(3^N) in the number of digits of the phone number provided
Planning Time: 2:44
18.Ā 4Sum
- Dare we use a linear pass, applying 3Sum at each index, resulting in an O(N^3) algorithm? I donāt see why 4 would be special.
Planning Time: 1:43
19.Ā Remove Nth Node From End of List
Question: is the list always at least N nodes long? If not, we probably just return the list unchanged.
- Store the head as the result
- The trick is that we donāt know how long the list is, so we have to somehow iterate to the end and prepare to know which node was N nodes behind.
- I think we can do the following:
- Store the head to return as the result
- Store the head again as our n-back cursor
- Create a new cursor and iterate across n nodes
- Now, for each additional iteration down the list, we iterate both cursors
- When we reach the end of the list, we have the node that is n behind our current position
- Okay, I guess we should be storing the (N + 1)th node back in our other cursor, so we can rewire the pointer to the node 2 in front of it
- Runtime complexity: O(N), Extra space complexity: O(1)
Planning Time: 5:14
22.Ā Generate Parentheses
- Ooh, this seems a bit tricky.
- Weāll have to understand how many valid ways we can open and close parentheses.
- Itās a combinatorics problem.
- We always must start by opening the first parenthesis.
- Then, if N > 1, we have a choice of closing our first or opening our second.
- At the third place, if N > 2, we have a choice of closing our first or second or opening our third.
- A clear solution doesnāt pop into mind, but Iām thinking about bitstrings for encoding the choice of whether to open or close another paren.
- I wonder if thereās a recursive solution that makes this easier⦠I think so, because weāve got a backtracking kind of thing going on.
- A stack is usually helpful in knowing when we can close a paren
- Letās see, recursively, this might look something like:
- base cases:
- we have open parens available, but canāt close any, so open one
- we have no open parens available, but we have some to close, so close one
- recursive steps:
- we could open or close a paren, so do both and recurse in each case
- I think the recursive function arguments may need to include a stack (or perhaps a counter for how many unclosed open parens are in the input string; as well as a prefix string
- This isnāt a perfect plan, but I think something to this effect will work and want to time box this exercise.
- Runtime complexity of back tracking: something like⦠well, all combinations makes me think O(2^N), but the constraint on well-formed pairs may cut this down considerably. If I had to guess, Iād say something closer to O(N^2)
Planning Time: 13:38
24.Ā Swap Nodes in Pairs
- Linked list pointer rewiring⦠okay
- Weāre just doing swaps.
- Linked lists are recursive data structures, so maybe recursion can save us some complexity.
- How about this:
- initialize 3 cursors
- track the first 3 elements with them
- rewire the pointers
- save the new head as the return value
- recursively swap on the 4th element
- return the original head
- Be careful with null pointers and detecting the end of the list
- O(N) runtime, O(1) space
- Seems like a bit of a pain to implement, but may not be too bad with recursion.
Planning Time: 5:37
26.Ā Remove Duplicates from Sorted Array
- Initialize 2 pointers: one for reading, one for writing
- Scan from left to right
- when duplicates appear, advance the reading pointer until the next unique element is found
- write that element to the writing pointer and advance the writing pointer
- continue forward, writing each element from the reading pointer to the writing pointer, except for duplicates, which we skip
- Runtime complexity: O(N), O(1) extra space
Planning Time: 3:38
28.Ā Find the Index of the First Occurrence in a String
- If weāre allowed to use built-ins, Iām pretty sure find does this for us.
- If weāre not allowed to use built-ins:
- Scan the input string left to right, looking for the first occurrence of the first character of the needle string
- when the first character of needle is found, continue scanning (with a new pointer) to see if the whole match is found
- If so, return the start index
- else continue scanning left to right
- Runtime complexity: O(n * k) where n is the length of the input string and k is the length of needle
Planning Time: 3:44
31.Ā Next Permutation
- š¤ A bit tricky
- Observations
- If the input array is in ascending sorted order, we swap the last 2 elements
- If the input array is in descending sorted order, we reverse it
- Almost seems like we can start at the end of the array and ask: is this element greater than the one previous?
- If so, swap them and weāre done
- Else step toward the front of the array and ask the same question again
- If we have to swap any but the last, sort the tail remainder in ascending order
- Thatās the best Iām coming up with right now.
- Using a sophisticated (merge or quick) (in-place) sorting algorithm would give us O(N * log(N)) runtime complexity.
- But it could be a challenge to implement that in an interview
- Using a bubble sort would be easier, but take O(N^2) runtime complexity.
- Maybe I should practice implementing merge or quick sort š¬
Planning Time: 12:50
32.Ā Longest Valid Parentheses
- drop any initial open parens
- drop any final close parens
- initialize an array of length of string
- scan left to right
- Initialize a counter to zero
- For each paren, subtract one if itās a closing paren, add one if itās open
- At each position, set the value of the array to the current count
- The longest valid substring starts at the first zero and ends at the last zero
- Runtime complexity: O(N); extra space: O(N)
Planning Time: 6:21
šŖ Reflections
- I covered a lot of problems by skipping implementation.
- I didnāt quite keep planning time as low as I wanted to on this last session, but I still go through 10 problems. Maybe Iāll use a timer next time.
- It could be worth going back to the problems I couldnāt think of a solution for and try to work those out.
- Or perhaps, the signal here is that Iām able to come up with decent solutions. Maybe I need to work on coding speed next.
- Iāll call it a day for now.