LeetCode: Linked List

Table Of Content
206. Reverse Linked List ::4:: - Easy
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: Recursive - Linked List/Simple Traversal
- Solution 2: Recursive Clean (Learning Not) - Linked List/Simple Traversal
- Solution 3: Iterative - Linked List/Simple Traversal
- Solution 4: Iterative Clean (Learning Not) - Linked List/Simple Traversal
143. Reorder List ::4:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: Slow.next Explicit Set Tail of Final List Two Way Disconnect Iteration Reversal - Linked List/Simple Traversal
- Solution 2: Shared Slow Between Left and Right List Implicit Disconnect - Linked List/Simple Traversal
- Solution 3: Stack - Linked List/Simple Traversal
- Solution 4: Brain Teaser slow longer or equal Right list Stack - Linked List/Simple Traversal
138. Copy List with Random Pointer ::3:: - Medium
- Intro
- Abstraction
- Space & Time Complexity
- Brute Force:
- Find the Bug:
- Solution 1: Two Pass Hashmap Create Nodes and Set Next and Random Deep References - Linked List/Simple Traversal
- Solution 2: Memoization Lazy Construction One Pass Hashmap - Linked List/Simple Traversal
- Solution 3: Three Pass In Place Interleaving Technique - 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: Head to Start of Cycle Proof Floyd Tortoise and Hare - 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 with Hashmap - Linked List/Simple Traversal
- Solution 2: Python Implementation OrderedDict a Doubly Linked List with Hashmap - 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 ← F
Overall: 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 count
Linked 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: 6
Linked 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 prev
206. 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/Simple Traversal
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Note:
# With recursive Linked Lists:
# A good way to think about the problem, is that you have access
# to the list in reverse order, so what actions do you need to
# take in order to reverse the list, while traversing the list
# from back to front
# Base case:
# If current list is empty or has only one node -> this node becomes the new head
if head == None or head.next == None:
return head
# Recursion: reverse the rest of the list (head.next to end)
# After Recursion: new_head is new head of reverse list (tail of original list)
new_head = self.reverseList(head.next)
# Recursion Part 1: Reverse Flow of next element (by adding head <- head.next)
# After recursion: Point second Element to Original Head
head.next.next = head
# Recursion Part 2: Reverse Flow of curr element (by adding None <- head), as current head may be first element in original list
# After recursion: point first element in original list to None
head.next = None
# Continue passing back new head (last element from original list), from the deepest recursive call
# overall: time complexity O(n)
# overall: space complexity O(n)
return new_head
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Solution 2: Recursive Clean (Learning Not) - Linked List/Simple Traversal
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head or not head.next:
return head
new_head = self.reverseList(head.next)
head.next.next = head
head.next = None
# overall: time complexity O(n)
# overall: space complexity O(n)
return new_head
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Solution 3: Iterative - Linked List/Simple Traversal
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Setting prev pointing to None (end of reversed list)
prev = None
# Starting at head
curr = head
# while _ is not None
# time complexity: iterate over list of n length O(n)
while curr != None:
# Grab next node, before disconnecting
next_node = curr.next
# Disconnect and Reverse Flow:
# <- prev curr -> next
# <- prev <- curr next
curr.next = prev
# Advance both prev and curr
prev = curr
curr = next_node
# Loop exits:
# val: (val) (None)
# node: prev -> curr
# overall: time complexity O(n)
# overall: space complexity O(1)
return prev
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Solution 4: Iterative Clean (Learning Not) - Linked List/Simple Traversal
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
prev = None
curr = head
while curr:
next_node = curr.next
curr.next = prev
prev = curr
curr = next_node
# overall: time complexity O(n)
# overall: space complexity 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/Simple Traversal
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
# Note:
# Building merged list from right to left,
# connecting current node and passing back as we go
# Base case:
# One of the lists is empty,
# return second list for the .next
if not list1:
return list2
if not list2:
return list1
# Recursive Call:
# Point smaller val -> rest of list
if list1.val < list2.val:
list1.next = self.mergeTwoLists(list1.next, list2)
return list1
else:
list2.next = self.mergeTwoLists(list1, list2.next)
return list2
# overall: time complexity O(n)
# overall: space complexity O(n)
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Solution 2: Iterative Sentinel Anchor - Linked List/Simple Traversal
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
# Note:
# DummyHead is used to enter the iterative loop cleanly
# without handling first node assigning case
# sentinel anchor for real head
dummyHead = ListNode(-1)
# curr node of list
tail = dummy
# Traverse both lists while both have nodes
# time complexity: iterate over both lists O(m + n)
while list1 and list2:
# grab smaller value between list1 and list2
if list1.val < list2.val:
# append and iterate list1
tail.next = list1
list1 = list1.next
else:
# append and iterate list2
tail.next = list2
list2 = list2.next
# 1st iteration: dummyHead/tail no longer point to same obj
# nth iteration: iterate tail to whatever the next_node is
tail = tail.next
# At least one list is non empty at this point:
# point tail to rest of list
if list1:
tail.next = list1
else
tail.next = list2
# Jump to merged head using dummyHead
# overall: time complexity O(m + n)
# overall: space complexity O(1)
return dummyHead.next
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, 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: Track Visited Notes - Linked List/Simple Traversal
def hasCycle(self, head: Optional[ListNode]) -> bool:
# Note:
# Track visited nodes to check for repeats (cycles)
# Nodes are stored by obj reference,
# perfect for cycle detection
visited = set()
# start of iteration
curr = head
# time complexity: iterate over list of n length O(n)
while curr:
# repeated node: (cycle detected)
if curr in visited:
return True
# add and continue iteration
visited.add(curr)
curr = curr.next
# no repeat nodes (no cycles)
# overall: time complexity O(n)
# overall: space complexity O(n)
return False
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Solution 2: Floyd Cycle Detection Tortoise and Hare - Linked List/Simple Traversal
def hasCycle(self, head: Optional[ListNode]) -> bool:
# Note:
# If no cycle: fast pointer will hit None (end of list)
# If cycle: once both fast and slow pointer enter cycle of size k,
# fast pointer will lap and hit slow pointer within k steps
# set pointers
slow, fast = head, head
# time complexity: iterate over list
while fast and fast.next:
# move both slow and fast pointers
slow = slow.next
fast = fast.next.next
# check if match (cycle detected)
if slow == fast:
return True
# reached end of list (no cycle detected)
# overall: time complexity O(n)
# overall: space complexity O(1)
return False
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: Slow.next Explicit Set Tail of Final List Two Way Disconnect Iteration Reversal - Linked List/Simple Traversal
def reorderList(self, head: Optional[ListNode]) -> None:
# Note:
# 1. Use Tortoise Hare to find the middle
# 2. Reverse second half of list in place
# 3. Merges first half and reverse second half alternating
# Result: Fully in place reordering
slow = fast = head
while fast and fast.next:
# slow travels half distance vs fast pointer
slow = slow.next
fast = fast.next.next
# Slow result:
# Odd List: slow -> odd mid
# Even List: slow -> mid2
# prev for reverse tail
prev = None
# 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
# Reverse second half of list
while curr:
# grab next
next_node = curr.next
# reverse flow
curr.next = prev
# iterate
prev = curr
curr = next_node
# iterate over left half and right half
left, right = head, prev
# Explicit Termination:
# right half 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
while right:
# grab next before disconnect
next_left, next_right = left.next, right.next
# alternate flow
left.next = right
right.next = next_left
# iterate left and right half -> connect next_nodes
left, right = next_left, next_right
# overall: time complexity O(n)
# overall: space complexity O(1)
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Solution 2: Shared Slow Between Left and Right List Implicit Disconnect - Linked List/Simple Traversal
def reorderList(self, head: Optional[ListNode]) -> None:
# Note
# 1. Use Tortoise and Hare to find midpoint.
# 2. Reverse the second half in place (without disconnecting manually)
# 3. Merge first and reversed second half alternating nodes
# Result: Fully in place reordering
slow = fast = head
while fast and fast.next:
# slow travels half distance vs fast pointer: iterate references in memory
fast = fast.next.next
slow = slow.next
# Slow result:
# Odd List: slow -> odd mid
# Even List: slow -> mid2
# prev for reverse tail
prev = None
# 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
# Implicit Disconnect:
# Both left and right contain slow (either odd mid or mid2) in their list
# during the first modification of slow, we point it to prev (None)
# now, both lists have a slow.next -> None
# Reverse second half of list:
while curr:
# grab next before disconnect
next_node = curr.next
# reverse flow
curr.next = prev
# iterate reference in memory
prev = curr
curr = next_node
# iterate over left and right lists
left, right = head, prev
# Implicit Termination:
# Now since:
# left equal or longer by 1,
# slow -> None is contained by both lists,
# we will reach right's copy of slow
# at the same time or earlier by 1 iteration.
# However, when we reach right's copy,
# right.next = next_left guarantees that
# the rest of the left list was connected,
# and since we already terminated the left list 'Implicit Disconnect'
# our merged list will terminate
while right != slow:
next_left, next_right = left.next, right.next
left.next = right
right.next = next_left
left, right = next_left, next_right
# brain teaser:
# why does this work:
right.next = None
# but right.next.next = None
# does not work
# overall: time complexity O(n)
# overall: space complexity O(1)
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Solution 3: 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: 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 |
---|---|---|---|---|
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: Two Pass Get Length and Stop Prev to Removal Index - 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 length of list
listLen = 0
curr = head
while curr:
listLen += 1
curr = curr.next
# Second pass: find the node behind target index
remove = listLen - n
# we adding an element to the list
dummy = ListNode(0, head)
curr = dummy
# this stops at the Node before the target remove index
# thus we can disconnect it
index = 0
while index != remove:
index += 1
curr = curr.next
# remove nth node from end by skipping it
curr.next = curr.next.next
# Return head dummy is pointing to (may be different from original)
# overall: time complexity O(n)
# overall: space complexity O(1)
return dummy.next
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Solution 2: One Pass Fast Slow with HeadStart of N+1 - Linked List/Simple Traversal
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
# Note:
# 1. Use two pointers spaced n+1 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
dummy = ListNode(-1, head)
slow = fast = dummy
# Set fast pointer:
# head start of n + 1 step,
# in order to create a gap of n between fast and slow
# (taking dummy node into account)
index = 0
while index != n+1:
index += 1
fast = fast.next
# When fast reaches end:
# n + 1 gap between slow (within list) and fast (end of list)
while fast:
fast = fast.next
slow = slow.next
# skip nth node
slow.next = slow.next.next
# Return head dummy is pointing to (may be different from original)
# overall: time complexity O(n)
# overall: space complexity O(1)
return dummy.next
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: Two Pass Hashmap Create Nodes and Set Next and Random Deep References - Linked List/Simple Traversal
def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
# Note:
# .get() returns 'None' if nothing exists,
# so perfect for our use case
# 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 node copy
orig_to_deepCopy = {}
# Iteration 1:
# Copy all nodes values (no next/random pointers yet)
curr = head
while curr:
# create
copy = Node(curr.val)
# insert
orig_to_deepCopy[curr] = copy
# iterate
curr = curr.next
# Iteration 2:
# All nodes created, now assign next and random pointers
curr = head
while curr:
# grab curr deepCopy
copy = orig_to_deepCopy[curr]
# grab and set next and random deepCopy (value could be None, .get() returns None by default if no answer exists)
copy.next = orig_to_deepCopy.get(curr.next)
copy.random = orig_to_deepCopy.get(curr.random)
# iterate
curr = curr.next
# Return deep copy of the head node
# overall: time complexity O(n)
# overall: space complexity O(n)
return orig_to_deepCopy[head]
Solution 2: Memoization Lazy Construction One Pass Hashmap - 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
# dummy node head trick
dummy = 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 = {}
# time complexity: iterate over list of n length
while curr:
# grab or create and grab curr deepCopy
if curr not in memo:
memo[curr] = Node(curr.val)
currDeepCopy = memo[curr]
# validate curr.random
if curr.random:
# grab or create and grab curr.random deepCopy
if curr.random not in memo:
memo[curr.random] = Node(curr.random.val)
currDeepCopy.random = memo[curr.random]
# First iteration: dummy points to deepCopy head
# next iteration: previous deepCopy node points to current deepCopy node
# after termination: entire deepCopy list is now connected
prev.next = currDeepCopy
# iterate to current deepCopy node connect next node
# iterate over original list
prev = prev.next
curr = curr.next
# Return deepCopy head
# overall: time complexity O(n)
# overall: space complexity O(n)
return memo[head]
# or
# return dummy.next
Solution 3: Three Pass In Place Interleaving Technique - Linked List/Simple Traversal
def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
# Note:
# 1. First interleave copy and paste nodes between original nodes O(1) space
# A -> B -> C ===> A -> A' -> B -> B' -> C -> C'
# 2. Assign random pointers to the copy nodes using original's .random
# 3. Detach copy list from original (restoring original list)
# and setting valid .next for deepCopy list
# A -> A' -> B -> B' -> C -> C' ===> A' -> B' -> C'
# 4. Return deepCopy head
# Result: constant space within original list, but requires 3 passes
# Empty Check
if not head:
return None
# Interleave deepCopy nodes
# A -> A' -> B -> B' -> C -> C'
curr = head
while curr:
# create and interweave
deepCopy = Node(curr.val)
deepCopy.next = curr.next
# link A -> A' and iterate to next orig
curr.next = deepCopy
curr = deepCopy.next
# Assign random pointers
curr = head
while curr:
# grab intertwined deepCopy
deepCopy = curr.next
# assign random from orig if non None
if curr.random:
deepCopy.random = curr.random.next
# iterate to next orig
curr = deepCopy.next
# Split orig and deepCopy list
# (A) orig head: A -> A' -> B -> ...
curr = head
# (A') deepCopy head: A -> A' -> B -> ...
deepCopyHead = head.next
# time complexity: iterate over list of 2n length O(n)
while curr:
# grab deepCopy
deepCopy = curr.next
# link orig to next orig node, iterate to next orig
# A -> A' -> B -> ...
# A -> B
curr.next = deepCopy.next
curr = curr.next
# if new orig is non None
# (B' -> C -> ...) vs (B' -> None)
# there will exists a new deepCopy
# iterate to deepCopy
if curr:
deepCopy.next = curr.next
# return deepCopy head
# overall: time complexity O(n)
# overall: space complexity O(1)
return deepCopyHead
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:
# Both lists and carry are empty
if not l1 and not l2 and carry == 0:
return None
# grab values if exist
val1 = l1.val if l1 else 0
val2 = l2.val if l2 else 0
# 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
# new node and attach
newNode = ListNode(sum % 10)
newNode.next = recursiveSum(l1, l2, carry)
# return: curr node -> rest of list
return newNode
# calculate new sum list over two lists
# overall: time complexity O(max(m, n))
# overall: space complexity 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
# dummy trick to return curr head
dummy = ListNode(-1)
prev = dummy
carry = 0
# Traverse while either list or carry is non empty
while l1 or l2 or carry:
# grab if value exists
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: time complexity O(max(m, n))
# overall: space complexity O(1)
return dummy.next
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 slow
Solution 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: Head to Start of Cycle Proof Floyd Tortoise and Hare - Linked List/Simple Traversal
def findDuplicate(self, nums: List[int]) -> int:
# Note:
# 1. 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
# When slow and fast pointers first meet inside the cycle:
# Slow pointer has traveled: L + x steps:
# L steps to reach the start of the cycle
# x steps inside the cycle to the meeting pointer
# Fast pointer has traveled L + x + (k * C) steps:
# 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)
# Fast Substitute:
# By definition, fast pointer speed is double slow pointer, so substitute
# L + x + (k * C) = 2(L + x)
# Solve for L:
# L + x + kC = 2L + 2x
# kX = L + x
# L = kC - x
# Rewrite Rule Context:
# Traveling x steps inside cycle of length C =
# to traveling C - x steps backwards, due to wrap around
# x steps forward = C - x backwards
# 3. Rearrange
# 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
# The above math proves the below algorithm:
# Note:
# Idea: Treat array as linked list: index -> nums[index]
# Problem set up with duplicate guarantees cycle in linked list representation
# 1. Detect cycle (slow = nums[slow], fast = nums[nums[fast]])
# 2. Move slow to head, advance both one step at a time to find cycle start (duplicate)
# Find meeting point in cycle
slow = nums[0]
fast = nums[0]
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
# Move slow to head, move fast and slow one step at time,
# fast and slow will meet at start of cycle
slow = nums[0]
while True:
if slow == fast:
break
slow = nums[slow]
fast = nums[fast]
# overall: time complexity O(n)
# overall: space complexity 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) |
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 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: 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: Min Heap 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 of: (val, list index, node)
min_heap = []
# Push the head of each non-empty list into the heap
for i_list, head_list in enumerate(lists):
if head_list:
# minHeap sorted by node.val,
# i_list index used to break ties
heappush(min_heap, (head_list.val, i_list, head_list))
dummy = ListNode(-1)
prev = dummy
# Pop smallest node, attach to merged list, push from same list if min.next
while min_heap:
# grab min value
minVal, i_list, node = heappop(min_heap)
prev.next = node
prev = prev.next
# valid node.next for current i_list
if node.next:
# grab next node from i_list
heappush(min_heap, (node.next.val, i_list, node.next))
# minHeap empty -> all lists empty
# return head of merged list
# overall: time complexity O(n log k)
# overall: space complexity O(k)
return dummy.next
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Solution 2: Merge Pair of List at a Time with Standard Merge Two Sorted Linked List - 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 |
---|---|---|---|---|
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: Recursive - Linked List/Simple Traversal
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# 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
# Base case: check if at least k nodes remain
kLen = 0
node = head
while node and kLen < k:
kLen += 1
node = node.next
# Base case: less than k nodes, return head
if kLen < k:
return head
# ----
# standard reverse linked list
# Reverse current sub list up to k
prev = None
curr = head
for _ in range(k):
# grab next
next = curr.next
# reverse flow
curr.next = prev
# iterate
prev = curr
curr = next
# curr -> start of next sub list
# prev -> new head of curr sub list
# head -> new tail of curr sub list
# connect head (new tail) via head.next to next reversed sub list
head.next = self.reverseKGroup(curr, k)
# return prev (new head) of curr sub list
# overall: time complexity O(n)
# overall: space complexity O(n/k) ~ O(n) recursive stack
return prev
Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
---|---|---|---|---|
Solution 2: Iterative - Linked List/Simple Traversal
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# 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
# dummy node trick for head of overall list
dummy = ListNode(0, head)
# group_prev_tail points to tail of the previous reverse group,
# where group.next points to head of next list
group_prev_tail = dummy
# while remaining sub list is greater than k
while True:
# last item in previous reversed sub list
kth_elem = group_prev_tail
# check if k more elements exist
for _ in range(k):
kth_elem = kth_elem.next
# less than k nodes remain, return head of entire list
if not kth_elem:
return dummy.next
# next sub list start
group_next = kth_elem.next
# ----
# standard reverse linked list
# Reverse group [group_prev_tail.next, kth]
prev = group_next
curr = group_prev_tail.next
# Reverse group in place for nodes [0, k):
for _ in range(k):
# grab next
next = curr.next
# reverse flow
curr.next = prev
# iterate
prev = curr
curr = next
# group_prev_tail -> (new tail) of prev sub list
# curr -> head of next sub list
# prev -> (new head) of curr sub list
# group_prev_tail.next -> (new tail) of curr sub list
# ----
# connect new tail of previous sub list
# to new head of curr sub list
# last node of previous group, still pointing to original head,
# which is now the tail
curr_tail = group_prev_tail.next
# point last node of previous group, to new head of curr sub list
group_prev_tail.next = prev
# update last node of previous group, to last node of curr sub list
group_prev_tail = curr_tail
# overall: time complexity O(n)
# overall: space complexity O(1)