LeetCode: Two Pointers II Linked List

Table Of Contents
206. Reverse Linked List ::4:: - Easy
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: Recursive Linked List Reversal - Linked List/Simple Traversal
- Solution 2: Recursive Using Not Linked List Reversal - Linked List/Simple Traversal
- Solution 3: Iterative Linked List Reversal - Linked List/Simple Traversal
- Solution 4: Iterative Using While Linked List Reversal - Linked List/Simple Traversal
287. Find the Duplicate Number ::3:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug: While True instead of While slow != fast
- Solution 1: Max Bit Length Duplicate Flips - Linked List/Simple Traversal
- Solution 2: Pigeonhole Over Range 1 to N Then Count Array Binary Search - Linked List/Simple Traversal
- Solution 3: [Follow up] [Linked List] 3 Step Modified Floyd Cycle FindMeeting() To ResetSlow() To FindCycleStart() [SC Opt] - Linked List/Simple Traversal
143. Reorder List ::4:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Linked List] 3 Step With Floyd Cycle Half Modification To Slow.next Reversal Manual Split To Right == None Merge Prepared Tail - Linked List/Simple Traversal
- Solution 2: [Linked List] 3 Step With Floyd Cycle Half Modification To Slow Reversal Manual Split To Right == Slow Merge Prepared Tail - Linked List/Simple Traversal
- Solution 3: [Stack] Floyd Cycle With Stack - Linked List/Simple Traversal
- Solution 4: [Stack] Brain Teaser slow longer or equal Right list Stack - Linked List/Simple Traversal
19. Remove Nth Node From End of List ::2:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Linked List] Two Pass GetLength() then StopPrev() To The SkipIndex() - Linked List/Simple Traversal
- Solution 2: [Linked List] One Pass Modified Floyd Algorithm With Fast Starting At kth Index - Linked List/Simple Traversal
138. Copy List with Random Pointer ::3:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Linked List] Two Pass Hashmap Create Nodes and Set Next and Random Deep References - Linked List/Simple Traversal
- Solution 2: [Linked List] One Pass Memoization HashMap Lazy Construction - Linked List/Simple Traversal
- Solution 3: [Linked List] Three Pass In Place Interleaving Technique [SC Opt] - Linked List/Simple Traversal
146. LRU Cache ::2:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug: Did not add to cache when adding to linked list put()
- Solution 1: [Doubly Linked List] Doubly Linked List with Hashmap - Linked List/Simple Traversal
- Solution 2: [Doubly Linked List] Python Implementation OrderedDict a Doubly Linked List with Hashmap - Linked List/Simple Traversal
23. Merge k Sorted Lists ::2:: - Hard
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Linked List] [MinHeap] Single MinHeap To Do Something Priority Queue - Linked List/Simple Traversal
- Solution 2: [Linked List] [Divide And Conquer] Iterative Divide And Conquer Merging 2 Lists Per Round So Log() [SC Opt] - Linked List/Simple Traversal
114. Flatten Binary Tree to Linked List ::3:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Tree] Recursive Pre Order and Relink - Linked List/Simple Traversal
- Solution 2: [Tree] Iterative Pre Order Stack - Linked List/Simple Traversal
- Solution 3: [Tree] Morris Traversal In Place [SC Opt] - Linked List/Simple Traversal
117. Populating Next Right Pointers in Each Node II ::3:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: [Tree] BFS Level Order - Linked List/Simple Traversal
- Solution 2: [Tree] Iterative Next Level Linked List - Linked List/Simple Traversal
- Solution 3: [Tree] Iterative Next Level Linked List V2 [SC Opt] - Linked List/Simple Traversal
Linked List Intro:
Leetcode problems with elegant solutions using a linked list.
What is a Linked List
A linked list is a linear data structure where each element (node) points to the next. It does not offer direct index access like arrays, as all traversal is pointer based.
Structure of a Linked List
Every linked list falls into one of two structural forms:
- Linear (acyclic)
- Nodes form a straight path that ends at None
- No loops or repeated visits
A → B → C → D → None- Path + Cycle (Cyclic)
- Initial path of nodes eventually linked back to an earlier node, forming a cycle.
- Structure: Path (μ nodes) → Cycle (λ nodes repeating forever)
A → B → C → D → E
↑ ↓
H ← G ← FOverall: Linked lists are either just a path or a path that leads to a cycle
Why Use a Linked List
Linked lists are ideal for:
- Memory efficient manipulation (no resizing like arrays)
- Representing dynamic data structures (stacks, queues, etc)
- Insertions and deletions in constant O(1) time (if the node is known)
Linked List Application: Linear Traversal
We can traverse a linked list node by node using a single pointer. Commonly used for printing, searching, summing values, etc.
Ex: Count number of nodes in the list
def countNodes(head):
count = 0
curr = head
while curr:
count += 1
curr = curr.next
return countLinked List Application: Dummy Head Trick
Using a dummy (sentinel) node simplifies edge cases in linked list operations, especially when manipulating the head node. Dummy node precedes the head, providing the uniform way to handle deletions, insertions, or merges without adding logic to handle the head.
Ex: Remove nth node from end of a list using dummy head
def removeNthFromEnd(head: Optional[ListNode], n: int) -> Optional[ListNode]:
# dummy (sentinel) node used to point to the head
dummy = ListNode(0, head)
fast = slow = dummy
# Move fast pointer n+1 steps ahead to keep gap
for _ in range(n + 1):
fast = fast.next
# Move both pointers until fast reaches end
while fast:
fast = fast.next
slow = slow.next
# Remove the nth node (slow.next)
slow.next = slow.next.next
# Return head dummy is pointing to (may be different from original)
return dummy.next Linked List Application: Tortoise and Hare
We can represent fast and slow pointers to traverse the list. Useful for splitting lists in half for sorting or palindrome checks and for checking for cycles.
Ex: Return middle node in list
def findMiddle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
# If F == None: Even length list
# S points to start of right half
# Left and right halves have equal length
# If F != None: Odd length list
# S points to middle node (belongs to left half)
# Left half is longer by 1 element
# None S: None
# S^F^ F: None
# 0 None S: 0
# S^F^ F: 0
# 0 1 None S: 1
# S^ F^ F: None
# 0 1 2 None S: 1
# S^ F^ F: 2
# 0 1 2 3 None S: 2
# S^ F^ F: None
# 0 1 2 3 4 None S: 2
# S^ F^ F: 4
# 0 1 2 3 4 5 None S: 3
# S^ F^ F: None
# 0 1 2 3 4 5 6 None S: 3
# S^ F^ F: 6Linked List Application: In Place Modification
Iteratively manipulate linked list during traversal for O(1) time and space.
Ex: Reverse the entire list and return a new head
def reverseList(head):
prev = None
curr = head
while curr:
next_node = curr.next
curr.next = prev
prev = curr
curr = next_node
return prev206. Reverse Linked List ::4:: - Easy
Topics: Linked List, Recursion
Intro
Given the head of a singly linked list, reverse the list, and return the reversed list. A linked list can be reversed either iteratively or recursively. Could you implement both?
| Example Input | Output |
|---|---|
| head = [1,2,3,4,5] | [5,4,3,2,1] |
| head = [1,2] | [2,1] |
| head = [] | [] |
Constraints:
The number of nodes in the list is the range [0, 5000].
-5000 ≤ Node.val ≤ 5000
Abstraction
Given a linked list, reverse it iteratively and recursively.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: Recursive Linked List Reversal - Linked List/Simple Traversal
def reverseList(self, currNode: Optional[ListNode]) -> Optional[ListNode]:
# Recursive Linked Lists:
# A linked list is made up of nodes which point to other nodes
# They must have pointers and they may or may not have values:
#
# () -> () -> () -> None
# (1) -> (3) -> (7) -> None
# Reverse Linked List:
# - Update parents to point to their children
# - Original head points to None (becoming a tail)
# - Return the original tail representing the start of the list (becoming a head)
#
# (1) -> (3) -> (7) -> None
# None <- (1) <- (3) <- (7)
# Empty Case:
# - Nothing to reverse, just return 'None'
if currNode == None:
return currNode
# Base Case:
# - Reached 'None' of original list
# - CurrHead is at the original tail, which will become the new head,
# - Begin passing original tail back up the recursive call
# tc: O(1)
if currNode.next == None:
return currNode
# Recursion:
# call reverse list on the rest of the list (head.next to end),
# and eventually the base case starts to pass back the tail (new head)
# Recursive call stack is n times so O(n) space and time
# tc: O(n)
# sc: O(n)
newHead = self.reverseList(currNode.next)
# -----------------
# Reverse order next and curr node
# Point next node to current node
currNode.next.next = currNode
# Point curr node to 'None' (temporarily act as a tail)
currNode.next = None
# overall: tc O(n)
# overall: sc O(n)
return newHead| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: Recursive Using Not Linked List Reversal - Linked List/Simple Traversal
def reverseList(self, currNode: Optional[ListNode]) -> Optional[ListNode]:
# Reverse Linked List:
# - Update parents to point to their children
# - Original head points to None (becoming a tail)
# - Return the original tail representing the start of the list (becoming a head)
# (1) -> (3) -> (7) -> None
# None <- (1) <- (3) <- (7)
# See Solution 1 Notes:
# Same as previous, just simplified with 'not'
# In python 'None' is considered 'falsy',
# meaning it behaves like False in conditional statements such as this one
# Empty Case:
# - Nothing to reverse, just return 'None'
if not currNode:
return currNode
# Base Case:
# - Reached 'None' of original list
# - CurrHead is at the original tail, which will become the new head,
# - Begin passing original tail back up the recursive call
# tc: O(1)
if not currNode.next:
return currNode
# reverse the rest of the list (.next onwards)
newHead = self.reverseList(currNode.next)
next = currNode.next
next.next = currNode
currNode.next = None
# overall: tc O(n)
# overall: sc O(n)
return newHead | Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 3: Iterative Linked List Reversal - Linked List/Simple Traversal
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Reverse Linked List:
# - Update parents to point to their children
# - Original head points to None (becoming a tail)
# - Return the original tail representing the start of the list (becoming a head)
# (1) -> (3) -> (7) -> None
# None <- (1) <- (3) <- (7)
# Creating a 'None' to act as the end of the list,
# so the new tail can point at it
prev = None
# Curr will point to the previous element
curr = head
# Iterate until we have reached the end of the list
# (remember the tail points to None):
# head -> () -> () -> tail -> None
# tc: iterate over n O(n)
while curr != None:
# Grab next node before disconnecting from current node
nextNode = curr.next
# Reverse/disconnect current element,
# and point to previous node:
curr.next = prev
# Before reverse:
# <- prev curr -> next -> ()
# After reverse:
# <- prev <- curr next -> ()
# Iterate both nodes: prev and curr
prev = curr
curr = nextNode
# Before iteration:
# <- prev <- curr next -> ()
# After iteration:
# <- () <- prev curr -> ()
# Loop exits when curr == None:
# Before exiting:
# () -> tail -> None
# After exiting:
# () <- prev -> curr
# curr is at 'None'
# prev is at the original tail / the new head
newHead = prev
# overall: tc O(n)
# overall: sc O(1)
return newHead | Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 4: Iterative Using While Linked List Reversal - Linked List/Simple Traversal
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Reverse Linked List:
# - Update parents to point to their children
# - Original head points to None (becoming a tail)
# - Return the original tail representing the start of the list (becoming a head)
# (1) -> (3) -> (7) -> None
# None <- (1) <- (3) <- (7)
prev = None
curr = head
# See Solution 3 Notes:
# Same as previous, just simplified with 'not'
# In python 'None' is considered 'falsy',
# meaning it behaves like False in conditional statements such as this one
while curr:
nextNode = curr.next
curr.next = prev
prev = curr
curr = nextNode
# overall: tc O(n)
# overall: sc O(1)
return prev | Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
21. Merge Two Sorted Lists ::2:: - Easy
Topics: Linked List, Recursion
Intro
You are given the heads of two sorted linked lists list1 and list2. Merge the two lists into one sorted list. The list should be made by splicing together the nodes of the first two lists. Return the head of the merged linked list.
| Example Input | Output |
|---|---|
| list1 = [1,2,4], list2 = [1,3,4] | [1,1,2,3,4,4] |
| list1 = [], list2 = [] | [] |
| list1 = [], list2 = [0] | [0] |
Constraints:
The number of nodes in the list is the range [0, 50].
-100 ≤ Node.val ≤ 100
Both list1 and list2 are sorted in non-decreasing order.
Abstraction
Given two sorted linked lists, merge and return head of merged list.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: Recursive Linked List Merging - Linked List/Simple Traversal
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
# Merge Linked Lists:
# - Compare two current heads of lists, grab smaller one
# - Iterate the head we just grabbed
# - When one list runs out of items, link the rest of the non empty list
# List1 and List2:
# (1) -> (3) -> (7) -> None
# (2) -> (9) -> (10) -> None
# MergedList:
# (1) -> (2) -> (3) -> (7) -> (9) -> (10) -> None
# Base case:
# One of the lists is empty,
# return remaining portion of the non empty list up the recursion call
if not list1:
return list2
if not list2:
return list1
# Compare two current heads of lists, grab smaller one:
if list1.val < list2.val:
# Grab list1 node,
# iterate the head we just grabbed by passing the next node to the recursive call,
# and connect list1 node by pointing to the rest of the merged list
list1.next = self.mergeTwoLists(list1.next, list2)
# return merged list1 node
return list1
else:
# Grab list2 node,
# iterate the head we just grabbed by passing the next node to the recursive call,
# and connect list2 node by pointing to the rest of the merged list
list2.next = self.mergeTwoLists(list1, list2.next)
# return merged list2 node
return list2
# overall: tc O(n + m)
# overall: sc O(n)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: Iterative DummyHead Linked List Merging [SC Opt] - Linked List/Simple Traversal
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
# Merge Linked Lists:
# - Iterate over both lists at the same time
# - Compare two current heads of lists, grab smaller one
# - When one list runs out of items, link the rest of the non empty list
# List1 and List2:
# (1) -> (3) -> (7) -> None
# (2) -> (9) -> (10) -> None
# MergedList:
# (1) -> (2) -> (3) -> (7) -> (9) -> (10) -> None
# DummyHead:
# - Allows entering the iterative loop cleanly
# without handling first node assigning case
# - Always points at original head
# sc: O(1)
dummyHead = ListNode(-1)
# Linked List Iterator:
# sc: O(1)
curr = dummyHead
# Traverse both lists while both have nodes
# tc: O(m + n)
while list1 and list2:
# grab smaller value between the two and iterate that list
if list1.val < list2.val:
# Grab list1 node
# iterate list1 (new head becomes next node)
curr.next = list1
list1 = list1.next
else:
# Grab list2 node
# iterate list2 (new head becomes next node)
curr.next = list2
list2 = list2.next
# iterate merged list
curr = curr.next
# One list is empty,
# connect remaining portion of non-empty list
if list1:
curr.next = list1
else:
curr.next = list2
# Originally:
# dh lst1 -> () -> ...
# curr lst2 -> () -> ...
# After:
# dh -> merged -> () -> ...
# dummyHead:
# dh is still pointing to original head of the merged list
newHead = dummyHead.next
# overall: tc O(m + n)
# overall: sc O(1)
return newHead| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
141. Linked List Cycle ::2:: - Easy
Topics: Hash Table, Linked List, Two Pointers
Intro
Given head, the head of a linked list, determine if the linked list has a cycle in it. There is a cycle in a linked list if there is some node in the list that can be reached again by continuously following the next pointer. Internally, pos is used to denote the index of the node that tail's next pointer is connected to. Note that pos is not passed as a parameter. Return true if there is a cycle in the linked list. Otherwise, return false. Follow up: Can you solve it using O(1) (i.e. constant) memory?
| Example Input | Output |
|---|---|
| head = [1,2,3,4,5] | [5,4,3,2,1] |
| head = [1,2] | [2,1] |
| head = [] | [] |
Constraints:
The number of nodes in the list is the range [0, 5000].
-5000 ≤ Node.val ≤ 5000
Abstraction
Given a linked list, determine if there exists a cycle.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Linked List] Track Seen Notes In Set - Linked List/Simple Traversal
def hasCycle(self, head: Optional[ListNode]) -> bool:
# Linked List Cycles:
#
# (1) -> (3) -> (7) -> (9)
# ^ | (cycle)
# | v
# (11)<- (10)
# Linked List Cycle Validation:
# To check if a linked list, we simply iterate over the list
# while keeping a set of nodes we have seen,
# and if we try to add a node we have already seen, a cycle exists
# Nodes stored by obj reference:
# sc: O(n)
seen = set()
# Linked List Iteration Representation:
# sc: O(1)
curr = head
# Iterate until we hit the 'None' at the end of the list
# tc: iterate over list O(n)
while curr:
# Check: is current node in seen list
# Implies: we have passed through as cycle
if curr in seen:
return True
# Implies: have not seen current node
# Then: add to seen list, continue iteration
seen.add(curr)
# Iterate
curr = curr.next
# Implies: have reached the end of list, no node was seen twice
# Then: no cycles exist
# overall: tc O(n)
# overall: sc O(n)
return False| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Linked List] Floyd Cycle Detection Tortoise and Hare [SC Opt] - Linked List/Simple Traversal
def hasCycle(self, head: Optional[ListNode]) -> bool:
# Floyd Cycle Detection Algorithm (Tortoise And Hare):
# Iterate linked list using 2 pointers:
# - fast: a pointer going 2x speed (hare)
# - slow: a pointer going 1x speed (tortoise)
# Cycle Validation:
# - Cycle exists:
# fast pointer hit the cycle and catches up with slow
# - No cycle Exists:
# fast pointer exits linked list and never catches up
# with slow
# Linked List Cycle:
#
# (1) -> (3) -> (7) -> (9)
# ^ | (cycle)
# | v
# (11)<- (10)
# Floyd Iterators:
# sc: O(1)
slow, fast = head, head
# Iterate until either:
# - fast hits the 'None' at the end of the list
# - fast hits the slow pointer
# tc: iterate over n O(n)
while fast and fast.next:
# Iterate slow 1x and fast 2x
slow = slow.next
fast = fast.next.next
# Check: if fast hit slow
# Implies: fast passed through a cycle
if slow == fast:
return True
# Implies: fast has reached end of list,
# never caught up with slow, no cycle exists in list
# overall: tc O(n)
# overall: sc O(1)
return False| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
287. Find the Duplicate Number ::3:: - Medium
Topics: Array, Two Pointers, Binary Search, Bit Manipulation
Intro
Given an array of integers nums containing n + 1 integers where each integer is in the range [1, n] inclusive. There is only one repeated number in nums, return
this repeated number. You must solve the problem without modifying the array nums and using only constant extra space.
| Example Input | Output |
|---|---|
| nums = [1,3,4,2,2] | 2 |
| nums = [3,1,3,4,2] | 3 |
| nums = [3,3,3,3,3] | 3 |
Constraints:
1 ≤ n ≤ 105
nums.length == n + 1
1 ≤ nums[i] ≤ n
All the integers in nums appear only once except for precisely one integer which appears two or more times.
Abstraction
Given list, return which number is a duplicate.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug: While True instead of While slow != fast
def findDuplicate(self, nums: List[int]) -> int:
slow = nums[0]
fast = nums[0]
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
slow = nums[0]
# INCORRECT:
# we are taking a step before the while loop check
# (which in some edge cases will get messy)
# Instead:
# while True:
# if slow == fast:
# break
# slow = nums[slow]
# fast = nums[fast]
while True:
slow = nums[slow]
fast = nums[fast]
if slow == fast:
break
return slowSolution 1: Max Bit Length Duplicate Flips - Linked List/Simple Traversal
def findDuplicate(self, nums: List[int]) -> int:
# Note:
# 1. Idea: Compare bit counts between full nums list and range [1..n]
# 2. Duplicate number will cause extra set bits in some positions
# 3. Identify bits where counts mismatch, reconstruct duplicate from bits
n = len(nums) - 1
bit_reconstruction = 0
# max_bits = how many binary bits needed to represent the largest number
# determines how many bit positions we need to check
# [1, 3, 4, 2, 2] -> 4 bit_length is 3 bits (100 binary)
# we will check bit positions 0, 1, 2
max_bit = max(num.bit_length() for num in nums)
# building duplicate number by iterating over each available bit position
# bit = 0 -> mask = 1 (binary 001)
# bit = 1 -> mask = 2 (binary 010)
# bit = 2 -> mask = 3 (binary 100)
for bit in range(max_bit):
# current mask
# 0 -> 1 << 0 = 0001
# 1 -> 1 << 1 = 0010
# 2 -> 1 << 2 = 0100
# 3 -> 1 << 3 = 1000
mask = 1 << bit
# for each num in array, count if bit is set
# &: bitwise AND
# sets current bit to 1 if both mask and bit are set to 1
# else sets current bit to 0,
# determines if current num has the bit set
curr_bit_set_count = sum((num & mask) > 0 for num in nums)
# across expected range [1..n], sum if bit is expected to be set
# &: bitwise AND
# checks how many numbers within range [1..n]
# are expected to have current bit set.
expected_bit_set_count = sum((i & mask) > 0 for i in range(1, n + 1))
# compare bit set to expected bit set,
# if bit is set more times than expected
# duplicate number must have that bit set:
# add it to bit reconstruction
if curr_bit_set_count > expected_bit_set_count:
bit_reconstruction |= mask
# overall: time complexity O(n log n)
# overall: space complexity O(1)
return bit_reconstruction| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| Bit Counting | O(n log n) | O(1) | log n bit positions, O(n) scan per bit | Only counters and bitmask |
| Max Bit Calc | O(n) | O(1) | Iterate to determine max bits | No additional memory allocation for iteration O(1) |
| Overall | O(n log n) | O(1) | Iteration for log n each with n dominates, leading to O(n log n) | No additional memory allocation, leading to constant O(1) |
Solution 2: Pigeonhole Over Range 1 to N Then Count Array Binary Search - Linked List/Simple Traversal
def findDuplicate(self, nums: List[int]) -> int:
# Note:
# Idea: Duplicate number must satisfy pigeonhole principle:
# With n+1 numbers range [1..n], at least one value is duplicated
# (n containers, n+1 pigeons -> at least one container has two pigeons)
# 1. Binary search on value range [1..n]
# 2. For mid value:
# mid = number of containers in lower half (including mid)
# count = numbers of pigeons (nums <= mid) in lower half
# 3. Decision:
# If count <= mid -> equal or lesser pigeons to containers -> duplicate is in upper half
# Else count > mid -> more pigeons than containers -> duplicate is in lower half
# smallest value 1
# largest value n (but we have n+1) n+1 - 1 -> n (the largest value)
left, right = 1, len(nums) - 1
# [ 1 4 3 5 2 ], for some n,
# there must be n-1 numbers below:
# for 2, there is 1 number below
# for 5, there are 4 numbers below
# Binary optimization search: '<'
while left < right:
mid = (left + right) // 2
# count how many numbers <= mid
curr_count = sum(num <= mid for num in nums)
# if count <= mid, there are no extra numbers in lower half:
# duplicate must be in upper half
if curr_count <= mid:
left = mid + 1
# if count > mid, there are too many numbers in lower half:
# duplicate must be in lower half
else:
right = mid
# Loop exit: left == right
# left and right are pointing at duplicate number
# overall: time complexity O(n log n)
# overall: space complexity O(1)
return left| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| Binary Search | O(log n) | O(1) | Binary search in O(log n) | No additional memory allocation for binary search |
| Counting | O(n) | O(1) | Iterate over list of n length | No additional memory allocation for iteration |
| Overall | O(n log n) | O(1) | Binary search O(log n) each with iteration of O(n) dominates, leading to O(n log n) | No additional memory allocation O(1) |
Solution 3: [Follow up] [Linked List] 3 Step Modified Floyd Cycle FindMeeting() To ResetSlow() To FindCycleStart() [SC Opt] - Linked List/Simple Traversal
def findDuplicate(self, nums: List[int]) -> int:
# ----------------------------------------------------------------------------------
# Linked List Find Cycle Start Algorithm Proof:
# 1. Variable Definitions:
# L = distance from head to start of cycle
# C = length of the cycle (number of nodes in the cycle)
# x = distance from the start of the cycle to the meeting point inside the cycle
# k = number of full cycles the fast pointer has completed by the time of the first meeting
# 2. Setup:
# Iteration 1: Slow + Fast Until They Meet
# Slow Reset: After Slow + Fast Meet,
# immediately set Slow back to head,
# then run again until Slow and Fast meet again
# Iteration 2: After the reset,
# Slow and Fast are guaranteed to meet at the cycle start
# -----------------------------------------
# Solving For L Variable Definitions:
# Distance Traveled:
# L + x + (k * C) steps
# Where:
# L steps to reach the start of the cycle
# x steps inside the cycle to the meeting point
# k full extra loops around the cycle (each length C)
# Solving For L:
# Total Distance Traveled To Meeting Point:
# Distance = Distance
# Fast Pointer Distance = Slow Pointer Distance
# L + x + (k * C) = 2(L + x)
# Solve for L:
# L + x + kC = 2L + 2x
# kX = L + x
# L = kC - x
# -----------------------------------------
# Cycle Wrap around Proof
# Traveling x steps inside cycle of length C =
# to traveling C - x steps backwards, due to wrap around
# x steps forward = C - x backwards
# so:
# x = C - x
# -----------------------------------------
# Plugging In L Variable Definition and Wrap Around Proof:
# L = kC - x
# L = kC + (C - x)
# L = kC + (C - x) <-- this proves moving slow to right will cover
# once we move slow to the head:
# L -> distance from head to start of cycle
# fast will stay at the meeting point:
# kC + (C - x)
# kC -> number of cycles (we can ignore this)
# (C - x) -> steps remaining to get to the start (since we have traveled x steps already)
# L = (C - x)
# so both slow and fast will travel the same amount of steps
# to get to the start of the cycle
# -----------------------------------------
# Treating The Array Like A Linked List With A Cycle:
# Cycle:
# since the list only has 1 duplicate (which may appear multiple times),
# that means a cycle exists.
# Array: value: [3, 1, 3, 4, 2]
# index: 0 1 2 3 4
# Treat array as linked list: index -> nums[index]
# Problem set up with duplicate guarantees cycle in linked list representation
# Array (Linked List) With Cycle (Horizontal):
#
# value: 3 4 2 3 4 2
# index: [0] -> [3] -> [4] -> [2] -> [3] -> [4] ...
# Array (Linked List) With Cycle (Vertical + Horizontal):
#
# value: 3 4 2 3
# index: [0] -> [3] -> [4] -> [2]
# ^ ^
# | |
# -------------------
# cycle
# nums[0]=3 -> jump to index 3
# nums[3]=4 -> jump to index 4
# nums[4]=2 -> jump to index 2
# nums[2]=3 -> jump to index 3 <= We've been here already,
# We've hit index 3 twice, cycle!
# Per this diagram:
# The goal is then to find the start of the cycle,
# which starts at the duplicate number,
# in this case the duplicate value of 3 at index 0 and 2
# with the start of the cycle being at index 3
# ----------------------------------------------------------------------------------
# The 3 Step Modified Floyd Cycle Algorithm
# -----------------------------------------
# Iteration 1:
# Ran Slow and Fast normally at x1 and x2 respectively,
# until they meet somewhere in the linked list
# Slow Pointer:
# Distance Traveled:
# L + x steps
# Where:
# L steps to reach the start of the cycle
# x steps inside the cycle to the meeting pointer
# -----------------------------------------
# Iteration 1: First Meeting Point
# Linked List Iterators:
# sc: O(1)
slow = nums[0]
fast = nums[0]
# Iterate Slow and Fast at x1 and x2 respectively so they meet at:
# Slow Fast
# L + x + (k * C) steps = 2(L + x) steps
# steps into the cycle where:
# L steps to reach the start of the cycle
# x steps inside the cycle to the meeting point
# k full extra loops around the cycle (each length C)
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
# -----------------------------------------
# Reset Slow: Set Back To Head
# Reset slow to head to prepare for iteration 2
slow = nums[0]
# -----------------------------------------
# Iteration 2: Second Meeting Point
# Fast and slow will meet at start of cycle,
# which starts at some instance of the duplicate number
# (duplicate number may appear more than twice)
while True:
if slow == fast:
break
slow = nums[slow]
fast = nums[fast]
# overall: tc O(n)
# overall: sc O(1)
return slow| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| Cycle Detection | O(n) | O(1) | Each pointer moves less than or equal to 2n O(n) | No additional memory allocation for iteration O(1) |
| Finding Entry | O(n) | O(1) | Both pointers move less than or equal to n steps O(n) | No additional memory allocation for iteration O(1) |
| Overall | O(n) | O(1) | Two iterations over list of n length dominate, leading to O(n) | No additional memory allocation, leading to O(1) |
83. Remove Duplicates from Sorted List ::2:: - Easy
Topics: Linked List
Intro
Given the head of a sorted linked list, delete all duplicates such that each element appears only once. Return the linked list sorted as well.
| Example Input | Output |
|---|---|
| head = [1,1,2] | [1,2] |
| head = [1,1,2,3,3] | [1,2,3] |
Constraints:
The number of nodes in the list is in the range [0, 300].
-100 ≤ Node.val ≤ 100
The list is guaranteed to be sorted in ascending order.
Abstraction
Given a linked list, remove duplicates and return original.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Linked List] Iterative In Place Traversal - Linked List/Simple Traversal
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Note:
# 1. List is already sorted, so duplicates are always adjacent
# 2. Walk the list, whenever curr.val == curr.next.val, skip curr.next
# 3. Otherwise advance curr
# No dummy node needed since we never remove the head
curr = head
while curr and curr.next:
if curr.val == curr.next.val:
# skip duplicate
curr.next = curr.next.next
else:
# no duplicate, advance
curr = curr.next
return head
# tc: O(n)
# sc: O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
82. Remove Duplicates from Sorted List II ::2:: - Medium
Topics: Linked List, Two Pointers
Intro
Given the head of a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numbers from the original list. Return the linked list sorted as well.
| Example Input | Output |
|---|---|
| head = [1,2,3,3,4,4,5] | [1,2,5] |
| head = [1,1,1,2,3] | [2,3] |
Constraints:
The number of nodes in the list is in the range [0, 300].
-100 ≤ Node.val ≤ 100
The list is guaranteed to be sorted in ascending order.
Abstraction
Given a linked list, any value that appears more than once is completely removed from the list, including the first occurrence.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Linked List] Iterative In Place Traversal - Linked List/Simple Traversal
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Note:
# 1. Dummy node needed since head itself may be a duplicate
# 2. prev pointer trails behind, only advances when curr group is distinct
# 3. When we find a duplicate value, skip ALL nodes with that value
# 4. Otherwise, advance prev and curr normally
dummyHead = ListNode(0, head)
prev = dummyHead
curr = head
while curr:
# check if curr is start of a duplicate group
if curr.next and curr.val == curr.next.val:
# skip all nodes with this value
dupVal = curr.val
while curr and curr.val == dupVal:
curr = curr.next
# connect prev to first node after duplicate group
prev.next = curr
else:
# distinct node, safe to advance prev
prev = curr
curr = curr.next
return dummyHead.next
# tc: O(n)
# sc: O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
143. Reorder List ::4:: - Medium
Topics: Linked List, Two Pointers, Stack, Recursion
Intro
You are given the head of a singly linked-list. The list can be represented as: There is a cycle in a linked list if there is some node in the list that can be L0 → L1 → … → Ln - 1 → Ln Reorder the list to be on the following form: L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → … You may not modify the values in the list's nodes. Only nodes themselves may be changed.
| Example Input | Output |
|---|---|
| head = [1,2,3,4] | [1,4,2,3] |
| head = [1,2,3,4,5] | [1,5,2,4,3] |
Constraints:
The number of nodes in the list is the range [1, 5 * 104].
1 ≤ Node.val ≤ 1000
Abstraction
Given a linked list, order it in values tending towards middle.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Linked List] 3 Step With Floyd Cycle Half Modification To Slow.next Reversal Manual Split To Right == None Merge Prepared Tail - Linked List/Simple Traversal
def reorderList(self, head: Optional[ListNode]) -> None:
# -----------------------------------------
# Linked List Reorder Pattern:
# Before
# (1) -> (2) -> (3) -> (4) -> (5) -> None
# After
# (1) -> (5) -> (2) -> (4) -> (3) -> None
# This combination is a zig zag of the following:
# (1 connects to 5, then 2, then 4, etc)
#
# list1 => (1) -> (2) -> (3) -> None
# list2 => (5) -> (4) -> None (+)
# Where:
# list1 = first half of the list
# list2 = reversed second half of the list
# -----------------------------------------
# 1. First Half Of List
# - keep pointing to original head
# - stop iteration at half
# - point middle to None to split list in half
# -----------------------------------------
# 2. Reversed Second Half Of List
# - stop iteration at half
# - keep pointing at middle element
# - reverse list starting at middle element
# -----------------------------------------
# 3. Join the two elements
# - iterate over forward and reversed list
# - add in correct order
# -----------------------------------------
# 1. First Half Of List
# Floyd Cycle Algorithm Variation:
# - use the slow fast pointers to find the middle of the list
# Odd Even Case:
# - depending on whether the list has an even or odd number of nodes,
# slow pointer will stop at either:
#
# s f
# () -> () -> () -> () -> () -> None
# mid
#
# s f
# () -> () -> () -> () -> None
# mid1 mid2
# slow travels half distance vs fast pointer
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# Slow result:
# Odd List:
# - slow = mid
# Even List:
# - slow = mid2
# -----------------------------------------
# Prepare for 2. reverse list:
# 1.1: set curr to beginning of right half
# 1.2: disconnect left and right half of lists
# Odd List:
# - curr = slow.next, (mid.next)
# left will be 1 element longer (keeps mid element)
#
# - - - - L - - - - - R - -
# s s.next f
# () -> () -> () -> () -> () -> None
# mid
# Even List:
# - curr = slow.next, (mid2.next)
# left half will be 2 elements longer (keeps mid1 and mid2)
#
# - - - - L - - - - R -
# s.next
# s f
# () -> () -> () -> () -> None
# mid1 mid2
# Prepare for iterative reversal with prev/curr
prev = None
curr = slow.next
# Disconnect 1st half of list:
# Set slow to be the tail of the left list (belonging solely to left list)
# - slow -> None
slow.next = None
# Odd Even Case:
# s s.next f
# () -> () -> () () -> () -> None
# mid
# |
# V
# None
#
# s.next
# s f
# () -> () -> () () -> None
# mid1 mid2
# |
# v
# None
# -----------------------------------------
# 2. Reverse Second Half Of List
# Keep reversing until we reach the end of the right half
# tc: O(n/2) ~ O(n)
while curr:
# grab next and reverse flow
next_node = curr.next
curr.next = prev
# iterate
prev = curr
curr = next_node
# After iteration:
# prev = tail
# prev.next = None
# -----------------------------------------
# 3. Connect First Half And Reversed Second Half:
# iterate over left half and right half
# - head is still at the start of the original list (start of the left half)
# - prev is at the end of the original list (start of the right half)
left = head
right = prev
# Explicit Termination Needed:
# right half is always shorter than the left half
# (by 1 or 2 nodes for even and odd original lists, see above)
# These extra 1 or 2 nodes serve as a prepared tail for the left half:
# - - - - L - - - - - R - -
# s f
# (1) -> (2) -> (3) -> (4) -> (5) (1) -> (2) -> (3) -> None (1) -> (5) -> (2) -> (4) -> (3) -> None
# mid ==> (5) -> (4) -> None ==>
# ^
# right terminates,
# notice how "(3) -> None" is already prepared
# - - - - L - - - - R -
# s f
# (1) -> (2) -> (3) -> (4) (1) -> (2) -> (3) -> None (1) -> (4) -> (2) -> (3) -> None
# mid1 mid2 ==> (4) -> None ==>
# ^
# right terminates,
# notice how "(2) -> (3) -> None" is already prepared
# The extra 1 or 2 prepared tail,
# allows us to know that when right terminates,
# the rest of the remaining list is already in the correct order
while right:
# grab next before disconnect
next_left, next_right = left.next, right.next
# merge lists with the alternating pattern (see above)
left.next = right
right.next = next_left
# iterate both lists
left, right = next_left, next_right
# overall: tc O(n)
# overall: sc O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Linked List] 3 Step With Floyd Cycle Half Modification To Slow Reversal Manual Split To Right == Slow Merge Prepared Tail - Linked List/Simple Traversal
def reorderList(self, head: Optional[ListNode]) -> None:
# -----------------------------------------
# Linked List Reorder Pattern:
# Before
# (1) -> (2) -> (3) -> (4) -> (5) -> None
# After
# (1) -> (5) -> (2) -> (4) -> (3) -> None
# This combination is a zig zag of the following:
# (1 connects to 5, then 2, then 4, etc)
#
# list1 => (1) -> (2) -> (3) -> None
# list2 => (5) -> (4) -> None (+)
# Where:
# list1 = first half of the list
# list2 = reversed second half of the list
# -----------------------------------------
# 1. First Half Of List
# - keep pointing to original head
# - stop iteration at half
# - point middle to None to split list in half
# -----------------------------------------
# 2. Reversed Second Half Of List
# - stop iteration at half
# - keep pointing at middle element
# - reverse list starting at middle element
# -----------------------------------------
# 3. Join the two elements
# - iterate over forward and reversed list
# - add in correct order
# -----------------------------------------
# 1. First Half Of List
# Floyd Cycle Algorithm Variation:
# - use the slow fast pointers to find the middle of the list
# Odd Even Case:
# - depending on whether the list has an even or odd number of nodes,
# slow pointer will stop at either:
#
# s f
# () -> () -> () -> () -> () -> None
# mid
#
# s f
# () -> () -> () -> () -> None
# mid1 mid2
# slow travels half distance vs fast pointer
slow = fast = head
while fast and fast.next:
fast = fast.next.next
slow = slow.next
# Slow result:
# Odd List:
# - slow = mid
# Even List:
# - slow = mid2
# -----------------------------------------
# Prepare for 2. reverse list:
# 1.1: set curr to beginning of right half
# 1.2: disconnect left and right half of lists
# Odd List:
# - curr = slow, (mid)
# left will be 1 element longer (keeps mid element)
#
# - - - - L - - - - - R - -
# s f
# () -> () -> () -> () -> () -> None
# mid
# Even List:
# - curr = slow, (mid2)
# left and right half will be equal
#
# - - L - - - - R - -
# s f
# () -> () -> () -> () -> None
# mid1 mid2
# Prepare for iterative reversal with prev/curr
prev = None
curr = slow
# Implicit Disconnect:
# Both left and right contain slow (either odd mid or mid2) in their list
# during the first iteration of reversing the list, we set "slow -> None"
# now, both lists share the same reference to "slow -> None"
# Odd Even Case:
# - depending on whether the list has an even or odd number of nodes,
# slow pointer will stop at either:
#
# s f
# () -> () -> () () -> () -> None
# mid
# |
# V
# None
#
#
# s f
# () -> () -> () () -> None
# mid1 mid2
# |
# V
# None
# Implicit Right -> Left Connect:
# In the first iteration of the reversal we set "slow -> None" and then "slow.next -> slow":
#
# s f
# () -> () -> () <- () -> () -> None
# mid
# |
# V
# None
#
#
# s f
# () -> () -> () <- () -> None
# mid1 mid2
# |
# V
# None
# -----------------------------------------
# 2. Reverse Second Half Of List
# Keep reversing until we reach the end of the right half
# tc: O(n/2) ~ O(n)
while curr:
# grab next and reverse flow
next_node = curr.next
curr.next = prev
# iterate
prev = curr
curr = next_node
# After iteration:
# prev = tail
# prev.next = None
# -----------------------------------------
# 3. Connect First Half And Reversed Second Half:
# iterate over left half and right half
# - head is still at the start of the original list (start of the left half)
# - prev is at the end of the original list (start of the right half)
left = head
right = prev
# Implicit Termination:
# right is either equal or shorter than the left half
# (by 1 node for odd and equal length for even original lists, see above)
# This extra 1 node or evenness serve as a prepared tail for the left half
# - - - - L - - - - - R - -
# s f
# (1) -> (2) -> (3) -> (4) -> (5) (1) -> (2) -> (3) -> None (1) -> (5) -> (2) -> (4) -> (3) -> None
# mid ==> (5) -> (4) -> (mid/3) ==>
# ^
# right terminates,
# notice how "(4) -> (3) -> None" is already prepared
#
# - - L - - - - R - -
# s f
# (1) -> (2) -> (3) -> (4) (1) -> (2) -> (3) -> None (1) -> (4) -> (2) -> (3) -> None
# mid1 mid2 ==> (4) -> (5) -> (mid/3) ==>
# ^
# right terminates,
# notice how "(4) -> (3) -> None" is already prepared
# the extra 1 or even tail,
# allows us to know that when right == slow,
# the rest of the remaining list is already in the correct order
# tc: O(n/2) ~ O(n)
while right != slow:
# grab next before disconnect
next_left, next_right = left.next, right.next
# merge lists with the alternating pattern (see above)
left.next = right
right.next = next_left
# iterate both lists
left, right = next_left, next_right
# brain teaser:
# why does this work:
right.next = None
# but right.next.next = None
# does not work
# overall: tc O(n)
# overall: sc O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 3: [Stack] Floyd Cycle With Stack - Linked List/Simple Traversal
def reorderList(self, head: Optional[ListNode]) -> None:
# Note:
# 1. Use Tortoise Hare to find the middle
# 2. Iterate over right half in order and store elements in stack
# 3. Pop all elements (in reverse order) and place in between left elements:
# left -> right -> left.next
# Result: Fully reordered
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# Slow result:
# Odd List: slow -> odd mid
# Even List: slow -> mid2
# store right half list in order
# space complexity: store half list of n length O(n)
stack = []
# Set Curr:
# Odd List:
# curr = slow.next, (mid.next)
# left will be 1 element longer (keeps mid element), right does not
# Even List:
# curr = slow.next, (mid2.next)
# left will be 2 elements longer (keeps mid1 and mid2), right does not
curr = slow.next
# Explicit Disconnect:
# Set slow (belonging solely to left list)
# to slow -> None
slow.next = None
# time complexity: iterate over list of n length O(n)
while curr:
stack.append(curr)
curr = curr.next
# forward iteration over list
left = head
# Odd List Len: left will be 1 element longer (keeps mid element)
# Even List Len: left is 2 elements longer (keeps 1st and 2nd mid elements)
# Explicit Termination:
# right half stack is always than the left half (by 1 or 2 nodes)
# when we terminate right,
# right.next = next_left guarantees that
# the rest of the left list was connected,
# and since we already terminated the left list 'Explicit Disconnect'
# our merged list will terminate
# time complexity: iterate over stack O(n)
while stack:
# iterate over right half
right = stack.pop()
# set: left -> right -> left.next
right.next = left.next
left.next = right
# iterate left list
left = right.next
# overall: time complexity O(n)
# overall: space complexity O(n)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 4: [Stack] Brain Teaser slow longer or equal Right list Stack - Linked List/Simple Traversal
def reorderList(self, head: Optional[ListNode]) -> None:
# Note:
# 1. Use Tortoise Hare to find the middle
# 2. Iterate over right half in order and store elements in stack
# 3. Pop all elements (in reverse order) and place in between left elements:
# left -> right -> left.next
# Result: Fully reordered
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# Slow result:
# Odd List: slow -> odd mid
# Even List: slow -> mid2
# store right half list in order
# space complexity: store list of n length O(n)
stack = []
# Set Curr:
# Odd List:
# curr = slow, (mid)
# left and right will be even (both keep mid element)
# Even List:
# curr = slow, (mid2)
# left will be 1 element longer (keeps mid1 and mid2), (right keeps mid2)
curr = slow
# No Implicit Disconnect:
# (deal with that later)
# time complexity: iterate over list of n length O(n)
while curr:
stack.append(curr)
curr = curr.next
# iterate over left list
left = head
# time complexity: iterate over stack O(n)
while stack:
# grab right half node element
right = stack.pop()
# place right half node in between left half curr and curr.next:
# curr -> tail -> curr.next
right.next = left.next
left.next = right
# iterate left half of list
left = right.next
# brain teaser:
# why does this work:
# Odd/Even Case:
# left is equal or longer by 1 node
# Odd:
# left and right are equal in length, both share 'slow'
# so at the last iteration, left -> right -> left.next
# but we never disconnected, so left slow -> right slow
# and since they share the same object in memory
# this does not matter since it is pointing to itself
# Even:
# left is longer by 1, both share 'slow'
# last iteration, right slow -> left slow
# but we never disconnected, so left slow -> right slow
# and since they share the same object in memory
# this does not matter since it is pointing to itself
right.next.next.next.next.next = None
# overall: time complexity O(n)
# overall: space complexity O(n)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
86. Partition List ::1:: - Medium
Topics: Linked List, Two Pointers
Intro
Given the head of a linked list and a value x, partition it such that all nodes less than x come before nodes greater than or equal to x. You should preserve the original relative order of the nodes in each of the two partitions.
| Example Input | Output |
|---|---|
| head = [1,4,3,2,5,2], x = 3 | [1,2,2,4,3,5] |
| head = [2,1], x = 2 | [1,2] |
Constraints:
The number of nodes in the list is in the range [0, 200].
-100 ≤ Node.val ≤ 100
-200 ≤ x ≤ 200
Abstraction
Given a linked list and a partition value, partition the list based on that value.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: todo - Linked List/Simple Traversal
def reorderList(self, head: Optional[ListNode]) -> None:
holder| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
147. Insertion Sort List ::1:: - Medium
Topics: Linked List, Sorting
Intro
Given the head of a singly linked list, sort the list using insertion sort, and return the sorted list's head. The steps of the insertion sort algorithm: First: Insertion sort iterates, consuming one input element each repetition and growing a sorted output list. Second: At each iteration, insertion sort removes one element from the input data, finds the location it belongs within the sorted list and inserts it there. Third: It repeats until no input elements remain.
| Example Input | Output |
|---|---|
| head = [4,2,1,3] | [1,2,3,4] |
| head = [-1,5,3,4,0] | [-1,0,3,4,5] |
Constraints:
The number of nodes in the list is in the range [1, 5000].
-5000 ≤ Node.val ≤ 5000
Abstraction
Given a linked list and a partition value, partition the list based on that value.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Linked List] Iterative In Place Insertion Sort - Linked List/Simple Traversal
def insertionSortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Note:
# 1. Dummy node needed since sorted portion grows from scratch
# 2. curr walks the unsorted input list one node at a time
# 3. For each curr, walk the sorted list from dummy to find insertion point
# 4. Insert curr between prev and prev.next in sorted list
dummyHead = ListNode(0)
curr = head
while curr:
# save next unsorted node before we overwrite curr.next
nextUnsorted = curr.next
# find insertion point in sorted list:
# walk until prev.next is >= curr, or end of sorted list
prev = dummyHead
while prev.next and prev.next.val < curr.val:
prev = prev.next
# insert curr between prev and prev.next
curr.next = prev.next
prev.next = curr
# advance to next unsorted node
curr = nextUnsorted
return dummyHead.next
# tc: O(n^2) — for each node, walk sorted portion to find insertion point
# sc: O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Linked List] Iterative In Place Insertion Sort [SC Opt] - Linked List/Simple Traversal
def insertionSortList(self, head: ListNode | None) -> ListNode | None:
dummy = ListNode(0)
prev = dummy # the last and thus largest of the sorted list
while head: # the current inserting node
next = head.next # Cache the next inserting node.
if prev.val >= head.val:
prev = dummy # Move `prev` to the front.
while prev.next and prev.next.val < head.val:
prev = prev.next
head.next = prev.next
prev.next = head
head = next # Update the current inserting node.
return dummy.next| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
148. Sort List ::1:: - Medium
Topics: Linked List, Two Pointers
Intro
Given the head of a linked list, return the list after sorting it in ascending order. Follow up: Can you sort the linked list in O(n logn) time and O(1) memory (i.e. constant space)?
| Example Input | Output |
|---|---|
| head = [4,2,1,3] | [1,2,3,4] |
| head = [-1,5,3,4,0] | [-1,0,3,4,5] |
| head = [] | [] |
Constraints:
The number of nodes in the list is in the range [0, 5 * 10^4]
-10^5 ≤ Node.val ≤ 10^5
Abstraction
Given a linked list and a partition value, partition the list based on that value.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Linked List] Recursive Top Down Merge Sort - Linked List/Simple Traversal
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Note:
# 1. Find midpoint with slow/fast pointers, split list in two
# 2. Recurse on each half
# 3. Merge the two sorted halves
# Base case: 0 or 1 nodes, already sorted
# base case
if not head or not head.next:
return head
# find midpoint and split
slow, fast = head, head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# split: slow is end of left half
mid = slow.next
slow.next = None
# recurse on each half
left = self.sortList(head)
right = self.sortList(mid)
# merge sorted halves
dummy = ListNode(0)
curr = dummy
while left and right:
if left.val <= right.val:
curr.next = left
left = left.next
else:
curr.next = right
right = right.next
curr = curr.next
curr.next = left or right
return dummy.next
# tc: O(n log n) — log n splits, O(n) merge per level
# sc: O(log n) — recursion stack depth| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Linked List] Iterative Bottom-Up Merge Sort [SC Opt] - Linked List/Simple Traversal
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Note:
# 1. Count list length
# 2. Instead of recursing top-down, merge bottom-up
# 3. Start with sublist size=1, merge pairs, double size each round
# 4. log n rounds, O(n) work per round -> O(n log n), O(1) space
if not head or not head.next:
return head
# count length
length = 0
node = head
while node:
length += 1
node = node.next
dummy = ListNode(0, head)
size = 1
while size < length:
curr = dummy.next
tail = dummy # tail of last merged group
while curr:
# split off left half of size `size`
left = curr
right = split(left, size)
curr = split(right, size)
# merge left and right, attach to tail
merged, tail = merge(left, right)
tail.next = curr # connect to remainder
size *= 2
return dummy.next
# tc: O(n log n)
# sc: O(1)
def split(head, size):
# walk `size` steps, sever link, return new head
for _ in range(size - 1):
if not head:
return None
head = head.next
if not head:
return None
rest = head.next
head.next = None
return rest
def merge(l1, l2):
# merge two sorted lists, return (head, tail)
dummy = ListNode(0)
curr = dummy
while l1 and l2:
if l1.val <= l2.val:
curr.next = l1
l1 = l1.next
else:
curr.next = l2
l2 = l2.next
curr = curr.next
curr.next = l1 or l2
while curr.next:
curr = curr.next
return dummy.next, curr
# tc: O(n)
# sc: O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
328. Odd Even Linked List ::1:: - Medium
Topics: Linked List
Intro
Given the head of a singly linked list, group all the nodes with odd indices together followed by the nodes with even indices, and return the reordered list. The first node is considered odd, and the second node is even, and so on. Note that the relative order inside both the even and odd groups should remain as it was in the input. You must solve the problem in O(1) extra space complexity and O(n) time complexity.
| Example Input | Output |
|---|---|
| head = [1,2,3,4,5] | [1,3,5,2,4] |
| head = [2,1,3,5,6,4,7] | [2,3,6,7,1,5,4] |
Constraints:
The number of nodes in the linked list is in the range [0, 10^4]
-10^6 ≤ Node.val ≤ 10^6
Abstraction
Given a linked list, group all the odd node followed by the even nodes.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Linked List] Iterative In Place Odd Even Partition - Linked List/Simple Traversal
def oddEvenList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Note:
# 1. Maintain two sublists: odd-indexed and even-indexed
# 2. odd pointer weaves through nodes 1, 3, 5, ...
# 3. even pointer weaves through nodes 2, 4, 6, ...
# 4. Save even head, append even list to end of odd list
if not head:
return head
odd = head
even = head.next
evenHead = even
while even and even.next:
# advance odd to next odd node
odd.next = even.next
odd = odd.next
# advance even to next even node
even.next = odd.next
even = even.next
# attach even list to end of odd list
odd.next = evenHead
return head
# tc: O(n)
# sc: O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
19. Remove Nth Node From End of List ::2:: - Medium
Topics: Linked List, Two Pointers
Intro
Given the head of a linked list, remove the nth node from the end of the list and return its head. Follow up: Could you do this in one pass?
| Example Input | Output |
|---|---|
| head = [1,2,3,4,5], n = 2 | [1,2,3,5] |
| head = [1], n = 1 | [] |
| head = [1,2], n = 1 | [1] |
Constraints:
The number of nodes in the list is sz.
1 ≤ sz ≤ 30
0 ≤ Node.val ≤ 100
1 ≤ n ≤ sz
Abstraction
Given a linked list, remove nth node and return head of list.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Linked List] Two Pass GetLength() then StopPrev() To The SkipIndex() - Linked List/Simple Traversal
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
# Note:
# 1. Traverse list once to calculate total length
# 2. Use a dummy node to handle edge cases (like removing head)
# 3. Traverse again, stop at node prior to removal index
# 4. point prior.next = prior.next.next, disconnecting removal node
# Result: removed node at distance n from end of list
# First Pass: get total length of list
# tc: O(n)
listLen = 0
curr = head
while curr:
listLen += 1
curr = curr.next
# DummyHead to keep track of original head
# sc: O(1)
dummyHead = ListNode(-1, head)
# Linked List Iterator
# sc: O(1)
curr = dummyHead
# Second Pass: calculate distance to node behind the target node
removeDist = listLen - n
i = 0
while i != removeDist:
i += 1
curr = curr.next
# Arrived at n-1th node:
# simply skip the nth node
curr.next = curr.next.next
# Originally:
# dh -> lst1 -> () -> ...
# curr
# After
# dh -> () -> () -> ...
# overall: tc O(n)
# overall: sc O(1)
return dummyHead.next| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Linked List] One Pass Modified Floyd Algorithm With Fast Starting At kth Index - Linked List/Simple Traversal
def removeNthFromEnd(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# Note:
# 1. Use two pointers spaced nth apart (fast and slow)
# 2. Move both pointers until fast hits end
# 3. Slow pointer will be right before the node to remove
# 4. Remove node by skipping it
# DummyHead to keep track of original head
# sc: O(1)
dummyHead = ListNode(-1, head)
# Linked List Iterators
# sc: O(1)
slow = fast = dummyHead
# Modified Floyd Algorithm With Fast Stopping:
# instead of doing fast x2 iteration,
# do a x1 iteration and make it stop at k+1 instead of 'None'
# tc: O(n)
i = 0
while i != k+1:
i += 1
fast = fast.next
# Fast is now at the nth index
# [ slow ... fast ... None]
# 0 ... k ... n
# Modified Floyd Algorithm With Fast Starting At k:
# There is a distance of k between the slow and fast pointers
# [slow .... n + 1 .... fast]
# Iterate slow and fast until fast reaches end of list:
# tc: O(n)
while fast:
fast = fast.next
slow = slow.next
# Before Iteration:
# slow = head
# fast = skip.next
# After Iteration:
# slow = skip.prev
# fast = 'None'
# Slow is pointing to Node we are planning to skip:
# skip it by changing .next to its parent (.next.next)
slow.next = slow.next.next
# Originally:
# dh -> lst1 -> () -> ...
# curr
# After
# dh -> () -> () -> ...
# overall: tc O(n)
# overall: sc O(1)
return dummyHead.next | Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
61. Rotate List ::2:: - Medium
Topics: Linked List, Two Pointers
Intro
Given the head of a linked list, rotate the list to the right by k places.
| Example Input | Output |
|---|---|
| head = [1,2,3,4,5], k = 2 | [4,5,1,2,3] |
| head = [0,1,2], k = 4 | [2,0,1] |
Constraints:
The number of nodes in the list is in the range [0, 500].
1 ≤ sz ≤ 30
-100 ≤ Node.val ≤ 100
0 ≤ k ≤ 2 * 10^9
Abstraction
Given a linked list, rotate the list by k steps
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: todo - Linked List/Simple Traversal
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
todo holder| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
138. Copy List with Random Pointer ::3:: - Medium
Topics: Hash Table, Linked List
Intro
A linked list of length n is given such that each node contains an additional random pointer, which could point to any node in the list, or null. Construct a deep copy of the list. The deep copy should consist of exactly n brand new nodes, where each new node has its value set to
the value of its corresponding original node. Both the next and random pointer of the new nodes should point to new nodes in the copied list such that the pointers in the original list and copied list represent the same list state. None of the pointers in the new list should point to nodes in the original list. For example, if there are two nodes X and Y in the original list, where X.random --> Y, then for the corresponding two nodes x and y in the copied list, x.random --> y. Return the head of the copied linked list. The linked list is represented in the input/output as a list of n nodes. Each node is represented as a pair of [val, random_index] where: val: an integer representing Node.val random_index: the index of the node (range from 0 to n-1) that the random pointer points to, or null if it does not point to any node. Your code will only be given the head of the original linked list.
| Example Input | Output |
|---|---|
| head = [[7,null],[13,0],[11,4],[10,2],[1,0]] | [[7,null],[13,0],[11,4],[10,2],[1,0]] |
| head = [[1,1],[2,1]] | [[1,1],[2,1]] |
| head = [[3,null],[3,0],[3,null]] | [[3,null],[3,0],[3,null]] |
Constraints:
0 ≤ n ≤ 1000
-104 ≤ Node.val ≤ 104
Node.random is null or is pointing to some node in the linked list.
Abstraction
Given a linked list where each node has an additional random node pointers, create a deep copy.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Linked List] Two Pass Hashmap Create Nodes and Set Next and Random Deep References - Linked List/Simple Traversal
def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
# .get():
# returns 'None' if nothing exists in the hashMap for that key,
# so its perfect for our use case of Node vs Tail cases
# Note:
# 1. Traverse once: create all nodes (no wiring of next/random yet)
# 2. Use a dictionary to map old nodes -> new nodes
# 3. Traverse Twice: wire up both next and random using the map
# 4. Return deep copy of head
# Empty Check:
if not head:
return None
# Orig node -> deep copy of node
origToDeepCopy = {}
# Iteration 1:
# Copy all nodes values (no next/random pointers yet)
# tc: O(n)
curr = head
while curr:
# create a deep copy,
# insert key:value into hashmap
# iterate original list
copy = Node(curr.val)
origToDeepCopy[curr] = copy
curr = curr.next
# Iteration 2:
# All nodes created in hashmap,
# now iterate over and assign next and random pointers from original list
# tc: O(n)
curr = head
while curr:
# grab deepCopy of node
deepCopy = origToDeepCopy[curr]
# Node vs Tail Grabbing Info:
# grab original node, and get .next and .random nodes,
# if we are at the tail .next and .random could == 'None',
# (No 'None' node exists in the HashMap)
# so .get() returns 'None' if no key:value pair exists,
# which covers our .next and .random == 'None cases for Tails
deepCopy.next = origToDeepCopy.get(curr.next)
deepCopy.random = origToDeepCopy.get(curr.random)
# Iterate over original list
curr = curr.next
# Return deepCopy of the head node,
# which will be connected to the rest of the deepCopy list
# overall: tc O(n)
# overall: sc O(n)
return origToDeepCopy[head]Solution 2: [Linked List] One Pass Memoization HashMap Lazy Construction - Linked List/Simple Traversal
def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
# Note:
# 1. Maintain a memo dictionary to store mapping on the fly
# 2. Iterate once through original list while building the copy
# 3. Create each copy node only whe first encountered (lazy construction)
# 4. Maintain a pointer to build out the next chain
# Empty check:
if not head:
return None
# DummyHead to keep track of original head
# sc: O(1)
dummyHead = Node(-1)
# prev allows deepCopy to connect once we create them lazily
# for the entire deepCopy list as we go
prev = dummy
curr = head
# orig -> deepCopy
memo = {}
# iterate over list of n length
# tc: O(n)
while curr:
# If we have not created a deepCopy of the curr node yet, create
if curr not in memo:
memo[curr] = Node(curr.val)
# Grab deepCopy
currDeepCopy = memo[curr]
# If curr node has a random connection
if curr.random:
# If we have not created a deepCopy of the curr.random node yet, create
if curr.random not in memo:
memo[curr.random] = Node(curr.random.val)
# Set deepCopy random to curr random
currDeepCopy.random = memo[curr.random]
# Connect previous iteration to curr node
prev.next = currDeepCopy
# Iterate over original list
prev = prev.next
curr = curr.next
# deepCopyHead = dummyHead.next
# or
deepCopyHead = memo[head]
# overall: tc O(n)
# overall: sc O(n)
return deepCopyHeadSolution 3: [Linked List] Three Pass In Place Interleaving Technique [SC Opt] - Linked List/Simple Traversal
def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
# -----------------------------------------
# Linked List Reorder Pattern:
# Before
# A -> B -> C -> D -> E -> None
# After
# A -> A' -> B -> B' -> C -> C' -> ... -> None
# This combination is a zip zag of the following:
# (A connects to A', then B, then B', etc)
#
# list1 => A -> B -> C -> None
# list2 => A' -> B' -> C' -> None (+)
# Where:
# list1 = original list
# list2 = deepCopy of nodes from original list
# -----------------------------------------
# 1. Merged List from Original List And Created DeepCopy Nodes
# - Start at original head
# - Iterate over original list
# - Create deepCopy of curr node as we iterate
# - Connect curr node to its corresponding deepCopy
# - Iterate to the next original node (skipping the deepCopy we just made)
# to continue making deepCopies for the rest of the original list
# -----------------------------------------
# 2. Split Merged List into Original And DeepCopy
# - Start at original head
# - Create deepCopy of curr node as we iterate
# - Connect curr node to its deepCopy
# - Iterate to the next original node (skipping the deepCopy we just made)
# -----------------------------------------
# Linked List Reorder Pattern Split:
# Before
# A -> A' -> B -> B' -> C -> C' -> ... -> None
# After
# A -> B -> C -> D -> E -> None
# A' -> B' -> C' -> D' -> E' -> None
# Where:
# mergedList = orig nodes with deepCopy nodes
# origList = orig list split off from mergedList
# deepCopyList = new deepCopyList split off from mergedList
# Which:
# leads to sc O(1) since we used the original input to create our deepCopy List
# Empty Check
if not head:
return None
# -----------------------------------------
# 1. Merged List from Original List And Created DeepCopy Nodes
# Linked List Iterator:
# sc: O(1)
curr = head
# Iteration 1: deepCopy Nodes
# tc: O(n)
while curr:
# Interleave deepCopy nodes
# A -> A' -> B -> B' -> C -> C'
deepCopy = Node(curr.val)
deepCopy.next = curr.next
# link A -> A' and iterate to next orig
curr.next = deepCopy
curr = deepCopy.next
# Linked List Iterator:
# sc: O(1)
curr = head
# Iteration 2: create the random connections
# tc: O(n)
while curr:
# grab intertwined deepCopy
deepCopy = curr.next
# If curr node has a random connection
if curr.random:
# Grab the deepCopy of the random node from the curr node reference:
# curr.random points to a original node, not the deepCopy,
# so we grab the deepCopy of the random node with curr.random.next,
# since original nodes and their deepCopy are directly next to each other:
# ... -> Random -> Random' -> ...
deepCopy.random = curr.random.next
# Iterate original list
curr = deepCopy.next
# -----------------------------------------
# 2. Split Merged List into Original And DeepCopy
# Original and DeepCopy List Iterators
# sc: O(1)
# head is still == A
curr = head
# head is .next comes from A -> A', so head.next is now == A'
deepCopyDummyHead = head.next
# Iterate over list of 2n length (node + deepCopyNode)
# tc: O(n)
while curr:
# grab curr nodes corresponding deepCopy (A -> A', grabbing A')
deepCopy = curr.next
# grab deepCopies next original node (A' -> B, grabbing B)
curr.next = deepCopy.next
# Iterate to B
curr = curr.next
# Before
# A -> A' -> B -> ...
# After
# A' -> B -> B' -> ...
# A -> B -> C -> ...
# If: B != 'None'
# Then: B must have (B -> B')
if curr:
# set A' -> B' (grabbing B' from B -> B')
deepCopy.next = curr.next
# Before
# A' -> B -> B' ...
# After
# A' -> B' -> ...
# return deepCopy head
# overall: tc O(n)
# overall: sc O(1)
return deepCopyDummyHead| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
2. Add Two Numbers ::2:: - Medium
Topics: Linked List, Math, Recursion
Intro
You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list. You may assume the two numbers do not contain any leading zero, except the number 0 itself.
| Example Input | Output |
|---|---|
| l1 = [2,4,3], l2 = [5,6,4] | [7,0,8] |
| l1 = [0], l2 = [0] | [0] |
| l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] | [8,9,9,9,0,0,0,1] |
Constraints:
The number of nodes in each linked list is in the range [1, 100].
0 ≤ Node.val ≤ 9
It is guaranteed that the list represents a number that does not have leading zeros.
Abstraction
Given a two linked lists representing numbers, return the sum.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: Recursive - Linked List/Simple Traversal
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
# Note:
# 1. Recursion to simulate digit addition with carry
# 2. Add Corresponding digits with carry passed to recursive call
# 3. Current node created with .next -> recursively created node
# 4. Base case: both lists empty and carry is 0
# Result: new sum list created recursively
# time complexity: iterate over two lists of m and n length O(max(m, n))
# space complexity: recursive call over two lists of m and n length O(max(m, n))
def recursiveSum(l1, l2, carry):
# Base Case:
# Stop once we have nothing to add
# - Both lists are empty
# - We have no carry
if not l1 and not l2 and carry == 0:
return None
# Check if value exists and grab
val1 = l1.val if l1 else 0
val2 = l2.val if l2 else 0
# Check curr sum
sum = val1 + val2 + carry
carry = sum//10
# Iterate lists if exist
l1 = l1.next if l1 else None
l2 = l2.next if l2 else None
# Create new node and put curr sum into it
newNode = ListNode(sum % 10)
# Attach node to next node
newNode.next = recursiveSum(l1, l2, carry)
# Pass curr node back up recursive calls
# tc: O(n)
# sc: O(n)
return newNode
# calculate new sum list over two lists
# overall: tc O(max(m, n))
# overall: sc O(max(m, n))
return recursiveSum(l1, l2, 0)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: Iterative - Linked List/Simple Traversal
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
# Note:
# 1. Dummy node to create a sum list
# 2. Iterate over both lists until complete
# 3. Added values node by node, and pass the carry
# Result: sum of two lists
# DummyHead to keep tack of original head
dummy = ListNode(-1)
# Linked List Iterators:
# sc: O(1)
prev = dummy
carry = 0
# Traverse while we still have something to add
# - either list is non empty
# - we have a carry value
while l1 or l2 or carry:
# Check if value exists and grab
v1 = l1.val if l1 else 0
v2 = l2.val if l2 else 0
# add values and carry
sum = v1 + v2 + carry
carry = sum // 10
# create new Node for new value
newNode = ListNode(sum % 10)
prev.next = newNode
prev = prev.next
# iterate list
l1 = l1.next if l1 else None
l2 = l2.next if l2 else None
# return new head of sum list
# overall: tc O(max(m, n))
# overall: sc O(n)
return dummyHead.next| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
146. LRU Cache ::2:: - Medium
Topics: Hash Table, Linked List, Design, Doubly Linked List
Intro
Design a data structure that follows the constraints of a Least Recently Used (LRU) cache. Implement the LRUCache class: LRUCache(int capacity) Initialize the LRU cache with positive size capacity. int get(int key) Return the value of the key if the key exists, otherwise return -1. void put(int key, int value) Update the value of the key if the key exists. Otherwise, add the key-value pair to the cache. If the number of keys exceeds the capacity from this operation, evict the least recently used key. The functions get and put must each run in O(1) average time complexity.
| Example Input | Output |
|---|---|
| nums = [1,3,4,2,2] | 2 |
| nums = [3,1,3,4,2] | 3 |
| nums = [3,3,3,3,3] | 3 |
Constraints:
1 ≤ capacity ≤ 3000
0 ≤ key ≤ 104
0 ≤ value ≤ 105
At most 2 * 105 calls will be made to get and put.
Abstraction
Design an efficient LRU cache using linked lists.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug: Did not add to cache when adding to linked list put()
class Node:
def __init__(self, key : int, val : int):
self.key = key
self.val = val
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity : int):
self.capacity = capacity
self.cache = {}
self.head = Node(0, 0)
self.tail = Node(0, 0)
self.head.next = self.tail
self.tail.prev = self.head
def _remove_node(self, node:Node):
prev, next = node.prev, node.next
prev.next = next
next.prev = prev
def _push_MRU(self, node:Node):
twoMRU = self.head.next
node.next = twoMRU
node.prev = self.head
self.head.next = node
twoMRU.prev = node
def get(self, key:int):
if key not in self.cache:
return -1
node = self.cache[key]
self._remove_node(node)
self._push_MRU(node)
return node.val
def put(self, key:int, val:int):
if key in self.cache:
node = self.cache[key]
self._remove_node(node)
node.val = val
else:
# INCORRECT:
# forgot to add new node to cache
# Instead:
# node = self.cache[key] = node
node = Node(key, val)
self._push_MRU(node)
if len(self.cache) > self.capacity:
lruNode = self.tail.prev
self._remove_node(lruNode)
self.cache.pop(lruNode.key)Solution 1: [Doubly Linked List] Doubly Linked List with Hashmap - Linked List/Simple Traversal
class Node:
def __init__(self, key: int, val: int):
# key -> value node
self.key = key
self.val = val
# doubly linked prev and next
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
# list capacity
self.capacity = capacity
# key -> Node
self.cache = {}
# MRU head
self.head = Node(0, 0)
# LRU tail
self.tail = Node(0, 0)
# set dummy head and tail
self.head.next = self.tail
self.tail.prev = self.head
# time complexity: O(1)
# space complexity: O(1)
def _remove_node(self, node: Node):
# Remove node from doubly linked list by linked neighbors
prev, next = node.prev, node.next
prev.next = next
next.prev = prev
# time complexity: O(1)
# space complexity: O(1)
def _push_MRU(self, node: Node):
# Insert node between dummy head and MRU
node.next = self.head.next
node.prev = self.head
# Update head and 2nd MRU
self.head.next = node
node.next.prev = node
# time complexity: O(1)
# space complexity: O(1)
def get(self, key: int) -> int:
# if node does not exist, return -1
if key not in self.cache:
return -1
# grab node
node = self.cache[key]
# Move node and update MRU
self._remove_node(node)
self._push_MRU(node)
# return val
return node.val
# time complexity: O(1)
# space complexity: O(1)
def put(self, key: int, value: int) -> None:
# If key exists, remove to avoid duplicates,
# need to grab node to update prev and next nodes
if key in self.cache:
node = self.cache[key]
self._remove_node(node)
# update existing node
node.val = value
# If key does not exist, create new node set to new value
else:
node = Node(key, value)
self.cache[key] = node
# push updates node to top of MRU
self._push_MRU(node)
# Remove LRU if capacity broken
if len(self.cache) > self.capacity:
lruNode = self.tail.prev
self._remove_node(lruNode)
self.cache.pop(lruNode.key)
# overall: time complexity O(1)
# overall: space complexity O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| remove | O(1) | O(1) | Linking prev and next of removed node in constant O(1) | No additional memory allocation for relinking nodes O(1) |
| insert_at_front | O(1) | O(1) | Linking new head in between dummy head and current head in constant O(1) | No additional memory allocation for relinking nodes O(1) |
| get | O(1) | O(1) | Hashmap lookup O(1) with DLL remove O(1) and DLL insert_at_front O(1) all in constant O(1) | No additional memory allocation for either sub step O(1) |
| put | O(1) | O(1) | Hashmap lookup O(1 ) with DLL remove O(1) and DLL insert_at_front O(1) all in constant O(1) | No additional memory allocation for either sub step O(1) |
| overall | O(1) | O(capacity) | All operations in constant O(1) | Hashmap and DLL will store up to capacity O(capacity) |
Solution 2: [Doubly Linked List] Python Implementation OrderedDict a Doubly Linked List with Hashmap - Linked List/Simple Traversal
class LRUCache:
def __init__(self, capacity: int):
# OrderedDict in Python is implemented internally
# as a doubly linked list + hashmap (same as our solution):
self.cache = OrderedDict()
self.capacity = capacity
# time complexity: O(1)
# space complexity: O(1)
def get(self, key: int) -> int:
# Check if key exists, else return -1
if key not in self.cache:
return -1
# Update MRU
self.cache.move_to_end(key)
# return value
return self.cache[key]
# time complexity: O(1)
# space complexity: O(1)
def put(self, key: int, value: int) -> None:
# If key exists, remove to avoid duplicates
if key in self.cache:
self.cache.pop(key)
# Insert new value, automatically updates MRU,
# automatically updates doubly linked list
self.cache[key] = value
# capacity exceeded, remove LRU
if len(self.cache) > self.capacity:
# last=True -> pops MRU
# last=False -> pops LRU
self.cache.popitem(last=False)
# overall: time complexity O(1)
# overall: space complexity O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| get | O(1) | O(1) | DLL move to end and lookup in constant O(1) | No additional memory allocation O(1) |
| put | O(1) | O(1) | DLL move to end and lookup in constant O(1) | No additional memory allocation O(1) |
| overall | O(1) | O(capacity) | All operations in constant O(1) | Hashmap and DLL store up to capacity O(capacity) |
23. Merge k Sorted Lists ::2:: - Hard
Topics: Linked List, Divide and Conquer, Heap (Priority Queue), Merge Sort
Intro
You are given an array of k linked-lists lists, each linked-list is sorted in ascending order. Merge all the linked-lists into one sorted linked-list and return it.
| Example Input | Output |
|---|---|
| lists = [[1,4,5],[1,3,4],[2,6]] | [1,1,2,3,4,4,5,6] |
| lists = [] | [] |
| lists = [[]] | [] |
Constraints:
k == lists.length
0 ≤ k ≤ 104
0 ≤ lists[i].length ≤ 500
-104 ≤ lists[i][j] ≤ 104
lists[i] is sorted in ascending order.
The sum of lists[i].length will not exceed 104
Abstraction
Given a list of sorted linked lists, combine into one sorted linked list.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Linked List] [MinHeap] Single MinHeap To Do Something Priority Queue - Linked List/Simple Traversal
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
# Note:
# 1. minHeap stores current head for each sorted list
# 2. minHeap sorted by value, list_index breaking ties:
# (value, list_index, node)
# 3. smallest value across heads added to list
# 4. push to minHeap to replaced added node
# Result: merge list in O(n log k)
# MinHeap():
# - O(log n) insertion and removal)
# MinHeap data representation: (value, index, node)
# - Need the value to validate min
# - Need index to check if ___
# - Need the node to modify connections for new linked list
minHeap = []
# For each non empty list,
# push the head into the heap
for i, head in enumerate(lists):
if head:
heappush(minHeap, (head.val, i, head))
# MinHeap() After:
# Contains the first node of each of the lists,
# with the min one, at the top of the minHeap
# DummyHead to keep track of new head
dummyHead = ListNode(-1)
prev = dummyHead
# While the minHeap still has nodes from lists
# - pop the smallest from the heap and add to the new list
# - grab a new node from the list we just lost a node from
# tc: O(n log k)
while minHeap:
# Current min node
(minVal, i, node) = heappop(minHeap)
# Point new list to curr node, and iterate
prev.next = node
prev = prev.next
# If curr min node, has more nodes in their list
if node.next:
# Grab next node from list and add to minHeap
heappush(minHeap, (node.next.val, i, node.next))
# minHeap Empty:
# all lists are empty, final list has been merged
mergedListHead = dummyHead.next
# overall: tc O(n log k)
# overall: sc O(k)
return mergedListHead| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Linked List] [Divide And Conquer] Iterative Divide And Conquer Merging 2 Lists Per Round So Log() [SC Opt] - Linked List/Simple Traversal
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
# Note:
# 1. Merge lists in pairs with standard merge two sorted lists
# 2. Each round halves the total number of lists -> O(log k) rounds
# 3. Each round processes all nodes (across all merges) -> O(n) work per round
# Result: Merging k lists in place
# Empty check
if not lists:
return None
# Merge two sorted linked lists
# time complexity: iterate over lists of n length O(n)
def mergeTwo(l1, l2):
# dummy node trick
dummy = ListNode(-1)
prev = dummy
# merge while both lists non empty
while l1 and l2:
# grab smaller head
if l1.val < l2.val:
prev.next = l1
l1 = l1.next
else:
prev.next = l2
l2 = l2.next
# iterate
prev = prev.next
# attach remaining list
if not l1:
prev.next = l2
else:
prev.next = l1
# return merged new merged list
return dummy.next
# iteratively merge lists in pairs until one list remains
# time complexity: log(len(list)) total merges
while len(lists) > 1:
merged = []
for i in range(0, len(lists), 2):
l1 = lists[i]
l2 = lists[i + 1] if i + 1 < len(lists) else None
merged.append(mergeTwo(l1, l2))
lists = merged
# overall: time complexity O(n log k)
# overall: space complexity O(1)
return lists[0]| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
24. Swap Nodes in Pairs ::2:: - Medium
Topics: Linked List, Recursion
Intro
Given a linked list, swap every two adjacent nodes and return its head. You must solve the problem without modifying the values in the list's nodes (i.e., only nodes themselves may be changed.)
| Example Input | Output |
|---|---|
| head = [1,2,3,4] | [2,1,4,3] |
| head = [] | [] |
| head = [1] | [1] |
| head = [1,2,3] | [2,1,3] |
Constraints:
The number of nodes in the list is in the range [0, 100].
0 ≤ Node.val ≤ 100
Abstraction
Given a linked list, swap every pair of elements.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Linked List] Iterative In Place Pair Swap - Linked List/Simple Traversal
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode(0)
dummy.next = head
prev = dummy
while prev.next and prev.next.next:
first = prev.next
second = prev.next.next
# swap
first.next = second.next
second.next = first
prev.next = second
prev = first
return dummy.next| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Linked List] Iterative In Place Pair Swap - Linked List/Simple Traversal
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Note:
# 1. Dummy node needed since head itself gets swapped
# 2. prev trails behind, stitches each swapped pair back to the list
# 3. For each pair: save pointers, swap, reconnect, advance
dummy = ListNode(0, head)
prev = dummy
while prev.next and prev.next.next:
# grab the pair
first = prev.next
second = prev.next.next
# swap
first.next = second.next
second.next = first
prev.next = second
# advance prev to end of swapped pair
prev = first
return dummy.next
# tc: O(n)
# sc: O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
25. Reverse Nodes in k-Group ::2:: - Hard
Topics: Linked List, Recursion
Intro
Given the head of a linked list, reverse the nodes of the list k at a time, and return the modified list. k is a positive integer and is less than or equal to the length of the linked list. If the number of nodes is not a multiple of k then left- out nodes, in the end, should remain as it is. You may not alter the values in the list's nodes, only nodes themselves may be changed. Follow-up: Can you solve the problem in O(1) extra memory space?
| Example Input | Output |
|---|---|
| head = [1,2,3,4,5], k = 2 | [2,1,4,3,5] |
| head = [1,2,3,4,5], k = 3 | [3,2,1,4,5] |
Constraints:
The number of nodes in the list is n. 1 ≤ k ≤ n ≤ 5000
0 ≤ Node.val ≤ 1000
-104 ≤ lists[i][j] ≤ 104
lists[i] is sorted in ascending order.
The sum of lists[i].length will not exceed 104
Abstraction
Given a linked lists, and group size k, reverse as many groups of k length as possible.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Linked List] Recursive - Linked List/Simple Traversal
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# ------------------------------------------------
# Reverse Nodes In k-Group Pattern: State
# k = 3, input: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> None
#
# Before:
# [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> None
# After:
# [2] -> [1] -> [4] -> [3] -> [6] -> [5] -> None
# Note:
# 1. Check if there are at least k nodes ahead (otherwise return head)
# 2. Reverse the first k nodes
# 3. Recurse and process sub list
# 4. Connect reversed head with resulting sub list
# Result: original lists with reversed groups of k length
# Early Exit:
# Get length of list,
# if list has less than k nodes, return head
kLen = 0
node = head
while node and kLen < k:
kLen += 1
node = node.next
if kLen < k:
return head
# Standard Iterative Linked List Reversal:
# Linked List Iterators:
# sc: O(1)
prev = None
curr = head
# Reverse the first k nodes
# tc: O(k)
for _ in range(k):
# Grab next node before disconnecting
# from current node
next = curr.next
# reverse flow of current node
curr.next = prev
# Iterate Linked List
prev = curr
curr = next
# Head is still pointing to original first node,
# but this original first node is now the new tail,
# which needs to be connected to the head of the
# next section, hence: we need to update head.next
head.next = self.reverseKGroup(curr, k)
# Prev is pointing to the original tail of the k section,
# but this original tail becomes the new head,
# hence, we need to return it up the recursive call
sectionNewHead = prev
# overall: tc O(n)
# overall: sc O(n)
return sectionNewHead| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Linked List] Iterative [SC Opt] - Linked List/Simple Traversal
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# ------------------------------------------------
# Reverse Nodes In k-Group Pattern: State
# k = 3, input: 1->2->3->4->5->6->None
#
# Before:
# [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> None
# After:
# [2] -> [1] -> [4] -> [3] -> [6] -> [5] -> None
# Note:
# 1. Dummy node handles head swaps cleanly
# 2. For each sub list:
# Find the kth node
# Reverse the group in place
# Connect previous reverse group's tail to head of new reversed group
# 3. Continue until remaining nodes < k, then return
# --------------------------------
# Initial State: k = 3, input: 1->2->3->4->5->6->None
#
# [dummy] -> [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> None
# |
# somePrevTail
# dummy node trick for head of overall list
dummyHead = ListNode(0, head)
somePrevTail = dummyHead
# Iterate linked list, while we still have at least k nodes
# tc: O(n)
while True:
# ------------------------------------------------
# Check if k nodes remain
# Last node (tail) of the prev k section
kthNode = somePrevTail
# Early Exit:
# If remaining list has less than k nodes, return head
# check if k more elements exist
for _ in range(k):
# Check next node
kthNode = kthNode.next
if not kthNode:
# return head
return dummyHead.next
# Reverse the next k section of nodes:
nextHead = kthNode.next
# Linked List Iterators:
# sc: O(1)
prev = nextHead
curr = somePrevTail.next
# --------------------------------
# After walking k steps,
# saving next section head,
# initializing curr/prev:
#
# curr prev
# | |
# [dummyHead] -> [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> None
# | | |
# somePrevTail kthNode nextHead
# ------------------------------------------------
# Standard Iterative Linked List Reversal:
# Reverse k nodes:
for _ in range(k):
# Grab next node before disconnecting from current node
next = curr.next
# Reverse curr node, point to previous node
curr.next = prev
# Iterate linked list
prev = curr
curr = next
# After reversal:
#
# - - - - - - - - - - - - - -
# | prev | curr
# | | v |
# [dummyHead] [3] -> [2] -> [1] -> [4] -> [5] -> [6] -> None
# | ^
# groupPrevTail (groupPrevTail.next,
# (unchanged) original head, now tail
# of reversed group)
#
# - - - - - - - - - - - - - - - - -
# prev k section curr k section
#
#
# Connecting prev/curr sections:
# - groupPrevTail: tail of the *previous* k section (dummy here on iter 1)
# - groupPrevTail.next: original head / new tail of the group we just reversed
# - prev: original tail / new head of the group we just reversed
# - curr: head of the *next* k section (not yet reversed)
# To link the "prev" and curr" sections: we need to:
# - connect the new tail of the previous k section => to the new head of the curr k section
# Grab head of the *next* k section (not yet reversed),
# grab [4] in the above diagram
currTail = somePrevTail.next
# Grab dummyHead to point to new head,
# [3] in this case
somePrevTail.next = prev
# update last node of previous group, to last node of curr sub list
somePrevTail = currTail
# After adjusting links:
#
# prev curr
# | |
# [dummy] -> [3] -> [2] -> [1] -> [4] -> [5] -> [6] -> None
# | ^
# groupPrevTail (groupPrevTail.next,
# (unchanged) original head, now tail
# of reversed group)
#
# - - - - - - - - - - - - - - - - -
# prev k section curr k section
# overall: tc O(n)
# overall: sc O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
109. Convert Sorted List to Binary Search Tree ::2:: - Medium
Topics: Linked List, Divide and Conquer, Tree, Binary Search Tree, Binary Tree
Intro
Given the head of a singly linked list where elements are sorted in ascending order, convert it to a height-balanced binary search tree. A height-balanced binary tree is a binary tree in which the depth of the two subtrees of every node never differs by more than one.
| Example Input | Output |
|---|---|
| head = [-10,-3,0,5,9] | [0,-3,9,-10,null,5] |
| head = [] | [] |
Constraints:
The number of nodes in the list is in the range [0, 2 * 10^4].
-10^5 ≤ Node.val ≤ 10^5
Abstraction
Given a linked list, convert it to a height balanced binary search tree
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Linked List] [Tree] Recursive Top Down Divide and Conquer - Linked List/Simple Traversal
def sortedListToBST(self, head: Optional[ListNode]) -> Optional[TreeNode]:
# Note:
# 1. Find midpoint with slow/fast pointers -> root
# 2. Left half -> recurse -> left subtree
# 3. Right half -> recurse -> right subtree
# 4. Midpoint becomes root of current subtree
# sc: O(log n) recursion stack, not O(1)
# base cases
if not head:
return None
if not head.next:
return TreeNode(head.val)
# find mid with slow/fast, sever left half
prev = None
slow, fast = head, head
while fast and fast.next:
prev = slow
slow = slow.next
fast = fast.next.next
# sever left half from mid
if prev:
prev.next = None
# slow is mid -> root
root = TreeNode(slow.val)
root.left = self.sortedListToBST(head if prev else None)
root.right = self.sortedListToBST(slow.next)
return root
# tc: O(n log n) — find mid is O(n), done log n times
# sc: O(log n) — recursion stack| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Linked List] [Tree] Recursive Inorder Simulation [SC Opt] - Linked List/Simple Traversal
def sortedListToBST(self, head: Optional[ListNode]) -> Optional[TreeNode]:
# Note:
# 1. Count list length
# 2. Simulate inorder traversal — left, root, right
# 3. self.curr advances through list naturally as inorder visits nodes
# 4. No need to find midpoint or sever list -> O(n) time
# Key insight: inorder traversal of a BST visits nodes in sorted order,
# so we can build the BST by advancing the list pointer
# in lockstep with the inorder traversal
# count length
length = 0
node = head
while node:
length += 1
node = node.next
self.curr = head
def convert(left, right):
if left > right:
return None
mid = (left + right) // 2
# build left subtree first (inorder: left -> root -> right)
leftChild = convert(left, mid - 1)
# self.curr is now at the midpoint -> root
root = TreeNode(self.curr.val)
self.curr = self.curr.next
root.left = leftChild
# build right subtree
root.right = convert(mid + 1, right)
return root
return convert(0, length - 1)
# tc: O(n) — each node visited exactly once
# sc: O(log n) — recursion stack only, no list severing| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
114. Flatten Binary Tree to Linked List ::3:: - Medium
Topics: Linked List, Stack, Tree, Depth First Search, Binary Tree
Intro
Given the root of a binary tree, flatten the tree into a "linked list": The "linked list" should use the same TreeNode class where the right child pointer points to the next node in the list and the left child pointer is always null. The "linked list" should be in the same order as a pre-order traversal of the binary tree. Follow up: Can you flatten the tree in-place (with O(1) extra space)?
| Example Input | Output |
|---|---|
| root = [1,2,5,3,4,null,6] | [1,null,2,null,3,null,4,null,5,null,6] |
| root = [] | [] |
| root = [0] | [0] |
Constraints:
The number of nodes in the tree is in the range [0, 2000].
-100 ≤ Node.val ≤ 100
Abstraction
Given a binary tree, return a linked list with the same order as the pre order traversal of the binary tree
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Tree] Recursive Pre Order and Relink - Linked List/Simple Traversal
def flatten(self, root: Optional[TreeNode]) -> None:
# Note:
# 1. Collect all nodes in preorder (root, left, right)
# 2. Relink all nodes: left=None, right=next node in list
# Straightforward but O(n) space for the list
nodes = []
def preorder(node):
if not node:
return
nodes.append(node)
preorder(node.left)
preorder(node.right)
preorder(root)
for i in range(len(nodes) - 1):
nodes[i].left = None
nodes[i].right = nodes[i + 1]
if nodes:
nodes[-1].left = None
nodes[-1].right = None
# tc: O(n)
# sc: O(n) — preorder list + recursion stack| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Tree] Iterative Pre Order Stack - Linked List/Simple Traversal
def flatten(self, root: Optional[TreeNode]) -> None:
# Note:
# 1. Simulate preorder iteratively with a stack
# 2. For each node, relink right to stack top (next preorder node)
# 3. No need to collect all nodes first — relink as we go
# Still O(n) space for stack, but no separate list needed
if not root:
return
stack = [root]
while stack:
node = stack.pop()
# push right first so left is processed first
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
# relink right to next preorder node (top of stack)
if stack:
node.right = stack[-1]
node.left = None
# tc: O(n)
# sc: O(n) — stack| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 3: [Tree] Morris Traversal In Place [SC Opt] - Linked List/Simple Traversal
def flatten(self, root: Optional[TreeNode]) -> None:
# Note:
# 1. For each node with a left child:
# find the rightmost node of the left subtree (inorder predecessor)
# attach current right subtree to that rightmost node
# move left subtree to right, clear left
# 2. Repeat until no left children remain
# Key insight: we are threading each node's right subtree
# onto the bottom-right of its left subtree,
# then pulling the left subtree up to the right
# O(1) space, no recursion, no stack
curr = root
while curr:
if curr.left:
# find rightmost node of left subtree
rightmost = curr.left
while rightmost.right:
rightmost = rightmost.right
# thread curr's right subtree onto rightmost
rightmost.right = curr.right
# pull left subtree up to right
curr.right = curr.left
curr.left = None
curr = curr.right
# tc: O(n)
# sc: O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
116. Populating Next Right Pointers in Each Node ::3:: - Medium
Topics: Linked List, Tree, Depth First Search, Breadth First Search, Binary Tree
Intro
You are given a perfect binary tree where all leaves are on the same level, and every parent has two children. The binary tree has the following definition: Populate each next pointer to point to its next right node. If there is no next right node, the next pointer should be set to NULL. Initially, all next pointers are set to NULL. Follow-up: You may only use constant extra space. The recursive approach is fine. You may assume implicit stack space does not count as extra space for this problem.
| Example Input | Output |
|---|---|
| root = [1,2,3,4,5,6,7] | [1,#,2,3,#,4,5,6,7,#] |
| root = [] | [] |
Constraints:
The number of nodes in the list is in the range [0, 2^12 - 1].
-1000 ≤ Node.val ≤ 1000
Abstraction
Given a linked list, swap every pair of elements.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Tree] BFS Level Order - Linked List/Simple Traversal
def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
# Note:
# 1. BFS level by level
# 2. For each node in level, point next to the next node in queue
# 3. Last node in each level points to None
# Straightforward but O(n) space for queue
if not root:
return root
queue = deque([root])
while queue:
size = len(queue)
for i in range(size):
node = queue.popleft()
# point to next node in same level
if i < size - 1:
node.next = queue[0]
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return root
# tc: O(n)
# sc: O(n) — queue holds up to n/2 nodes (last level)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Tree] Recursive DFS - Linked List/Simple Traversal
def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
# Note:
# 1. For each node, connect its two children (easy, same parent)
# 2. Also connect right child to left child of node.next (cross connection)
# 3. Recurse left then right
# Key insight: by the time we process a node,
# node.next is already populated by parent
if not root or not root.left:
return root
# connect children of same parent
root.left.next = root.right
# connect across parents using already-set root.next
if root.next:
root.right.next = root.next.left
self.connect(root.left)
self.connect(root.right)
return root
# tc: O(n)
# sc: O(log n) — recursion stack, perfect binary tree height| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 3: [Tree] Iterative Level Traversal [SC Opt] - Linked List/Simple Traversal
def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
# Note:
# 1. Use already-populated next pointers to traverse each level
# 2. leftmost tracks the first node of each level (always left child)
# 3. curr traverses current level via next pointers
# 4. For each curr, connect its children using same/cross logic
# Key insight: once a level's next pointers are set,
# we can traverse it like a linked list
# to set the next level's pointers
if not root:
return root
leftmost = root
while leftmost.left:
curr = leftmost
while curr:
# same parent connection
curr.left.next = curr.right
# cross parent connection
if curr.next:
curr.right.next = curr.next.left
curr = curr.next
# drop down to next level
leftmost = leftmost.left
return root
# tc: O(n)
# sc: O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
117. Populating Next Right Pointers in Each Node II ::3:: - Medium
Topics: Linked List, Tree, Depth First Search, Breadth First Search, Binary Tree
Intro
Given a binary tree Populate each next pointer to point to its next right node. If there is no next right node, the next pointer should be set to NULL. Initially, all next pointers are set to NULL. Follow-up: You may only use constant extra space. The recursive approach is fine. You may assume implicit stack space does not count as extra space for this problem.
| Example Input | Output |
|---|---|
| root = [1,2,3,4,5,null,7] | [1,#,2,3,#,4,5,7,#] |
| root = [] | [] |
Constraints:
The number of nodes in the tree is in the range [0, 6000].
-100 ≤ Node.val ≤ 100
Abstraction
Same as 116 but binary tree is not guaranteed to be perfect
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [Tree] BFS Level Order - Linked List/Simple Traversal
def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
# Note:
# 1. Identical to problem 116 Solution 1
# 2. BFS naturally handles missing children since we
# only enqueue children that exist
# 3. Works for both perfect and imperfect trees
if not root:
return root
queue = deque([root])
while queue:
size = len(queue)
for i in range(size):
node = queue.popleft()
if i < size - 1:
node.next = queue[0]
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return root
# tc: O(n)
# sc: O(n)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [Tree] Iterative Next Level Linked List - Linked List/Simple Traversal
def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
# Note:
# 1. Can't use 116 Sol 3's leftmost.left trick — children may be missing
# 2. Instead, use a dummy node to build next level's linked list
# 3. dummy.next always points to first child found in next level
# 4. tail tracks where to attach next child found
# Key insight: treat the next level as a linked list being built
# as we traverse the current level via next pointers
if not root:
return root
curr = root
while curr:
# dummy head for next level's linked list
dummy = ListNode(0)
tail = dummy
# traverse current level via next pointers
while curr:
# try to attach left child to next level list
if curr.left:
tail.next = curr.left
tail = tail.next
# try to attach right child to next level list
if curr.right:
tail.next = curr.right
tail = tail.next
curr = curr.next
# drop down: next level's list starts at dummy.next
curr = dummy.next
return root
# tc: O(n)
# sc: O(1) — dummy/tail are reused each level| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 3: [Tree] Iterative Next Level Linked List V2 [SC Opt] - Linked List/Simple Traversal
def connect(self, root: 'Node') -> 'Node':
# Note:
# 1. Same idea as Solution 2 but without dummy node
# 2. leftmost tracks first node of next level
# 3. pre trails behind, links children together as we find them
# 4. if pre exists, pre.next = current child
# if leftmost not yet set, current child is the new leftmost
leftmost = root
while leftmost:
cur = leftmost
leftmost = None # reset for next level
pre = None # reset chain for next level
while cur:
# try left child
if cur.left:
if not leftmost:
leftmost = cur.left # first node of next level
if pre:
pre.next = cur.left # chain to previous child
pre = cur.left
# try right child
if cur.right:
if not leftmost:
leftmost = cur.right # first node of next level
if pre:
pre.next = cur.right # chain to previous child
pre = cur.right
cur = cur.next
return root
# tc: O(n)
# sc: O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|