For six weeks I built things.
Week 7 I solved problems.
It's a different muscle. Building an app is about organising features and data so a user can do something useful. Solving an algorithm problem is about taking a precise question — given this input, return this output, as efficiently as possible — and finding the sharpest path through it. This week I learned how to talk about "efficient," started a brand new repository just for these solutions, and worked through eleven problems from the NeetCode 150.
Here's what happened.
Day 43 — Big O Notation: Naming the Thing I'd Been Feeling
Every project I've built so far has had a "this feels slow" or "this feels fine" instinct behind it. Day 43 gave that instinct a name: Big O notation.
The idea: describe how an algorithm's time or space requirements grow as the input grows, ignoring constant factors and focusing on the shape of the growth. A few that immediately mapped onto code I'd already written:
-
O(1) — constant time. A dictionary lookup.
dict[key]takes the same time whether the dictionary has 10 entries or 10 million. -
O(n) — linear time. A loop through a list once. Checking every item in
seenfrom my password manager. - O(n²) — quadratic time. Nested loops over the same data — the kind of thing that feels fine on small input and falls apart on large input.
- O(log n) — logarithmic time. Binary search — cutting the problem in half every step.
The verification task for the day was to explain why a dictionary lookup is O(1) and a list lookup is O(n). The answer: a dictionary uses hashing to jump straight to a value's location, while a list has to walk through elements one at a time until it finds a match (or doesn't).
I went back to my Week 6 URL shortener and looked at generate_code():
if code not in urls:
urls is a dictionary. That in check is O(1). If I'd built the URL shortener as a list of tuples instead, that same line would have been O(n) — checking every stored URL one at a time for every single code generation attempt. The data structure decision I made in Week 6 without thinking about complexity turned out to already be the right one. Naming why it was right was the real lesson of Day 43.
Day 44 — Arrays & Hashing: Three Classic Problems
This is also the day I started a brand new GitHub repository — neetcode-submissions — separate from my main progress repo. DSA practice felt like its own track, deserving its own home.
Two Sum
Given an array and a target, return the indices of the two numbers that add up to the target. The naive approach checks every pair — O(n²). The hash map approach does it in one pass:
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
seen = {}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
if seen[complement] > i:
return [i, seen[complement]]
return [seen[complement], i]
seen[num] = i
The insight: instead of asking "have I seen a pair that sums to target?", ask "have I seen the complement of this number before?" Every number gets stored in seen as you go, so by the time you reach any number, every number before it is already a fast O(1) lookup away. One pass, O(n) time.
Contains Duplicate
Does any value appear more than once in the array?
class Solution:
def hasDuplicate(self, nums: List[int]) -> bool:
seen = set()
for num in nums:
if num in seen:
return True
seen.add(num)
return False
A set instead of a dict here because I don't need to store where a number was seen — just whether. Sets are the right tool when you only care about membership.
Valid Anagram
Are two strings anagrams of each other?
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
if len(s) != len(t):
return False
count = {}
for i in range(len(s)):
count[s[i]] = count.get(s[i], 0) + 1
count[t[i]] = count.get(t[i], 0) - 1
for value in count.values():
if value != 0:
return False
return True
The trick here: count letters in s up, and the same letters in t down, in the same pass. If the two strings are true anagrams, every letter's net count ends at exactly zero. count.get(s[i], 0) was a small but important detail — it avoids a KeyError on a letter's first appearance.
All three problems solved with one pattern underneath them: trade memory for speed using a hash map or set. That pattern repeats so often in algorithm problems that recognising it on sight is apparently half the battle.
Day 45 — More Hashing: Anagrams and Top-K
Group Anagrams
Given a list of strings, group the ones that are anagrams of each other:
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
groups = {}
for word in strs:
key = "".join(sorted(word))
if key not in groups:
groups[key] = []
groups[key].append(word)
return list(groups.values())
The elegant part: sorted(word) turns any anagram into the same canonical key. "eat", "tea", and "ate" all sort to ['a', 'e', 't'], joined back into "aet". Anagrams collapse into identical dictionary keys automatically — no manual comparison between every pair of words needed.
Top K Frequent Elements
Given a list, return the k most frequent elements:
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
count = {}
for num in nums:
count[num] = count.get(num, 0) + 1
bucket = [[] for _ in range(len(nums) + 1)]
for number, freq in count.items():
bucket[freq].append(number)
result = []
for freq in range(len(bucket) - 1, -1, -1):
for num in bucket[freq]:
result.append(num)
if len(result) == k:
return result
return result
This one took the longest to understand. The naive approach is to count frequencies, then sort by frequency — but sorting costs O(n log n). The bucket approach is smarter: create a list of empty buckets, one for every possible frequency (1 through len(nums)), and drop each number into the bucket matching its frequency. Then walk the buckets from highest frequency down to lowest, collecting numbers until you have k of them.
No sorting at all. O(n) time. The first time I saw "frequency as an array index" rather than "frequency as a value to sort by," something shifted in how I think about using data structures creatively.
Day 46 — Two Pointers: A Different Way to Walk an Array
Two pointers is a pattern where instead of one index walking through data, two indices move toward each other (or apart) based on conditions — usually on sorted data.
Valid Palindrome
class Solution:
def isPalindrome(self, s: str) -> bool:
left, right = 0, len(s) - 1
while left < right:
while left < right and not s[left].isalnum():
left += 1
while left < right and not s[right].isalnum():
right -= 1
if s[left].lower() != s[right].lower():
return False
left += 1
right -= 1
return True
left starts at the beginning, right starts at the end. They walk toward each other, skipping non-alphanumeric characters with the inner while loops, comparing characters as they go. The moment they meet in the middle without a mismatch, it's a palindrome.
Two Sum II (sorted input)
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
left, right = 0, len(numbers) - 1
while left < right:
current_sum = numbers[left] + numbers[right]
if current_sum == target:
return [left + 1, right + 1]
elif current_sum < target:
left += 1
else:
right -= 1
return []
Because the input is already sorted, two pointers beats the hash map approach from Day 44 — no extra memory needed at all. If the current sum is too small, move left up (increase the sum). Too big, move right down (decrease the sum). This was the moment "sorted input" started meaning something specific to me: it's a signal that two pointers might be the better tool.
3Sum
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
result = []
n = len(nums)
for i in range(n - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue
left = i + 1
right = n - 1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total < 0:
left += 1
elif total > 0:
right -= 1
else:
result.append([nums[i], nums[left], nums[right]])
while left < right and nums[left] == nums[left + 1]:
left += 1
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
return result
This was the hardest problem of the week. It's Two Sum II wrapped inside another loop — fix one number, then two-pointer the rest of the array for a pair that sums to the negative of the fixed number. The two while loops after finding a valid triplet (while left < right and nums[left] == nums[left + 1]) exist purely to skip duplicate values so the same triplet doesn't get added twice. That duplicate-skipping logic took several tries to get right, and getting it wrong silently produces duplicate answers rather than crashing — which made it a genuinely good debugging exercise.
Day 47 — Sliding Window: Watching a Window Move
Sliding window is a cousin of two pointers — instead of two indices meeting in the middle, you maintain a "window" of elements that expands and shrinks as you scan through the data.
Best Time to Buy and Sell Stock
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if len(prices) < 2:
return 0
min_so_far = prices[0]
max_profit = 0
for price in prices[1:]:
profit = price - min_so_far
if min_so_far > price:
min_so_far = price
if profit > max_profit:
max_profit = profit
return max_profit
One pass through the prices. Track the lowest price seen so far (min_so_far) and the best profit achievable if you sold today (price - min_so_far). This is technically more "running minimum" than classic sliding window, but the core idea is shared: maintain state about a window of the past as you scan forward, rather than recomputing from scratch at every position.
Longest Substring Without Repeating Characters
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
seen = {}
start = 0
max_length = 0
for i, char in enumerate(s):
if char in seen and seen[char] >= start:
start = seen[char] + 1
seen[char] = i
current_length = i - start + 1
if current_length > max_length:
max_length = current_length
return max_length
This is the real sliding window. start marks the left edge of the current window, i marks the right edge as it scans forward. The moment a repeated character shows up inside the current window (seen[char] >= start), the window's left edge jumps past the previous occurrence. The window never re-scans characters it's already passed — every character gets visited exactly once, giving O(n) time despite checking for duplicates within a moving range.
The check seen[char] >= start rather than just char in seen was the detail I got wrong on my first attempt. A character can appear in seen from before the current window started — that old appearance shouldn't shrink the window. Only a repeat inside the current window matters.
Day 48 — Review + Mock: Solving Without Hints, Against the Clock
The last day of the week had no new concepts. Just three problems from earlier in the week, re-solved from scratch, with a timer running and no notes to check.
The verification bar: solved in under 30 minutes each, clean code.
What this day actually tested wasn't whether I remembered the syntax — it was whether I understood the pattern well enough to rebuild the logic without re-deriving it from first principles. Re-solving Two Sum II under time pressure, I reached for two pointers immediately because the input being sorted is now a recognised signal, not something I have to puzzle out again.
That's the real measure of whether a concept "clicked" this week: not whether I can explain it once, but whether I reach for the right tool automatically under pressure four days later.
📁 What I Solved This Week
| Problem | Pattern | File |
|---|---|---|
| Two Sum | Hash map | two-integer-sum/submission-0.py |
| Contains Duplicate | Set | duplicate-integer/submission-0.py |
| Valid Anagram | Hash map (counting) | is-anagram/submission-0.py |
| Group Anagrams | Hash map (canonical key) | anagram-groups/submission-0.py |
| Top K Frequent Elements | Bucket sort | top-k-elements-in-list/submission-0.py |
| Valid Palindrome | Two pointers | is-palindrome/submission-0.py |
| Two Sum II | Two pointers | two-integer-sum-ii/submission-0.py |
| 3Sum | Two pointers + sort | three-integer-sum/submission-2.py |
| Best Time to Buy/Sell Stock | Sliding window (running min) | buy-and-sell-crypto/submission-0.py |
| Longest Substring Without Repeating Characters | Sliding window | longest-substring-without-duplicates/submission-0.py |
All of it lives in its own dedicated repo:
👉 github.com/Omk4314/neetcode-submissions
What Actually Clicked This Week
- Big O gives you language, not just intuition. I already had a sense for "this feels slow." Now I can say why — and compare two approaches before writing either one.
-
Hash maps and sets trade memory for speed. That single sentence explains four of this week's problems. Recognising "I need fast lookup" as the actual requirement is the real skill, not memorising the syntax of
dict(). - Sorted input is a hint, not a coincidence. Every time a problem says the input is sorted, two pointers is probably available and probably better than a hash map.
- Sliding window means never re-scanning. The longest-substring problem only works in O(n) because the window's left edge only ever moves forward, never backward.
- Mock tests under time pressure reveal what you actually know. Day 48 was uncomfortable in a useful way — it's much easier to follow a worked example than to reproduce the same idea from a blank file with a clock running.
What I Want to Learn Next
Week 8 is going to push further into the NeetCode 150:
- Stacks — a data structure I've used without naming, time to use it on purpose
- Binary search — O(log n) properly, not just defined
- Linked lists — my first non-array-based data structure
- Trees — recursion is coming whether I'm ready or not
Seven weeks of building things. One week of solving problems. Both feel like real programming — just different muscles. I suspect Month 2 is going to keep alternating between them.
If you're grinding NeetCode or LeetCode too, drop your favourite problem from this week's list in the comments — I'd love to compare approaches.
See you in Week 8. 🐍
Week 7 complete. New repo, new muscle, same daily habit.













