The LeetCode Grind
This page documents my process of grinding LeetCode and the lessons learned from it.
Grind 75
Grind 75 is a well-known set of LeetCode questions designed for technical interview preparation. It covers all the essential algorithms and data structures, making it a comprehensive curriculum that fits into an eight-week schedule.
I completed Grind 75 in about eight weeks, with 27 days of actual coding. I did it in TypeScript, a language I had wanted to learn for a long time. Interestingly, I found the first two weeks to be the most challenging because I was learning many algorithms from scratch. Once I became familiar with the core design patterns and algorithms, tackling 'Hard' problems felt no more difficult than the 'Easy' ones did at the very beginning.

Here, I would like to take note of the common algorithms and their application from the easiest to the most advanced ones. I won’t dive into the details of the algorithms, but rather focus on the concepts and key takeaways.
- Bitmap
- Array / Stack / Queue
- BFS / DFS
- DP / Hash Table / Backtracking
- Priority queue / Heap
Bitmap
In LeetCode problems, we often need to count occurrences of alphabetical characters. A simple approach is to create a number[] and use the characters' ASCII values as indices. Since the ASCII codes for these characters span from 65 ('A') to 122 ('z'), we need an array of size 58 ($122 - 65 + 1 = 58$) to fit them all. Note that this range also includes non-alphabetical symbols such as [, \, ], ^, and _.
let sum = new Array(58).fill(0);
let A = 'A'.charCodeAt();
for (let char of 'hello') {
sum[char.charCodeAt(0) - A]++; //Would be a good idea to guard it with < 128.
}
Alternatively, a simpler way is to use 128 as the size to cover the full ASCII. And by using typed arrays, slots are initialized as 0s like follow:
let sum = new Int32Array(128); // will be filled with 0 by itself
for (let char of 'hello') {
sum[char.charCodeAt(0)]++; //Would be a good idea to guard it with < 128.
}
Array / Stack / Queue
They are the two basic data structures in programming, with different usage and advantages.
| Operation | Description | Time Complexity |
|---|---|---|
| arr.push() | Add element in both queue and stack | O(1) |
| arr.pop() | Remove element from stack (back) | O(1) |
| arr.shift() | Remove element from queue (front) | O(N) |
| arr.splice(a,b) | Add / Remove / Modify a section of array | O(N) |
| arr.slice(a,b) | Get a portion of the array | O(N) |
Here are some benchmarks I tested. Shift is way more inefficient than I expected.

We can observe two important points:
-
shift()runs in O(N) time because JavaScript does not provide a native queue implementation. This can become a bottleneck in problems with strict time limits. -
Both
splice()andslice()run in O(N) time. While this is expected, use of these methods can still cause performance issues under tight computational constraints.
The solution to these two problems is using pointers.
shift()
For shift(), a better real-world practice is to use packages like "yocto-queue". But in LeetCode, a simple implementation can be achieved by using a head pointer pointing to the start of the array.
To add an element, we do the same, push():
let head = 0; // init
let arr: number[] = [];
arr.push(1);
And to shift() an element, we utilize the head pointer:
console.log(arr[head]);
head++; //the first element is gone.
// optional: if head > 1000, splice the first 1000 out
And to iterate the array, we do:
// (arr.length - head) becomes the size of the array
while (arr.length - head > 0) {
console.log(arr[head]);
head++;
}
for (let i = head; i < arr.length; i++) {
// do sth here
}
We can wrap all these into a class, but in the context of LeetCode, this is more than enough. If we need to solve a bigger question, I would prefer using a real library instead.
splice() and slice()
In some questions like 224. Basic Calculator, we need to work on the array recursively (at least in my approach). These two operations create a new array and will lead to run time exceeded error. Instead, using the old school two pointers to indicate start and end, and a pass by reference of the array would do the trick in O(1).
DFS / BFS
They are the fundamentals of many LeetCode problems, which are not difficult but worth mentioning. One easy application of them is traversing a tree.
Depth-first search is usually implemented with recursion. Where we recurse in a different order to achieve a different traversal order.
// Left, Right, Current (LRC order) / post-order
function dfs(root: TreeNode): void {
if (root == null) return;
dfs(root.left);
dfs(root.right);
console.log(root.val);
}
Breadth-first search is usually implemented using a queue. We can carry information such as the level using a tuple. Here we traversing the tree layer by layer:
function bfs(root: TreeNode): void {
if (root == null) return;
let queue: [TreeNode, number][] = [[root, 0]];
while (queue.length > 0) {
let [node, level] = queue.shift();
console.log(`Value: ${node.val}, Level: ${level}`);
if (node.left != null) {
queue.push([node.left, level + 1]);
}
if (node.right != null) {
queue.push([node.right, level + 1]);
}
}
}
DP / HashMap / Backtracking
Dynamic Programming (DP) Essentially, DP is about saving and reusing intermediate answers in a recursive problem. Take [70. Climbing Stairs](https://leetcode.com/problems/climbing-stairs/) as an example:
First, we need to figure out a recursive answer / path.
function climbStairs(n: number): number {
if (n == 1) return 1;
if (n == 2) return 2;
return climbStairs(n - 1) + climbStairs(n - 2);
}
Then we add a way to memorize the results we used before, usually with a HashMap.
function climbStairs(n: number): number {
let seen = new Map<number, number>();
seen.set(1, 1);
seen.set(2, 2);
function c(n: number): number {
if (seen.has(n)) return seen.get(n);
let result = c(n - 1) + c(n - 2);
seen.set(n, result);
return result;
}
return c(n);
}
And once we understand our solution is about building the answer from the ground up, i.e., to calculate N=4, we need to calculate N=3 and N=2, we can further optimize the algorithm.
function climbStairs(n: number): number {
if (n <= 2) return n;
let prev2 = 1;
let prev1 = 2;
for (let i = 3; i <= n; i++) {
const current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
HashMap
I stumbled across an interesting comment under the LRU cache question. And the key questions here are:
- Is Map in JavaScript deterministic (follow insertion order)?
- Is insert/delete operations in Map O(1)

Answer:
-
A map in JavaScript is deterministic. Which means the order of the elements follows the insertion order. You can find this from ECMA 24.1.3.5. The same also applies to Set.

-
Both Insert or Delete are O(1) operations, regardless of the size. I used a benchmark script to verify the O(1) characteristics.
Note: 16777216 items is the design maximum of the V8 engine. https://stackoverflow.com/questions/54452896/maximum-number-of-entries-in-node-js-map

Back Tracking
Honestly, I don't think I have enough backtracking knowledge to write insight on it, haha.
Backtracking is simply returning to the previous step when there is no path. Usually, backtracking is recursion-based, aims to trim off failed paths and avoid forking multiple copies of data by modifying the steps.
Priority queue / Heap
It is a data structure where we only care about the Largest element / Smallest element. It is a complete binary tree presented as an array.
| Operation | Time Complexity |
|---|---|
| Insert | O(log n) |
| Find Min / Find Max | O(1) |
| Delete Min / Delete Max | O(log n) |
| Increase Key / Decrease Key | O(log n) |
| Build Heap | O(n) |
| Merge (two heaps) | O(n) (not efficient for binary heaps) |
In LeetCode, you can simply use the Heap type (or implement it like me if you have not done it before)
bottomHalf: MaxHeap<number> = new MaxHeap<number>();
topHalf: MinHeap<number> = new MinHeap<number>();
LeetCode status as of 2026-01-23
