So my master plan for mastering leetcode is
- Building foundational understanding about concepts. Identifying concepts.
- Identify a maximum of 3-5 questions that I would do on repeat to achieve mastery for a given subject.
- Write down main helper functions that help and assist for handling common edges cases for a given subject. Example: Middle Index for Binary Search, get column for 2D Grid, getting adjacency nodes for 2D grid, doing out of bounds checks for 2D grid. etc.
- Have my writings accessible from various platforms and devices, so I can maximize re-learnings from my own and coding from different devices at various times with ease.
- Have a notebook of top 20 questions.
- Not spend too much time on blogging. Just grokking leetcode. Dump in public gists. Find them by searching as such:
user:mfaani #leetcode #array
General Tips
- Read the question well.
- Identify a small sample input to apply your code against
- When leetcoding, try to submit as you make changes to your code. This helps to catch any compiler bug that later becomes difficult to catch. Often things like missing a
{become hard to track. Like just return a dummy bool, string, so you’d be sure the code you wrote so far compiles. - Print early on. This a skill of its own. Use your prints like asserts. Write your prints with your own expectation in regards to your sample input.
- Don’t get tripped on Character to String conversions
- Bound checks
>vs>=;countvscount - 1. Basically any math operation can cause you an issue - Questions that use both iteration and dfs, may require you to duplicate logic. Example, make sure you’re not processing a visited node again. (Leetcode: number of islands)
- For any operation, function call, ask yourself: what is the type of the left. What is the type on the right. Do they match?
- Generally edges should get processed as default. Example assume edges in a 2D grid are
0. - What you’re looking for isn’t there. Every line can assume something that’s not true…
- Does your iteration have any leftover work? Often after the
for-loopI have to process the last state/batch. - While brute force or whatever gets you to a solution is highly advised during your actual interview, during practice, it can be helpful to see totally different approaches.
- Improvements should result in:
- Time Complexity improvement. An improvement from O(2N) to O(N) isn’t worth your time
- Reduction of code indentation. That is:
- 4 conditions is usually better than 6 conditions
- 4 sequential conditions is better than 4 indented conditions. My point is: don’t waste time on improving things that don’t get you much benefit.
In real Leetcode. Brute force is triumphs anything. Because that means you have a working solution. Optimizations should usually come after. While optimized solutions are better, often writing them is more difficult. Hence it’s better to write the more easier solution first. Then just mention / talk how it could be improved. Or perhaps just ask your interviewer if they prefer things differently. However you may not have an interviewer if the assessment is done through some automatic tool (Ex: CodeSignal)
Dumpster
String Indexing
- use
index(after)for advancing the index - compare with
< word1.endIdexto do the bound check - This helps you avoid having optional indexes that get created from using the
index(_:offsetBy:limitedBy:)
1768. Merge Strings Alternately
class Solution {
func mergeAlternately(_ word1: String, _ word2: String) -> String {
var i1 = word1.startIndex
var i2 = word2.startIndex
var result = ""
while i1 < word1.endIndex || i2 < word2.endIndex {
if i1 < word1.endIndex {
result += String(word1[i1])
i1 = word1.index(after: i1)
}
if i2 < word2.endIndex {
result += String(word2[i2])
i2 = word2.index(after: i2)
}
}
return result
}
}```
I initially used `max` on endIndex then used `formIndex` which returned optional, but the above solution was far easier / cleaner.
## Grid Helper functions
```swift
struct Point: Hashable {
let x, y: Int
}
extension Point {
var neighbors: [Point] {
return [Point(x, y + 1), Point(x, y - 1), Point(x + 1,y), Point(x - 1, y)]
}
func getAllowedNeighborsForLimits(_ x: Int, _ y: Int) {
neighbors.filter {$0.x =< x && $0.y =< y}
}
}
/*
Imagine a 5 * 5 array and your point is x:1 y:4
You're 2nd column, 5th row or in programmatic words: column 1, row 4
row4 means [point.y] (as how much down),
column1 means [point.x] (as how much to the right)
rows are the first array in a 2D Grid
columns are the second array in a 2D Grid
so
tldr [point.y][point.x] is the correct way of doing things.
*/
extension Array<Array<Int>> {
subscript (point: Point) -> Int {
get {
self[point.y][point.x]
}
set {
self[point.y][point.x] = newValue
}
}
Linked List
- There’s no extra cost with splitting a
LinkedList(orArraydepending on how you use the data structure). LinkedLists are event better because you can insert at the head and it’s notO(n). So if you split into two parts and then insert at the head when needed, you’re totally fine. - There’s no need to remove an item at a certain location. Instead you just point the surrounding nodes to each other, i.e. the previous to the next.
- First store/get your next/previous pointers, then mess things up. https://leetcode.com/problems/odd-even-linked-list is good example
- You don’t need a for-loop with LinkLists. It’s more of just while the next node isn’t
nil… - ⚠️ - Point the next element of a node back to itself, will cause an overvflow. Be aware
There are basically TWO kinds of errors you’d run into that Leetcode will overflow.
- If you reference set a -> b and b -> a. Example:
node.next = node - if you do a while loop that never ends. Example:
while 2 == 2 { ... }ORwhile index < num.endIndex(where your logic doesn’t end up incrementingindex)
Just knowing these two will help you catch issues easier
- This is probably too simple of a note, but unlike Arrays where you just append and thengs get moved to the end, with LinkedLists, obviously if you repeatedly do
head.next = newNodeyou’re not appending anything. You’re just replacing what’s next. It took me a question or two to have my brain re-wired for LinkedList.- This is why for a linkedList, typically you’d want a pointer to its
headand another to itstail, otherwise appending items to the list becomesO(n)if you only use theheadobject.
- This is why for a linkedList, typically you’d want a pointer to its
extension ListNode: CustomDebugStringConvertible {
public var debugDescription: String {
var arr: [Int] = [val]
var node = self
while let next = node.next {
arr.append(next.val)
node = next
}
return arr.reduce("") {
$0 + "\($1) - "
}
}
}