LeetCode: Two Pointers II Linked List

143. Reorder List ::4:: - Medium
- Intro
- Abstraction
- Pseudocode
- Solution 1: [Stack] 3 Step FindMiddle() PushToStackToReverse() PopFromStackToMergeAlternating() Left is One Or Two Nodes Longer Stack - Linked List/Simple Traversal
- Solution 2: [Stack] 3 Step FindMiddle() PushToStackToReverse() PopFromStackToMergeAlternating() Slow Is Shared So Left Is Equal Or One Node Longer Stack - Linked List/Simple Traversal
- Solution 3: [Linked List] 3 Step FindMiddle() ReverseRight() MergeAlternating() Slow Node Owned By Left List With Left List Being 1 or 2 Nodes Longer [SC Opt] - Linked List/Simple Traversal
- Solution 4: [Linked List] 3 Step FindMiddle() ReverseRight() MergeAlternating() Shared Slow Node With Left List Being Even or 1 Node Longer [SC Opt] - Linked List/Simple Traversal
287. Find the Duplicate Number ::3:: - Medium
- Intro
- Abstraction
- Pseudocode
- Solution 1: [Bit Masking] Create Bit Mask For Longest Number Then Bit Mask Each Slot Looking For The Duplicate Flip - Linked List/Simple Traversal
- Solution 2: [Binary Search] Pigeonhole Search Side With An Extra Number - Linked List/Simple Traversal
- Solution 3: [Linked List] 3 Step Modified Floyd Cycle With FindMeeting() ResetSlow() FindCycleStart() [SC Opt] - Linked List/Simple Traversal
138. Copy List with Random Pointer ::3:: - Medium
- Intro
- Abstraction
- Pseudocode
- 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
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.
Pseudocode
Sol 2: Iterative Reversal Using Prev and Curr:
1. (prev = None)
2. (curr = head)
3. while curr != None:
a. nextNode = curr.next
b. curr.next = prev
c. prev = curr
d. curr = nextNode
4. newHead = prev
5. return newHeadSolution 1: [Linked List] Recursive Reversal Passing Back New Head - Linked List/Simple Traversal
def reverseList(self, currNode: Optional[ListNode]) -> Optional[ListNode]:
# Recursive Reversal:
# - Recurse down to the original tail, which becomes the new head
# - As the call stack unwinds, each node's next pointer is flipped to point backward
# - The original head ends up with next = None, becoming the new tail
# Reversal Phase:
# Input: (1) -> (3) -> (7) -> None
# Recurse down: (1) -> (3) -> (7) -> None
# to original tail ^
# base case: newHead = (7)
# Unwind: (1) -> (3) <- (7)
# (flip 7,3) newHead still (7)
# Unwind: (1) <- (3) <- (7)
# (flip 3,1) ^ currNode.next = None (new tail)
# Final: None <- (1) <- (3) <- (7)
# Note:
# 1. Base case: currNode is None or currNode.next is None, already at new head
# 2. Recurse first, then reverse the link on the way back up
# 3. currNode.next.next = currNode flips the pointer direction
# 4. currNode.next = None severs the old forward link
# Empty List:
# Nothing to reverse
if currNode == None:
return currNode
# Base Case:
# Reached the original tail,
# becomes the new head by being passed back
if currNode.next == None:
return currNode
# Recurse to the end of the list before reversing any links
# to hit the base case above
# tc: O(n)
# sc: O(n) — recursion stack depth
newHead = self.reverseList(currNode.next)
# Unwinding:
# Reverse the link between curr and next node
currNode.next.next = currNode
# Sever the old forward link, curr temporarily acts as a tail
currNode.next = None
# overall: tc O(n)
# overall: sc O(n)
return newHeadSolution 2: [Linked List] Iterative Reversal Using Prev and Curr - Linked List/Simple Traversal
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Iterative Reversal:
# - prev trails behind curr, holding the already reversed portion of the list
# - Each node's next pointer is flipped to point at prev before advancing
# - Once curr runs off the end, prev is left at the new head
# Reversal Phase:
# Input: None (1) -> (3) -> (7) -> None
# ^ ^
# prev curr
# Step 1: None <- (1) (3) -> (7) -> None
# ^ ^
# prev curr
# Step 2: None <- (1) <- (3) (7) -> None
# ^ ^
# prev curr
# Step 3: None <- (1) <- (3) <- (7) None (loop exits)
# ^ ^
# prev curr
# Note:
# 1. prev starts at None, will end up as the new head
# 2. curr walks the original list, one node at a time
# 3. Save nextNode before overwriting curr.next, or the rest of the list is lost
# 4. Loop exits when curr == None, prev holds the new head
# Prev acts as the new tail, will end up as the new head
prev = None
# Curr acts as the node we are reversing
curr = head
# tc: O(n)
while curr != None:
# Grab next node before severing connection
nextNode = curr.next
# Flip connection, reversing node
curr.next = prev
# Advance to flip next node
prev = curr
curr = nextNode
# Curr is at None
# Prev is at the original tail which is now the new head
newHead = prev
# overall: tc O(n)
# overall: sc O(1)
return newHead24. 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.
Pseudocode
Sol 1: Pairwise Swap:
1. dummy = ListNode(0, head)
2. prev = dummy
3. while prev.next and prev.next.next:
a. first = prev.next
b. second = prev.next.next
c. first.next = second.next
d. second.next = first
e. prev.next = second
f. prev = first
4. return dummy.nextSolution 1: [Linked List] Iterative In Place Pair Swap - Linked List/Simple Traversal
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Pairwise Swap:
# - Adjacent nodes are swapped two at a time
# - Each swapped pair is stitched back to the node before it
# - A dummy node is used since the head itself may be swapped away
# Swapping Phase:
# Input: 1 -> 2 -> 3 -> 4 -> null
# dummy -> 1
# Step 1: dummy -> 2 -> 1 -> 3 -> 4 -> null
# prev
# Step 2: dummy -> 2 -> 1 -> 4 -> 3 -> null
# prev
# Idea:
# - Keep a dummy node before head so the first pair can be swapped uniformly
# - prev always points to the node just before the current pair
# - For each pair: grab first/second, rewire their next pointers, reconnect prev,
# then advance prev to the end of the swapped pair (old first node)
# 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
dummyHead = ListNode(0, head)
curr = curr
# Swap each adjacent pair, two nodes at a time
# tc: O(n)
while curr.next and curr.next.next:
# Grab the current pair to swap
pairOne = curr.next
pairTwo = curr.next.next
# curr -> first -> second -> third -> fourth
# Move first to skip second, and now point to third
first.next = second.next
# Reverse flow for second, point second to first
second.next = first
# Move curr to skip first, and now point to second
curr.next = second
# Advance curr to the node before the pair of third and fourth
curr = first
# Grab head of new list
newHead = dummyHead.next
# overall: tc O(n)
# overall: sc O(1)
return newHead25. 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.
Pseudocode
Sol 1: Recursive:
1. kLen = 0, node = head
2. while node and kLen < k:
a. kLen = kLen + 1
b. node = node.next
3. if kLen < k:
a. return head
4. prev = null, curr = head
5. for i in range(k):
a. next = curr.next
b. curr.next = prev
c. prev = curr
d. curr = next
6. head.next = reverseKGroup(curr, k)
7. sectionNewHead = prev
8. return sectionNewHead
Sol 2: Iterative:
1. dummyHead = ListNode(0, head)
2. somePrevTail = dummyHead
3. while true:
a. kthNode = somePrevTail
b. for i in range(k):
- kthNode = kthNode.next
- if not kthNode: return dummyHead.next
c. nextHead = kthNode.next
d. prev = nextHead
e. curr = somePrevTail.next
f. for i in range(k):
- next = curr.next
- curr.next = prev
- prev = curr
- curr = next
g. currTail = somePrevTail.next
h. somePrevTail.next = prev
i. somePrevTail = currTailSolution 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 sectionNewHeadSolution 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)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.
Pseudocode
Sol 2: Iterative Merging Compare Node At Each And DummyHead Trick:
1. dummyHead = ListNode(-1)
2. curr = dummyHead
3. while list1 and list2:
a. if list1.val < list2.val:
curr.next = list1
list1 = list1.next
b. else:
curr.next = list2
list2 = list2.next
c. curr = curr.next
4. if list1:
curr.next = list1
5. else:
curr.next = list2
6. newHead = dummyHead.next
7. return newHeadSolution 1: [Linked List] Recursive Merging Compare Node At Each Level - Linked List/Simple Traversal
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
# Recursive Merge:
# - At each level, the smaller of the two current heads is chosen
# - That node's next pointer is wired to the result of merging the rest of the list
# - Recursion bottoms out once one list is exhausted
# list1: (1) -> (3) -> (7) -> None
# list2: (2) -> (9) -> (10) -> None
# Merged: (1) -> (2) -> (3) -> (7) -> (9) -> (10) -> None
# Base case:
# One of the lists is empty,
# return remaining portion of the non empty list
if not list1:
return list2
if not list2:
return list1
# Compare heads of both lists, grab smaller one:
# tc: O(n + m)
# sc: O(n + m) — recursion stack depth
if list1.val < list2.val:
# Grab list1 node:
# recurse on its remainder against list2
list1.next = self.mergeTwoLists(list1.next, list2)
# return merged list1 node
return list1
else:
# Grab list2 node:
# recurse on its remainder against list1
list2.next = self.mergeTwoLists(list1, list2.next)
# return merged list2 node
return list2
# overall: tc O(n + m) — each node visited once across recursion
# overall: sc O(n + m) — recursion stack depthSolution 2: [Linked List] Iterative Merging Compare Node At Each And DummyHead Trick [SC Opt] - Linked List/Simple Traversal
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
# Iterative Merge:
# - Both lists are walked simultaneously, one pointer each
# - The smaller of the two current nodes is appended to the merged list each step
# - Once one list is exhausted, the remainder of the other is attached directly
# list1: (1) -> (3) -> (7) -> None
# list2: (2) -> (9) -> (10) -> None
# Merged: (1) -> (2) -> (3) -> (7) -> (9) -> (10) -> None
# DummyHead:
# - Allows entering the iterative loop cleanly without handling first node assigning case
# - Points to the new head
dummyHead = ListNode(-1)
# Iterator:
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:
curr.next = list1
list1 = list1.next
else:
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
# dummyHead is still pointing to original head of the merged list
newHead = dummyHead.next
# overall: tc O(n + m)
# overall: sc O(1)
return newHead2. 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.
Pseudocode
Sol 2: Iterative Digit by Digit Addition:
1. dummy = ListNode(-1)
2. prev = dummy
3. carry = 0
4. while l1 or l2 or carry:
a. v1 = l1.val if l1 else 0
b. v2 = l2.val if l2 else 0
c. sum = v1 + v2 + carry
d. carry = sum // 10
e. newNode = ListNode(sum % 10)
f. prev.next = newNode
g. prev = prev.next
h. l1 = l1.next if l1 else null
i. l2 = l2.next if l2 else null
5. return dummy.nextSolution 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)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)
prev = dummy
# Tracking remainder
remainder = 0
# Traverse while we still have something to add
# - either list is non empty
# - we have a remainder value
while l1 or l2 or remainder:
# Check if value exists and grab
v1 = l1.val if l1 else 0
v2 = l2.val if l2 else 0
# add values and remainder
sum = v1 + v2 + remainder
remainder = 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.next141. 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.
Pseudocode
Sol 2: Floyd Cycle Detection Tortoise and Hare:
1. slow, fast = head, head
2. while fast and fast.next:
a. slow = slow.next
b. fast = fast.next.next
c. if slow == fast:
return True
3. return FalseSolution 1: [Linked List] Track Seen Notes In Set - Linked List/Simple Traversal
def hasCycle(self, head: Optional[ListNode]) -> bool:
# Seen-Set Detection:
# - Every visited node is recorded in a set
# - If a node is ever visited a second time, a cycle exists
# - If traversal reaches None, no node repeats and there is no cycle
# Cycle Shape:
#
# (1) -> (3) -> (7) -> (9)
# ^ | (cycle)
# | v
# (11)<- (10)
# Idea:
# - Walk the list one node at a time
# - Before advancing, check if curr has already been seen
# - If it has, we've looped back into a previously visited node: cycle found
# - Otherwise add curr to the seen set and continue
# Note:
# 1. Nodes are stored by object reference, not by value
# 2. Reaching None means every node was visited exactly once, no cycle
# 3. Revisiting a node before reaching None means a cycle exists
# Nodes stored by obj reference:
# sc: O(n)
seen = set()
# List Iterator:
curr = head
# tc: O(n)
while curr:
# Check:
# If node exists in seen list, we have pass through a cycle
if curr in seen:
return True
# Add new node to seen list
seen.add(curr)
# Iterate
curr = curr.next
# Reached end if list:
# No node was seen twice, no cycle exists
# overall: tc O(n)
# overall: sc O(n)
return FalseSolution 2: [Linked List] Floyd Cycle Detection Tortoise and Hare [SC Opt] - Linked List/Simple Traversal
def hasCycle(self, head: Optional[ListNode]) -> bool:
# Floyd's Tortoise and Hare:
# - slow advances 1 step at a time, fast advances 2 steps at a time
# - If a cycle exists, fast eventually laps slow and they meet inside the cycle
# - If no cycle exists, fast reaches None before ever meeting slow
# Cycle Shape:
#
# (1) -> (3) -> (7) -> (9)
# ^ | (cycle)
# | v
# (11)<- (10)
# Idea:
# - Move slow one step and fast two steps each iteration
# - If fast ever equals slow, fast has lapped slow inside a cycle
# - If fast (or fast.next) hits None, the list ends and no cycle exists
# Note:
# 1. slow and fast both start at head
# 2. Loop condition fast and fast.next guards against null-pointer access
# 3. slow == fast mid-loop can only happen inside a cycle, never on a straight list
# Floyd Iterators:
slow = head
fast = head
# tc: O(n)
while fast and fast.next:
# Iterate slow 1x and fast 2x
slow = slow.next
fast = fast.next.next
# Check:
# Fast has caught with slow, fast passed through a cycle
if slow == fast:
return True
# Fast reach end of list:
# Fast never caught up with slow, no cycle exists in list
# overall: tc O(n)
# overall: sc O(1)
return False143. 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.
Pseudocode
Sol 3: 3 Step Floyd + Explicit Disconnect + In Place Reversal:
1. slow = fast = head
2. while fast and fast.next:
a. slow = slow.next
b. fast = fast.next.next
3. prev = null
4. curr = slow.next
5. slow.next = null
6. while curr:
a. next_node = curr.next
b. curr.next = prev
c. prev = curr
d. curr = next_node
7. left = head
8. right = prev
9. while right:
a. next_left = left.next
b. next_right = right.next
c. left.next = right
d. right.next = next_left
e. left = next_left
f. right = next_right
Sol 3: Floyd Cycle With Stack:
1. slow = fast = head
2. while fast and fast.next:
a. slow = slow.next
b. fast = fast.next.next
3. stack = []
4. curr = slow.next
5. slow.next = null
6. while curr:
a. stack.append(curr)
b. curr = curr.next
7. left = head
8. while stack:
a. right = stack.pop()
b. right.next = left.next
c. left.next = right
d. left = right.nextSolution 1: [Stack] 3 Step FindMiddle() PushToStackToReverse() PopFromStackToMergeAlternating() Left is One Or Two Nodes Longer 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)
returnSolution 2: [Stack] 3 Step FindMiddle() PushToStackToReverse() PopFromStackToMergeAlternating() Slow Is Shared So Left Is Equal Or One Node Longer Stack - Linked List/Simple Traversal
def reorderList(self, head: Optional[ListNode]) -> None:
# Strategy:
# 1. Use slow/fast pointers to find the middle
# 2. Push the right half onto a stack (preserves order, reverses on pop)
# 3. Pop each node and splice it in: left -> right -> left.next
# Find Middle (Slow/Fast Pointers):
# slow ends on mid for odd lists, mid2 for even lists
# tc: O(n)
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# Set Curr:
# curr = slow (NOT slow.next) -- includes the shared mid node
# Odd List: left and right both include mid, so lengths are equal
# Even List: left is 1 element longer (keeps mid1 and mid2), right keeps mid2 (shared)
curr = slow
# No Explicit Disconnect:
# slow is never severed from the left half here (handled after merge)
# (we exclude this -> slow.next = None)
# Store Right Half:
# sc: O(n/2) ~ O(n)
# tc: O(n/2) ~ O(n)
stack = []
while curr:
stack.append(curr)
curr = curr.next
# Merge Alternating:
# left = original head (start of left half)
# popping the stack yields right half nodes in reverse order
left = head
# Left Equal Or One Node Longer:
# left and right share the mid node, so left is equal length (odd)
# or one node longer (even) than right — no disconnect happens here,
# so the list isn't yet terminated once the stack empties
# tc: O(n)
while stack:
# Pop next right-half node (reverse order)
right = stack.pop()
# Splice right node in after left node
right.next = left.next
left.next = right
# Advance left pointer
left = right.next
# brain teaser:
# why does this work:
# Both right and left shared the 'slow' node,
# and in the last iteration, we set right.next = left.next
# which means right is pointing to the shared 'slow' node,
# which results in slow pointing to itself
# 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: tc O(n)
# overall: sc O(n)
returnSolution 3: [Linked List] 3 Step FindMiddle() ReverseRight() MergeAlternating() Slow Node Owned By Left List With Left List Being 1 or 2 Nodes Longer [SC Opt] - Linked List/Simple Traversal
def reorderList(self, head: Optional[ListNode]) -> None:
# Reorder Pattern:
# 1 -> 2 -> 3 -> 4 -> 5 becomes 1 -> 5 -> 2 -> 4 -> 3
#
# This is a zig-zag merge of:
# list1 = first half: 1 -> 2 -> 3
# list2 = reversed second half: 5 -> 4
# Strategy:
# 1. Find the middle and split the list into two halves
# 2. Reverse the second half
# 3. Merge the two halves by alternating nodes
# -----------------------------------------
# 1: Find Middle (Floyd Cycle Algorithm Slow/Fast Pointers)
# - slow travels half distance by the time fast makes it to the end
# - use the slow fast pointers to find the middle of the list
#
# - slow ends on mid for odd lists
#
# s f
# () -> () -> () -> () -> () -> None
# mid
#
# - slow mid2 for even lists
#
# s f
# () -> () -> () -> () -> None
# mid1 mid2
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# -----------------------------------------
# Prepare for 2. reverse list:
# - slow becomes tail of left list responsible for:
# a. creating head of right list
# b. sever left and right list
# a) Create head of right list -> curr
prev = None
curr = slow.next
# Odd lists:
# - slow ends on mid
# - left list will be 1 element longer (keeps mid)
# - s.next becomes head of right
#
# - - - - L - - - - - R - -
# s s.next f
# () -> () -> () -> () -> () -> None
# mid
# Even lists:
# - slow ends on mid2
# - left list will be 2 elements longer (keeps mid1 and mid2)
# - s.next becomes head of right
#
# - - - - L - - - - R -
# s.next
# s f
# () -> () -> () -> () -> None
# mid1 mid2
# b) Sever left and right list
slow.next = None
# Odd lists:
# - slow ends on mid for odd lists
#
# s s.next f
# () -> () -> () () -> () -> None
# mid
# |
# V
# None
# Even lists:
# - slow mid2 for even lists
#
# s.next
# s f
# () -> () -> () () -> None
# mid1 mid2
# |
# v
# None
# -----------------------------------------
# 2. Reverse Second Half Of List
# Iterate reversal until we reach end of right half
# tc: O(n/2) ~ O(n)
while curr:
# Grab next node before severing connection
nextNode = curr.next
# Flip connection, reversing node
curr.next = prev
# Advance to flip next node
prev = curr
curr = nextNode
# Curr is at None
# Prev is at the original tail which is now the new head
newRightHead = prev
# -----------------------------------------
# 3. Merge Left and Reversed Right List Alternating
# - left = start of left half
# - right = start of reversed right half
left = head
right = newRightHead
# Right Half Always Shorter:
# - left half has 1-2 extra nodes (the middle element(s)),
# - once right is exhausted, the rest of the list is already in correct order
# - the 1-2 extra nodes on the left list acts as a prepared tail
# - the 1-2 prepared tails allows us to know when right terminates,
# the rest of the remaining list is already in the correct order
# Prepared Tails:
# Odd lists:
# - slow ends on mid for odd lists
#
# - - - - 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
# Even lists:
# - slow mid2 for even lists
#
# - - - - 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
# tc: O(n/2) ~ O(n)
while right:
# Grab next before disconnect
nextLeft = left.next
nextRight = right.next
# Merge lists alternating
left.next = right
right.next = nextLeft
# Iterate both lists
left = nextLeft
right = nextRight
# overall: tc O(n)
# overall: sc O(1)
returnSolution 4: [Linked List] 3 Step FindMiddle() ReverseRight() MergeAlternating() Shared Slow Node With Left List Being Even or 1 Node Longer [SC Opt] - Linked List/Simple Traversal
def reorderList(self, head: Optional[ListNode]) -> None:
# Reorder Pattern:
# 1 -> 2 -> 3 -> 4 -> 5 becomes 1 -> 5 -> 2 -> 4 -> 3
#
# This is a zig-zag merge of:
# list1 = first half: 1 -> 2 -> 3
# list2 = reversed second half: 5 -> 4
# Strategy:
# 1. Find the middle and split the list into two halves
# 2. Reverse the second half
# 3. Merge the two halves by alternating nodes
# -----------------------------------------
# 1. First Half Of List
# Floyd Cycle Algorithm Variation:
# - slow travels half distance by the time fast makes it to the end
# - use the slow fast pointers to find the middle of the list
#
# - slow ends on mid for odd lists
#
# s f
# () -> () -> () -> () -> () -> None
# mid
#
# - slow mid2 for even lists
#
# s f
# () -> () -> () -> () -> None
# mid1 mid2
slow = head
fast = head
while fast and fast.next:
fast = fast.next.next
slow = slow.next
# -----------------------------------------
# Prepare for 2. reverse list:
# - slow becomes tail of left list responsible for:
# a. creating head of right list
# b. sever left and right list
# a) Create head of right list -> curr
prev = None
curr = slow
# Odd List:
# - slow ends on mid
# - left and right will be equal (share mid)
# - s stays tail of left
# - s becomes head of right
#
# - - - - L - - - - - R - -
# s f
# () -> () -> () -> () -> () -> None
# mid
# Even List:
# - slow ends on mid2
# - left will be 1 element longer (share mid)
# - s stays tail of left
# - s becomes head of right
#
# - - L - - - - R - -
# s f
# () -> () -> () -> () -> None
# mid1 mid2
# Implicit Disconnect:
# - Left and right share slow in their list (odd mid or even mid2)
# - during the right reversal, we set 'slow -> None'
# - Will act as a prepared tail for the eventually merged list
# Reversal First Iteration:
# - we set "slow -> None"
# - we set "slow.next -> slow":
#
# s f
# () -> () -> () <- () -> () -> None
# mid
# |
# V
# None
#
#
# s f
# () -> () -> () <- () -> None
# mid1 mid2
# |
# V
# None
# -----------------------------------------
# 2. Reverse Second Half Of List
# Iterate reversal until we reach end of right half
# tc: O(n/2) ~ O(n)
while curr:
# Grab next node before severing connection
nextNode = curr.next
# Flip connection, reversing node
curr.next = prev
# Advance to flip next node
prev = curr
curr = nextNode
# Curr is at None
# Prev is at the original tail which is now the new head
newRightHead = prev
# -----------------------------------------
# 3. Merge Left and Reversed Right List Alternating
# - left = start of left half
# - right = start of reversed right half
left = head
right = newRightHead
# Right Half Equal Or Shorter:
# - left half is equal or has 1 extra node (the mid element)
# - but left and right half always share slow
# - once right reaches slow, we've reached the shared portion of the list
# - the shared portion acts as a prepared tail and tells us when right terminates
# Prepared Tail (Shared Mid):
# Odd lists:
# - slow ends on mid for odd lists
# - - - - L - - - - - R - -
# s f
# (1) -> (2) -> (3) -> (4) -> (5) (1) -> (2) -> (3) -> None (1) -> (5) -> (2) -> (4) -> (3) -> None
# mid ==> (5) -> (4) -> (3) -> None ==>
# ^
# right reaches shared slow,
# notice how "(3) -> None" is already prepared
# Even lists:
# - slow mid2 for even lists
#
# - - L - - - - R - -
# s f
# (1) -> (2) -> (3) -> (4) (1) -> (2) -> (3) -> None (1) -> (4) -> (2) -> (3) -> None
# mid1 mid2 ==> (4) -> (3) -> None ==>
# ^
# right reaches shared slow,
# notice how "(4) -> (3) -> None" is already prepared
# tc: O(n/2) ~ O(n)
while right != slow:
# Grab next before disconnect
next_left, next_right = left.next, right.next
# Merge lists alternating
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
# When right == slow, its a valid node so we can set its .next to None
# but this is redundant since we already pointed slow to None during the reversal phase,
# so right.next.next = None is actually doing None.next = None,
# which is why we get the error: 'NoneType' object has no attribute 'next'
# overall: tc O(n)
# overall: sc O(1)
return328. 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, then connect the two lists.
Pseudocode
Sol: Odd-Even Grouping:
1. if head is null:
a. return head
2. odd = head
3. even = head.next
4. evenHead = even
5. while even and even.next:
a. odd.next = even.next
b. odd = odd.next
c. even.next = odd.next
d. even = even.next
6. odd.next = evenHead
7. return headSolution 1: [Linked List] Iterative In Place Odd Even Partition - Linked List/Simple Traversal
def oddEvenList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Odd-Even Grouping:
# - All odd-indexed nodes are relinked together, followed by all even-indexed nodes
# - Relative order within each group is preserved
# - Achieved in a single pass by weaving two interleaved pointers forward
# Grouping Phase (1-indexed):
# Input: 1 -> 2 -> 3 -> 4 -> 5 -> null
# odd even
# Step 1: 1 -> 3 2 -> 4 -> 5 -> null
# odd even
# Step 2: 1 -> 3 -> 5 -> null 2 -> 4 -> null
# odd even
# Final: 1 -> 3 -> 5 -> 2 -> 4 -> null
# Idea:
# - Use two pointers, odd and even, starting at nodes 1 and 2
# - Weave odd forward through odd-indexed nodes, even forward through even-indexed nodes
# - Save the head of the even list before it's disconnected from the odd list
# - Once even runs out, attach the saved even list to the end of the odd list
# 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
# base case:
# empty list is already "grouped"
if not head:
return head
evenDummyHead = even
evenCurr = head.next
oddCurr = head
# weave odd/even pointers forward, relinking each to skip the other group
# tc: O(n)
while even and even.next:
# Index 1, 3, 5 -> odd indexed
odd.next = even.next
odd = odd.next
# Index 2, 4, 6 -> even indexed
even.next = odd.next
even = even.next
# Odd list becomes left hand side new list
# Even list becomes right hand side of new list
# Attach the tail of the odd list to head of even list
odd.next = evenHead
# overall: tc O(n)
# overall: sc O(1)
return head86. 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.
Pseudocode
Sol 1: Two Dummy Head Bucket Split:
1. lessDummyHead = ListNode(-1)
2. lessTail = lessDummyHead
3. greaterDummyHead = ListNode(-1)
4. greaterTail = greaterDummyHead
5. curr = head
6. while curr:
a. if curr.val < x:
lessTail.next = curr
lessTail = lessTail.next
b. else:
greaterTail.next = curr
greaterTail = greaterTail.next
c. curr = curr.next
7. greaterTail.next = None
8. lessTail.next = greaterDummyHead.next
9. return lessDummyHead.nextSolution 1: [Linked List] Two Dummy Head Bucket Split - Linked List/Two Pointers Partition List
def partition(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
# Dual Dummy-Head Split:
# - Every node is routed into one of two chains: less-than-x, or greater-or-equal-to-x
# - Both chains are built in relative original order
# - Join the left and right list at the end
# Partition Phase:
# Input: 1 -> 4 -> 3 -> 2 -> 5 -> 2 -> None, x = 3
# Route: less: 1 -> 2 -> 2 -> None
# greater: 4 -> 3 -> 5 -> None
# Splice: 1 -> 2 -> 2 -> 4 -> 3 -> 5 -> None
# Idea:
# - Walk the original list once, routing each node to the less or greater chain
# - Use 2 dummyHeads to have access to both partition lists
# - After the pass, turn the right side greater chain into a tail,
# grab the head of the left less list, and connect the left to right list
# Original list iterator
curr = head
# List1 iterator
lessDummyHead = ListNode(-1)
lessCurr = lessDummyHead
# List 2 iterator
greaterDummyHead = ListNode(-1)
greaterCurr = greaterDummyHead
# Iterate over original linked list, and partition into less or greater than
# tc: O(n)
while curr:
# Send anything less than the value to the less list
if curr.val < x:
lessCurr.next = curr
lessCurr = lessCurr.next
# Everything else to the greater list
else:
greaterCurr.next = curr
greaterCurr = greaterCurr.next
# Check next node in original list
curr = curr.next
# List 2 will become the right hand side of the list, and holds the tail of the new list
greaterCurr.next = None
# List 1 will become the left hand side of the list, and holds the head of the new list
# and will connect the left and right half of the list
lessCurr.next = greaterDummyHead.next
# Grab new head of list
newHead = lessDummyHead.next
# overall: tc O(n)
# overall: sc O(1)
return newHead147. 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.
Pseudocode
Sol 1: In Place Insertion Sort With Optimal Avoiding DummyHead Restart:
1. dummyHead = ListNode(0)
2. curr = head
3. prev = dummyHead
4. while curr:
a. nextUnsorted = curr.next
b. if prev.val >= curr.val:
prev = dummyHead
c. while prev.next and prev.next.val < curr.val:
prev = prev.next
d. curr.next = prev.next
e. prev.next = curr
f. curr = nextUnsorted
5. return dummyHead.nextSolution 1: [Linked List] Generic Iterative In Place Insertion Sort - Linked List/Simple Traversal
def insertionSortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# In Place Insertion Sort:
# - A sorted portion is grown one node at a time, starting empty
# - Each node from the unsorted input is inserted into its correct position
# - A dummy head lets the sorted portion start from nothing without special-casing
# Sorting Phase:
# Input: 4 -> 2 -> 1 -> 3 -> None
# Step 1: sorted: 4 -> None (sorted list was empty, insert 4, )
# Step 2: sorted: 2 -> 4 -> None (compare 4 and 2, swap)
# Step 3: sorted: 1 -> 2 -> 4 -> None (compare 4 and 1, swap, compare 2 and 1, swap)
# Step 4: sorted: 1 -> 2 -> 3 -> 4 -> None (compare 4 and 3, swap, compare 2 and 3, stay in place)
# Idea:
# - curr walks the original unsorted list, one node at a time
# - For each curr, walk swap from left to right starting at dummyHead to find where it belongs
# - Splice curr in between prev and prev.next
# - Save curr.next before overwriting it, or the rest of the unsorted list is lost
# 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
# Each new node walks sorted partial list from dummyHead to right
# tc: O(n^2)
while curr != None:
# Grab next node before overwriting curr.next
nextUnsorted = curr.next
# Insertion Sort Quick Start Optimization:
# avoid restarting completely at dummyHead,
# if new element should go in front of the element we just inserted
if curr.val <= prev.val:
prev = dummyHead
# Walk curr to correct point in sorted partial list:
# - end of sorted list (prev.next != None)
# - found correct spot for curr (prev <= curr <= prev.next)
while prev.next != None and prev.next.val < curr.val:
prev = prev.next
# Place curr at correct spot in sorted partial list
curr.next = prev.next
prev.next = curr
# Advance iterator to next unsorted node
curr = nextUnsorted
# Grab head of new sorted list
newHead = dummyHead.next
# overall: tc O(n^2)
# overall: sc O(1)
return newHead148. Sort List ::2:: - 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, sort it. Use merge sort.
Pseudocode
Sol 1: Top-Down Recursive Merge Sort:
1. if head is null or head.next is null:
a. return head
2. slow = head, fast = head.next
3. while fast and fast.next:
a. slow = slow.next
b. fast = fast.next.next
4. mid = slow.next
5. slow.next = null
6. left = sortList(head)
7. right = sortList(mid)
8. dummyHead = ListNode(0)
9. curr = dummyHead
10. while left and right:
a. if left.val <= right.val:
- curr.next = left
- left = left.next
b. else:
- curr.next = right
- right = right.next
c. curr = curr.next
11. curr.next = left or right
12. return dummyHead.next
Sol 2: Bottom-Up Iterative Merge Sort:
1. if head is null or head.next is null:
a. return head
2. n = 0, node = head
3. while node:
a. n = n + 1
b. node = node.next
4. dummyHead = ListNode(0, head)
5. size = 1
6. while size < n:
a. curr = dummyHead.next
b. tail = dummyHead
c. while curr:
- left = curr
- right = split(left, size)
- curr = split(right, size)
- merged, mergedTail = merge(left, right)
- tail.next = merged
- mergedTail.next = curr
- tail = mergedTail
d. size = size * 2
7. return dummyHead.next
Helper split(head, size):
1. for i in range(size - 1):
a. if not head: return null
b. head = head.next
2. if not head: return null
3. rest = head.next
4. head.next = null
5. return rest
Helper merge(l1, l2):
1. dummyHead = ListNode(0)
2. curr = dummyHead
3. while l1 and l2:
a. if l1.val <= l2.val:
- curr.next = l1
- l1 = l1.next
b. else:
- curr.next = l2
- l2 = l2.next
c. curr = curr.next
4. curr.next = l1 or l2
5. while curr.next:
a. curr = curr.next
6. return dummyHead.next, currSolution 1: [Linked List] Recursive Top Down Merge Sort - Linked List/Simple Traversal
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Merge Sort:
# - List is recursively split in half until sublists of size 0 or 1 remain
# - Sorted sublists are merged back together on the way up the recursion
# - Each level of recursion doubles the size of the merged result
# Splitting Phase (Top-Down):
# Level 0: [8, 4, 7, 3, 2, 6, 5, 1] 2^0 = 1 subarrays (n elements, O(n))
# / \
# Level 1: [8, 4, 7, 3] [2, 6, 5, 1] 2^1 = 2 subarrays (n/2 elements, O(n/2))
# / \ / \
# Level 2: [8, 4] [7, 3] [2, 6] [5, 1] 2^2 = 4 subarrays (n/4 elements, O(n/4))
# / \ / \ / \ / \
# Level 3: [8] [4] [7] [3] [2] [6] [5] [1] 2^3 = 8 subarrays (n/8 element, O(1))
# Idea:
# - Use slow/fast pointers to find the midpoint of the list
# - Split the list into two halves at the midpoint
# - Recurse on each half until reaching base case (0 or 1 nodes)
# - Merge the two sorted halves as recursion unwinds
# Note:
# 1. Find midpoint with slow/fast pointers, split list in two
# 2. Recurse on each half
# 3. Merge the two sorted halves
# 4. Base case: 0 or 1 nodes, already sorted
# base case:
# list is already sorted
if not head or not head.next:
return head
# find midpoint using slow/fast pointers
# tc: O(n)
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 is start of right half
mid = slow.next
slow.next = None
# recurse on each half
# tc: O(log n) levels of recursion
left = self.sortList(head)
right = self.sortList(mid)
# merge sorted halves
dummyHead = ListNode(0)
curr = dummyHead
# tc: O(n) total work across this level
while left and right:
# grab smaller value between the two and iterate that list
if left.val <= right.val:
curr.next = left
left = left.next
else:
curr.next = right
right = right.next
curr = curr.next
# one list is empty,
# connect remaining portion of non-empty list
curr.next = left or right
# overall: tc O(n log n) — log n levels of recursion, O(n) merge work per level
# overall: sc O(log n) — recursion stack depth
return dummyHead.nextSolution 2: [Linked List] Iterative Bottom Up Merge Sort [SC Opt] - Linked List/Simple Traversal
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Merge Sort:
# - List is divided into sublists of length size
# - Adjacent sublist pairs are merged into sorted groups of length (size * 2)
# - size doubles each round until it covers the whole list
# Merging Phase (Bottom-Up):
# Round 0: [8] [4] [7] [3] [2] [6] [5] [1] size = 1 (8 subarrays, O(1) each)
# \ / \ / \ / \ /
# Round 1: [4, 8] [3, 7] [2, 6] [1, 5] size = 2 (4 subarrays, O(2) each)
# \ / \ /
# Round 2: [3, 4, 7, 8] [1, 2, 5, 6] size = 4 (2 subarrays, O(4) each)
# \ /
# Round 3: [1, 2, 3, 4, 5, 6, 7, 8] size = 8 (1 subarray, O(8))
# Idea:
# - Start with sublists of size 1 (each already "sorted")
# - Walk the list splitting off and merging pairs of sublists,
# reattaching each merged group to the end of the result so far
# - Double `size` and repeat until size >= length, at which point
# the entire list has been merged into one sorted list
# Note:
# 1. Count list length
# 2. Instead of recursing top-down, merge bottom-up
# 3. Start with sublist size=1, merge sorted pairs of that size,
# doubling size each round until size >= length
# 4. Splitting and merging only rewires existing nodes,
# no new nodes or recursion needed
# Helper:
# split off a sublist of length size starting at head,
# sever the link, and return the head of the remainder
def split(head, size):
# tc: O(size)
for _ in range(size - 1):
if not head:
return None
head = head.next
if not head:
return None
# sever link between left sublist and remainder
rest = head.next
head.next = None
# overall: tc O(size)
# overall: sc O(1)
return rest
# Helper: merge two sorted sublists, return (mergedHead, mergedTail)
def merge(l1, l2):
dummyHead = ListNode(0)
curr = dummyHead
# tc: O(n), n = combined length of l1 and l2
while l1 and l2:
# grab smaller value between the two and iterate that list
if l1.val <= l2.val:
curr.next = l1
l1 = l1.next
else:
curr.next = l2
l2 = l2.next
curr = curr.next
# one list is empty,
# connect remaining portion of non-empty list
curr.next = l1 or l2
# advance curr to the tail of the merged list
while curr.next:
curr = curr.next
# overall: tc O(n)
# overall: sc O(1)
return dummyHead.next, curr
# 0 or 1 node:
# list is already sorted
if not head or not head.next:
return head
# 1. Count list length
n = 0
node = head
while node:
n += 1
node = node.next
# dummyHead:
# allows uniform access to the front of the list across rounds
dummyHead = ListNode(0, head)
# Size:
# Length of sublists to merge this round (start with length 1, then 2, 4, 8, etc...)
# tc: O(log n) rounds total, size doubles each round
size = 1
while size < n:
# Track head of curr sublist we are merging
curr = dummyHead.next
# tail tracks the end of the last merged group,
# so we can attach the next merged group after it
tail = dummyHead
# merge consecutive pairs of sublists of length 'size'
# tc: O(n) total work across this round
while curr:
# split off left sublist of size `size`
left = curr
right = split(left, size)
# split off right sublist of size `size`,
# curr becomes the start of the remainder
curr = split(right, size)
# merge left and right sublists
merged, mergedTail = merge(left, right)
# connect previous tail to this round's merged group
tail.next = merged
# connect this merged group's tail to the remainder
mergedTail.next = curr
# advance tail for the next merged group
tail = mergedTail
# Double size for next round
size *= 2
# overall: tc O(n log n) — log n rounds, O(n) work per round
# overall: sc O(1) — only reuses existing nodes, no recursion
return dummyHead.next61. 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
Pseudocode
Sol 1: Circular Shift:
1. if not head or not head.next:
a. return head
2. n = 1, tail = head
3. while tail.next:
a. tail = tail.next
b. n = n + 1
4. k = k % n
5. if k == 0:
a. return head
6. tail.next = head
7. newTail = head
8. for i in range(n - k - 1):
a. newTail = newTail.next
9. newHead = newTail.next
10. newTail.next = null
11. return newHeadSolution 1: [Linked List] Circular Shift - Linked List/Simple Traversal
def rotateRight(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# Circular Shift:
# - List is temporarily joined into a circle: tail -> head
# - New tail is found at position (n - k % n - 1) from the old head
# - Breaking the circle after the new tail yields the rotated list
# Rotation Phase:
# Input: 0 -> 1 -> 2 -> 3 -> 4 -> null, k = 2, n = 5
# effective shift = k % n = 2
# Step 1: 0 -> 1 -> 2 -> 3 -> 4 -
# ^ |
# | - - - - - - - - - - - (circular)
# Step 2: tail: (n - k - 1) = 2
# head (n - k) = 3
# Step 3: 0 -> 1 -> 2 -> null 3 -> 4 -> 0 -> 1 -> 2 -> null
# Idea:
# - Find the length of the list and its tail
# - Connect tail to head, forming a circular list
# - Walk (n - k % n - 1) steps from head to find the new tail
# - Break the circle by setting newTail.next = None
# Note:
# 1. k can be larger than list length, so use k % n to get effective shift
# 2. If k % n == 0, no rotation needed, return head unchanged
# 3. Joining into a circle avoids a second pass to relink the ends
# Empty List Or Single Node:
# Already rotated
if not head or not head.next:
return head
# Length of list to calculate new tail index
# tc: O(n)
n = 1
curr = head
while curr.next != None:
curr = curr.next
n += 1
tail = curr
# Early No Rotation:
# shift ends up back at start, no rotation needed
k %= n
if k == 0:
return head
# List into circle:
# connect tail to head
tail.next = head
# Walk to 1 before the new head,
# giving us the new tail
# tc: O(n)
newTail = head
for _ in range(n - k - 1):
newTail = newTail.next
# Grab the new head
newHead = newTail.next
# Circle into list:
# sever connection from new tail to new head
newTail.next = None
# overall: tc O(n)
# overall: sc O(1)
return newHead19. 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.
Pseudocode
Sol 2: Fixed Gap Two Pointers:
1. dummyHead = ListNode(-1, head)
2. slow = fast = dummyHead
3. i = 0
4. while i != k + 1:
a. i = i + 1
b. fast = fast.next
5. while fast:
a. fast = fast.next
b. slow = slow.next
6. slow.next = slow.next.next
7. return dummyHead.nextSolution 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]:
# Two-Pass Length Offset:
# - Total list length is computed on a first pass
# - The node just before the target is located at index (length - n) from a dummy head
# - Skipping that node's next pointer removes the target in a single splice
# Removal Phase:
# Input: 1 -> 2 -> 3 -> 4 -> 5 -> null, n = 2, listLen = 5
# removeDist = listLen - n = 3
# Pass 1: dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null (walk full list, count length)
# Pass 2: dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
# curr (walked 3 steps from dummy)
# Splice: dummy -> 1 -> 2 -> 3 -> 5 -> null (curr.next skipped, node 4 removed)
# Idea:
# - Traverse the list once to calculate total length
# - Use a dummy node to handle edge cases (like removing head)
# - Traverse again from dummy, stopping at the node just before the removal index
# - Point prior.next = prior.next.next, disconnecting the removal node
# 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
# 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
dummyHead = ListNode(-1, head)
curr = dummyHead
# Second Pass: walk to node just before the target node
# tc: O(n)
removeDist = listLen - n
i = 0
while i != removeDist:
i += 1
curr = curr.next
# arrived at (n-1)th node from target: skip the target node
curr.next = curr.next.next
# overall: tc O(n)
# overall: sc O(1)
return dummyHead.nextSolution 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]:
# Fixed-Gap Two Pointers:
# - fast is sent ahead by (k + 1) steps, opening a gap of k nodes between slow and fast
# - Both pointers then advance together, so the gap stays fixed
# - When fast runs off the end, slow lands just before the target node
# Removal Phase:
# Input: 1 -> 2 -> 3 -> 4 -> 5 -> null, k = 2
# dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
# slow
# Step 1: fast advances (k + 1) = 3 steps from dummy
# dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
# slow fast
# Step 2: advance slow and fast together until fast hits null
# dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
# slow fast (now null)
# Splice: dummy -> 1 -> 2 -> 3 -> 5 -> null (slow.next skipped, node 4 removed)
# Idea:
# - Send fast ahead by (k + 1) steps from a dummy head, opening a gap of k nodes
# - Advance slow and fast together, preserving that gap, until fast falls off the list
# - slow now sits just before the node k from the end; skip it to remove
# Note:
# 1. Use two pointers spaced k apart (fast and slow)
# 2. Move both pointers together until fast hits the 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
dummyHead = ListNode(-1, head)
slow = fast = dummyHead
# send fast ahead by (k + 1) steps to open a gap of k nodes
# tc: O(k)
i = 0
while i != k + 1:
i += 1
fast = fast.next
# advance both pointers together, preserving the gap, until fast falls off the list
# tc: O(n)
while fast:
fast = fast.next
slow = slow.next
# slow is pointing to the node just before the target: skip it
slow.next = slow.next.next
newHead = dummyHead.next
# overall: tc O(n)
# overall: sc O(1)
return newHead287. 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.
Pseudocode
Sol 3: Modified Floyd Cycle:
1. slow = nums[0]
2. fast = nums[0]
3. while true:
a. slow = nums[slow]
b. fast = nums[nums[fast]]
c. if slow == fast: break
4. slow = nums[0]
5. while true:
a. if slow == fast: break
b. slow = nums[slow]
c. fast = nums[fast]
6. return slowSolution 1: [Bit Masking] Create Bit Mask For Longest Number Then Bit Mask Each Slot Looking For The Duplicate Flip - 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_reconstructionSolution 2: [Binary Search] Pigeonhole Search Side With An Extra Number - 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 leftSolution 3: [Linked List] 3 Step Modified Floyd Cycle With FindMeeting() ResetSlow() 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 slow83. 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.
Pseudocode
Sol 1: Adjacent Duplicate Removal:
1. curr = head
2. while curr and curr.next:
a. if curr.val == curr.next.val:
- curr.next = curr.next.next
b. else:
- curr = curr.next
3. return headSolution 1: [Linked List] Iterative In Place Compare Curr To Next And Skip Connection - Linked List/Simple Traversal
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Adjacent Duplicate Removal:
# - List is already sorted, so duplicate values are always adjacent
# - curr walks the list, skipping over any node whose value matches curr's
# - Once curr.next holds a distinct value, curr advances onto it
# Removal Phase:
# Input: 1 -> 1 -> 2 -> 3 -> 3 -> null
# curr
# Step 1: 1 -> 2 -> 3 -> 3 -> null (curr.next skipped, duplicate 1 removed)
# curr
# Step 2: 1 -> 2 -> 3 -> 3 -> null (values differ, curr advances)
# curr
# Step 3: 1 -> 2 -> 3 -> null (curr.next skipped, duplicate 3 removed)
# curr
# Idea:
# - Walk curr through the list one node at a time
# - Whenever curr.val == curr.next.val, splice curr.next out of the list
# - Otherwise the values differ, so advance curr onto curr.next
# 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
# 4. No dummy node needed since we never remove the head
curr = head
# tc: O(n)
while curr != None and curr.next != None:
# Sever connection from current val to any following duplicates
if curr.val == curr.next.val:
curr.next = curr.next.next
# No duplicate, keep connection
else:
curr = curr.next
# overall: tc O(n)
# overall: sc O(1)
return head82. 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.
Pseudocode
Sol 1: Full Duplicate Group Removal:
1. dummyHead = ListNode(0, head)
2. prev = dummyHead
3. curr = head
4. while curr:
a. if curr.next and curr.val == curr.next.val:
- dupVal = curr.val
- while curr and curr.val == dupVal:
> curr = curr.next
- prev.next = curr
b. else:
- prev = curr
- curr = curr.next
5. return dummyHead.nextSolution 1: [Linked List] Iterative In Place Compare Curr To Next And Skip Group Connection - Linked List/Simple Traversal
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Full Duplicate Group Removal:
# - List is already sorted, so duplicate values form contiguous groups
# - Any node that has a duplicate is removed entirely, not just the extras
# - A dummy node is used since the head itself may belong to a duplicate group
# Removal Phase:
# Input: 1 -> 2 -> 3 -> 3 -> 4 -> null
# dummy -> 1
# prev curr
# Step 1: curr.val == curr.next.val? No (1 != 2) -> advance prev, curr
# dummy -> 1 -> 2 -> 3 -> 3 -> 4 -> null
# prev curr
# Step 2: curr.val == curr.next.val? Yes (3 == 3) -> skip entire group
# dummy -> 1 -> 2 -> 4 -> null
# prev curr
# Idea:
# - Keep a dummy node before head so a duplicate at the head can be removed uniformly
# - prev always points to the last confirmed distinct node
# - If curr starts a duplicate group, advance curr past every node sharing that value,
# then connect prev directly to whatever comes after the group
# - Otherwise curr is distinct, so advance prev and curr together
# 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
# walk the list, removing entire duplicate groups as they're found
# tc: O(n)
while curr:
# Check if curr is at the start of a duplicate group
if curr.next != None and curr.val == curr.next.val:
# skip all nodes with this value
dupVal = curr.val
while curr != None and curr.val == dupVal:
curr = curr.next
# Curr has passed the duplicate group we found,
# connect prev to the new curr
prev.next = curr
# Distinct node, safe to advance prev
else:
prev = curr
curr = curr.next
# overall: tc O(n)
# overall: sc O(1)
return dummyHead.next146. 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.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)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 Pointers:
self.prev = None
self.next = None
class LRUCache:
# Strategy:
# 1. HashMap for O(1) key -> node lookup
# 2. Use a doubly linked list with dummyHead/dummyTail sentinels,
# to avoid null checking edge cases at the ends
# 3. _remove_node() unlinks a node from its current position in O(1)
# 4. _push_MRU() reinserts a node just after dummyHead in O(1)
# 5. get() and put() combine these two primitives to maintain
# recency order
# Ordering Convention:
# dummyHead <-> MRU <-> ... <-> LRU <-> dummyTail
# get(key): move accessed node to MRU position after dummyHead
# put(key, val): insert/update node at MRU position
# evict LRU (dummyTail.prev) if over capacity
def __init__(self, capacity: int):
# Capacity of cache
self.capacity = capacity
# Node Cache:
# key -> node, for O(1) lookup
self.cache = {}
# MRU dummyHead / LRU dummyTail:
# list starts empty, connect dummyHead <-> dummyTail
self.dummyHead = Node(0, 0)
self.dummyTail = Node(0, 0)
self.dummyHead.next = self.dummyTail
self.dummyTail.prev = self.dummyHead
# All functions have:
# tc: O(1)
# sc: O(1)
# Both get() and put() need to do the exact same 4 line pointer shift
# to insert the new node after dummyHead as the new MRU,
# so we put that logic into 2 private functions to avoid duplicating logic across 2 functions
# Remove provided node by connecting neighboring nodes to each other
def _remove_node(self, node: Node):
# Grab neighbor nodes
prev, next = node.prev, node.next
# Connect neighbors to each other
prev.next = next
next.prev = prev
# Insert node in front of dummyHead, becomes the new MRU
def _push_MRU(self, node: Node):
# Place new MRU after dummyHead
node.next = self.dummyHead.next
node.prev = self.dummyHead
# Point dummyHead to new MRU
self.dummyHead.next = node
# point prev of old MRU to new MRU
node.next.prev = node
# Return node if it exists
def get(self, key: int) -> int:
# Miss:
if key not in self.cache:
return -1
# Hit:
# Grab node
node = self.cache[key]
# Remove old location
self._remove_node(node)
# Node becomes new MRU
self._push_MRU(node)
# return grabbed val
return node.val
# Insert node
def put(self, key: int, value: int) -> None:
# If node already exists in cache, remove from list to avoid duplicates
if key in self.cache:
node = self.cache[key]
self._remove_node(node)
node.val = value
# Create new node and add to cache
else:
node = Node(key, value)
self.cache[key] = node
# New node becomes new MRU
self._push_MRU(node)
# If list is over capacity, remove LRU
if self.capacity < len(self.cache):
# Grab lru
lruNode = self.dummyTail.prev
# Remove lru
self._remove_node(lruNode)
# Remove from cache
del self.cache[lruNode.key]
# overall: tc O(1)
# overall: sc O(n)138. Copy List with Random Pointer ::3:: - Medium
Topics: Deep Copy, 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.
Pseudocode
Sol 2: One Pass Memoization HashMap Lazy Construction:
1. if not head:
a. return null
2. dummyHead = Node(-1)
3. prev = dummyHead
4. curr = head
5. memo = {}
6. while curr:
a. if curr not in memo:
- memo[curr] = Node(curr.val)
b. currDeepCopy = memo[curr]
c. if curr.random:
- if curr.random not in memo:
> memo[curr.random] = Node(curr.random.val)
- currDeepCopy.random = memo[curr.random]
d. prev.next = currDeepCopy
e. prev = prev.next
f. curr = curr.next
7. deepCopyHead = memo[head]
8. return deepCopyHead
Sol 3: Three Pass In Place Interleaving:
1. if not head:
a. return null
2. curr = head
3. while curr:
a. deepCopy = Node(curr.val)
b. deepCopy.next = curr.next
c. curr.next = deepCopy
d. curr = deepCopy.next
4. curr = head
5. while curr:
a. deepCopy = curr.next
b. if curr.random:
- deepCopy.random = curr.random.next
c. curr = deepCopy.next
6. curr = head
7. deepCopyDummyHead = head.next
8. while curr:
a. deepCopy = curr.next
b. curr.next = deepCopy.next
c. curr = curr.next
d. if curr:
- deepCopy.next = curr.next
9. return deepCopyDummyHeadSolution 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]':
# Strategy:
# 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
# .get():
# returns None if nothing exists in the hashmap for that key,
# perfect for our Node vs Tail cases (tail's .next/.random are None,
# and no None key exists in the map)
# Empty Check:
if not head:
return None
# Orig Node -> DeepCopy Node:
origToDeepCopy = {}
# Pass 1: Create all nodes
# tc: O(n)
curr = head
while curr:
# Create a deep copy and insert into hashmap
copy = Node(curr.val)
origToDeepCopy[curr] = copy
# Iterate
curr = curr.next
# Iteration 2: Wire Next And Random
# tc: O(n)
curr = head
while curr:
# Grab deepCopy of curr node
deepCopy = origToDeepCopy[curr]
# Node vs Tail:
# curr.next/curr.random could be None if curr is the tail,
# but get() returns None for a missing key so the tail case is covered
deepCopy.next = origToDeepCopy.get(curr.next)
deepCopy.random = origToDeepCopy.get(curr.random)
# Iterate
curr = curr.next
# Return new deepCopy head node,
# which is connected to the rest of the new deepCopy list
newDeepCopyHead = origToDeepCopy[head]
# overall: tc O(n)
# overall: sc O(n)
return newDeepCopyHeadSolution 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:
# keeps a fixed reference to the start of the deepCopy list
# sc: O(1)
dummyHead = Node(-1)
# prev connects each deepCopy node as we lazily build the list
prev = dummy
curr = head
# Orig Node -> DeepCopy Node:
memo = {}
# tc: O(n)
while curr:
# Create deepCopy of curr node if not already created
if curr not in memo:
memo[curr] = Node(curr.val)
# Grab deepCopy
currDeepCopy = memo[curr]
# Random Connection:
if curr.random:
# Create deepCopy of curr.random if not already created
if curr.random not in memo:
memo[curr.random] = Node(curr.random.val)
# Set deepCopy random pointer
currDeepCopy.random = memo[curr.random]
# Connect previous deepCopy node to curr deepCopy node
prev.next = currDeepCopy
# Iterate both lists
prev = prev.next
curr = curr.next
# DeepCopy head:
# newDeepCopyHead = dummyHead.next
# or
newDeepCopyHead = memo[head]
# overall: tc O(n)
# overall: sc O(n)
return newDeepCopyHeadSolution 3: [Linked List] Three Pass In Place Interleaving Technique [SC Opt] - Linked List/Simple Traversal
def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
# Interleave Pattern:
# A -> B -> C -> None becomes A -> A' -> B -> B' -> C -> C' -> None
# This is a zig-zag merge of:
# list1 = original list: A -> B -> C -> None
# list2 = deepCopy list: A' -> B' -> C' -> None
# Strategy:
# 1. Interleave a deepCopy node after each original node
# 2. Use the interleaved structure to wire random pointers
# (a node's deepCopy random is just curr.random.next)
# 3. Split the merged list back into original and deepCopy lists
# Using the original list itself to build the deepCopy avoids
# a hashmap entirely, achieving sc O(1)
# Split Pattern:
# Before
# A -> A' -> B -> B' -> C -> C' -> ... -> None
# After
# A -> B -> C -> D -> E -> None
# A' -> B' -> C' -> D' -> E' -> None
# Empty Check
if not head:
return None
# -----------------------------------------
# 1. Merged List from Original List And Created DeepCopy Nodes
curr = head
# Step 1: Interleave DeepCopy Nodes
# A -> A' -> B -> B' -> C -> C' -> ...
# tc: O(n)
while curr:
# Create deepCopy and interleave after original node
# A -> A' -> B -> B' -> C -> C'
deepCopy = Node(curr.val)
deepCopy.next = curr.next
# Link original node to new deep copy
curr.next = deepCopy
# Iterate to next original node
curr = deepCopy.next
# Reset Iterator to head original node:
curr = head
# Step 2: Wire Random Pointers
# curr.random points to an original node, and since every
# original node is immediately followed by its deepCopy,
# we can access that nodes deepCopy via curr.random.next
# tc: O(n)
while curr:
# Grab interleaved deepCopy
deepCopy = curr.next
# Random Connection:
if curr.random:
# Grab the deepCopy of the random node from the curr node reference:
# ... -> Random -> Random' -> ...
deepCopy.random = curr.random.next
# Iterate to next original node
curr = deepCopy.next
# -----------------------------------------
# 2. Split Merged List into Original And DeepCopy
# Original and DeepCopy List Iterators
# sc: O(1)
# Original list Iterator:
# head is still == A
curr = head
# DeepCopy List Iterator:
# deepCopyHead is A -> A', so head.next is now == A'
deepCopyDummyHead = head.next
# Step 3: Split Merged List
# curr walks the original list, deepCopy walks the copy list
# tc: O(n)
while curr:
# Grab curr's deepCopy: A -> A'
deepCopy = curr.next
# Re-link curr original node to the next original node
# A -> A' -> B -> ... becomes A -> B -> ...
curr.next = deepCopy.next
# Iterate to the next original node B
curr = curr.next
# Validate if we have another original node:
# connect current deepCopyNode to the next deepCopyNode
if curr != None:
# set A' -> B' (grabbing B' from B -> B')
deepCopy.next = curr.next
# return saved deepCopy head
newDeepCopyHead = deepCopyDummyHead
# overall: tc O(n)
# overall: sc O(1)
return newDeepCopyHead109. 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
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Linked List] [Tree] 2 Step GetLen() Recursive In order Simulation [SC Opt] - Linked List/Simple Traversal
def sortedListToBST(self, head: Optional[ListNode]) -> Optional[TreeNode]:
# In Order Traversal Simulation:
# - Given a BST, In Order traversal results in sorted order
# - Linked List is already sorted, so advance a single pointer in lockstep
# will build the BST without ever searching for a midpoint
# Building Phase:
# Input (sorted list): Index Range [0, 4], mid = 2:
# -10 -> -3 -> 0 -> 5 -> 9 -> None convert(0,4)
# [0] [1] [2] [3] [4] / \
# convert(0,1) convert(3,4)
# Left Subtree convert(0,1), mid=0: After Left Subtree Built:
# convert(0,1) 0
# / \ /
# convert(0,-1) convert(1,1) -10
# (None) | \
# self.curr: -10 -> -3 -3
# root = -10, .right = -3
# self.curr now at 0, becomes root: Right Subtree convert(3,4), mid=3:
# 0 convert(3,4)
# / \ / \
# -10 (right subtree pending) convert(3,2) convert(4,4)
# \ (None) |
# -3 self.curr: 5 -> 9
# root = 5, .right = 9
# Final Result:
# 0
# / \
# -3 5
# / /
# -10 9
# Idea:
# - Count the list length upfront, giving an index range [0, length - 1] to build over
# - Recurse using only index bounds (left, right), never touching the list directly
# - Because recursion always builds the left subtree before visiting the root,
# self.curr naturally lands on the correct node when the root is assigned
# - Advance self.curr by one node each time a root is assigned, then build the right subtree
# Note:
# 1. Count list length upfront to establish the index range
# 2. self.curr is shared, shared mutable state, advances once per node visited
# 3. Recursion order left -> root -> right mirrors the sorted list's natural order
# 4. No midpoint search or list severing needed, unlike Solution 1
# Linked List length
length = 0
node = head
while node:
length += 1
node = node.next
# Running pointer that walks straight through the original linked list
self.curr = head
def convert(left, right):
# base case:
# empty index range has no corresponding subtree
if left > right:
return None
# (+1) Bias towards upper gives:
# 0
# / \
# -3 9
# / /
# -10 5
mid = (left + right + 1) // 2
# () Bias towards lower gives:
# 0
# / \
# -10 5
# \ \
# -3 9
# mid = (left + right) // 2
# Build left subtree first
# (inorder: left -> root -> right)
leftSubtree = convert(left, mid - 1)
# Create root node
root = TreeNode(self.curr.val)
# Iterate running pointer
self.curr = self.curr.next
# Connect left subtree
root.left = leftSubtree
# Build right subtree
rightSubtree = convert(mid + 1, right)
# Connect right subtree
root.right = rightSubtree
return root
bstHead = convert(0, length - 1)
# overall: tc O(n)
# overall: sc O(log n)
return bstHead114. 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
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Tree] Morris Traversal In Place [SC Opt] - Linked List/Simple Traversal
def flatten(self, root: Optional[TreeNode]) -> None:
# Morris Threading:
# - For each node with a left child:
# 1. right subtree is threaded into the rightmost node of its left subtree
# give use the (the inorder predecessor)
# 2. left subtree is pulled up to become the right subtree
# 3. left is cleared
# Flattening Phase:
# Input (original tree): Input (after threading 4->5):
# 1 1
# / \ /
# 2 5 2 5
# / \ \ / \ \
# 3 4 6 3 4 6
# Input (right subtree pulled up): Result (after step 1, curr=1 done):
# 1 1
# / \ \
# 2 2
# / \ / \
# 3 4 3 4
# \ \
# 5 5
# \ \
# 6 6
# Mid Step 2 (thread 3->4): Mid Step 2 (pull left up):
# 1 1
# \ \
# 2 2
# / /
# 3 4 3
# \ \
# 5 4
# \ \
# 6 5
# \
# 6
# Final Result:
# 1
# \
# 2
# \
# 3
# \
# 4
# \
# 5
# \
# 6
# Idea:
# - Walk curr through the tree via its (soon-to-be) right pointers
# - If curr has a left child, find the rightmost node of that left subtree
# - Thread curr's original right subtree onto that rightmost node
# - Move the left subtree to the right, clear left, then advance curr
# 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
# 3. Key insight: each node's right subtree is threaded onto the bottom-right
# of its left subtree, then the left subtree is pulled up to the right
# 4. O(1) space, no recursion, no stack
# Iterator
curr = root
# tc: O(n)
while curr:
# If node has a left subtree
if curr.left:
# Grab the rightmost node of that left subtree
rightmost = curr.left
while rightmost.right:
rightmost = rightmost.right
# Thread the current right node,
# after the rightmost node we just grabbed
rightmost.right = curr.right
# Pull the left subtree up to become the new right node
curr.right = curr.left
curr.left = None
# Iterate to the right node
curr = curr.right
# overall: tc O(n)
# overall: sc O(1)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.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Tree] Iterative Level Traversal - Linked List/Simple Traversal
def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
# Iterative Traversal Using Existing Next Pointers:
# - Once a level's next pointers are set, that level can be walked like a linked list
# - Each pass over the current level sets the next pointers for the level below it
# - leftmost tracks the first node of each level, always reached via .left
# Connection Phase:
# Input: Level 0 (curr = 1):
# 1 1 -> None
# / \ |
# 2 3 1.left.next = 1.right
# / \ / \ 2.next = 3
# 4 5 6 7 1.next is None, skip cross
# After Level 0: Level 1 (curr = 2, then 3):
# 1 -> None 2 -> 3 -> None
# / \ 2.left.next = 2.right
# 2 - 3 4.next = 5
# / \ / \ 2.next(3) set, cross-connect
# 4 5 6 7 5.next = 6
# Mid Level 1 (curr = 3): After Level 1:
# 1 -> None 1 -> None
# / \ / \
# 2 - 3 2 - 3
# / \ / \ / \ / \
# 4 5 6 7 4 5-6 7
# |
# 3.left.next = 3.right
# 6.next = 7
# 3.next is None, skip cross
# Final Result:
# 1 -> None
# / \
# 2 -- 3 -> None
# / \ / \
# 4--5--6--7 -> None
# Idea:
# - Start leftmost at root, drop down one level each outer iteration
# - Walk curr across parents using previous next pointers
# - Connect siblings to siblings and the right sibling to the left cousin
# - Iterate to the parent's sibling to grab the next set of siblings
# Empty Check:
if not root:
return root
# At root level, the root itself is the leftmost node
leftmost = root
# Drop Down One Level At A Time:
# Assuming perfect BST, continue until no left children remain (bottom level reached)
# tc: O(n)
while leftmost.left:
# Start node for this level
curr = leftmost
# leftmost = 2
#
# 1 -> None
# / \
# 2 -----> 3 -> None
# / \ / \
# 4 5 6 7
# Walk Current Level:
# traverse using already-set next pointers from the level above
while curr:
# Connect siblings to same parent root
curr.left.next = curr.right
# leftmost = 2
#
# 2 --> 3 -> None
# / \ / \
# 4 --> 5 --> 6 7
# Cross Parent Connection:
# Iterate to parent's sibling, to access next set of siblings
if curr.next:
# Connect sibling to cousin
curr.right.next = curr.next.left
# leftmost = 2
#
# 2 --> 3 -> None
# / \ / \
# 4 --> 5 --> 6 7
# Iterate to next parent's sibling root
curr = curr.next
# Drop down to next level
leftmost = leftmost.left
# overall: tc O(n)
# overall: sc O(1)
return root117. 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
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Tree] Iterative Next Level Linked List - Linked List/Simple Traversal
def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
# Note:
# - We can't use the leftmost.left trick from 116
# as children may be missing from imperfect BST
# Dummy-Head Next-Level Construction:
# - The next level is built as a linked list while traversing the current level
# - A dummy node lets the first child found attach cleanly, no special-casing
# - tail tracks the end of the next level's list, extending it as children are found
# Connection Phase:
# Input: Level 0 (curr = 1):
# 1 1 -> None
# / \ |
# 2 3 dummy -> 2 -> 3
# / \ \ tail ends at 3
# 4 5 7 curr = dummy.next -> 2
# After Level 0: Level 1 (curr = 2, then 3):
# 1 -> None 2 -> 3 -> None
# / \ curr=2: left=4, right=5
# 2 - 3 dummy -> 4 -> 5
# / \ \ curr=curr.next -> 3
# 4 5 7 3: no left, right=7
# dummy -> 4 -> 5 -> 7
# Mid Level 1 (curr = 3): After Level 1:
# 1 -> None 1 -> None
# / \ / \
# 2 - 3 2 - 3
# / \ \ / \ \
# 4 5 7 4 5 -- 7
# |
# 3.left is None, skip
# 3.right = 7 -> tail.next = 7
# curr = dummy.next -> 4, level 2 begins
# Final Result:
# 1 -> None
# / \
# 2 -- 3 -> None
# / \ \
# 4 - 5 -- 7 -> None
# Empty Check
if not root:
return root
# At root level, root is the only node, traversal starts here
curr = root
# curr = 1
#
# 1 -> None
# / \
# 2 3
# Build Each Level's Linked List From The Level Above:
# tc: O(n)
while curr:
# dummyHead tracks left most node for below level
belowDummyHead = ListNode(0)
# currBelow acts as iterator for below level
currBelow = belowDummyHead
# belowDummyHead -> None
# curr = 1
# Walk Above Level:
# traverse using already set next pointers from previous iteration
while curr:
# Check if left or right child exists,
# if they do, attach to currBelow and iterate,
# this accounts for missing children
if curr.left:
currBelow.next = curr.left
currBelow = currBelow.next
# curr = 1
# belowDummyHead -> 2
# currBelow = 2
if curr.right:
currBelow.next = curr.right
currBelow = currBelow.next
# curr = 1
# belowDummyHead -> 2 -> 3
# currBelow = 3
# Iterate to next node on current above level
curr = curr.next
# Drop down:
# start curr at the left most node in the below level
curr = belowDummyHead.next
# overall: tc O(n)
# overall: sc O(1)
return root