Linked List:
Definitions:
- A nonlinear data structure consists of nodes with pointers to the next nodes.
- A linked list can be singly-linked, or doubly-linked, with just head pointer or together with tail pointer.
- For LeetCode, a singly-linked list with head pointer is usually given.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next- May fall under many patterns, such as fast and slow pointers.
General trick:
1. Sentinel head (also tail):
I create a dummy head first, modify everything after, and then return the actual head with dummy.next
dummy = ListNode(None)
head = dummy
# Do a lot of stuff with head
return dummy.nextThis is useful as I can use the head as a pointer to traverse the linked list while still need to return the head of the linked list in result.
Problems:
1. Reverse Linked List
Intuition
This is a basic problem - a problem that can be become a sub-problem for a bigger task in the future LeetCode problems. This is like a formula that you have to remember, and then exploit it over and over again.
Algorithm
When dealing with Linked List, the king is pointer - something as dangerous as pointing gun to your headan object that points to the particular position of a node in the list. The number of pointers a problem requires depend on the amount of information I need at when processing each node in the list. Here, I need to know 3 pieces: the current node (obviously), the previous node to point the current node to, and the next node to move the pointer. Hence, I will use three pointers - pre, cur, and nex to keep track while traversing the list.
Complexity
Time complexity: \(O(n)\): Traversing the whole list once.
Space complexity: \(O(1)\): Pointers are essentially integers, taking constant memory
Code
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Easy case:
if head is None or head.next is None:
return head
# General case:
pre, cur, nex = None, head, head.next
while nex:
cur.next = pre
pre = cur
cur = nex
nex = nex.next
cur.next = pre
return cur2. Reverse Nodes in k-Group
Intuition
The problem is twofold: getting the \(k^{th}\) node from the linked list (which is easy) and reverse just \(k\) nodes of the linked list, which is tricky.
I look at the algorithm to reverse the whole linked list, the hint is knowing the node right before the head of the group to point the tail to, and knowing the node right after the tail of the group to point the head to. The big problem is implementation.
For implementation, the node right before can be initialized as a dummy node first pointing to the head of the input linked list. The purpose is two fold. After all operations, the head of the return linked list can be accessed as dummy.next. Furthermore, the head of a group can be accessed as previous_to_group.next, while the node right after the group is next_to_group.next.
Algorithm
- Initialize
dummynode andprevious_to_group. - Iterate the linked list:
- Get the \(k^{th}\) i.e., the tail of a group and node right after it.
- Reverse this k-node group.
- Set
previous_to_groupfor the next iteration.
- Return
dummy.nexti.e., the head of the new linked list
Step 2. is implemented as a separate method.
Complexity
Time complexity: \(O(n)\): I need to iterate the linked list twice.
Space complexity: \(O(1)\): I only need to store pointers.
Code
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
dummy = ListNode(0, head)
previous_to_group = dummy # Node right before the group
while True:
kth_node = self.getKthNode(previous_to_group, k)
if not kth_node:
break
next_to_group = kth_node.next
# Reverse k nodes
prev, curr = kth_node.next, previous_to_group.next
while curr != next_to_group:
temp = curr.next
curr.next = prev
prev = curr
curr = temp
temp = previous_to_group.next
previous_to_group.next = kth_node
previous_to_group = temp
return dummy.next
def getKthNode(self, current_node, k):
while current_node and k > 0:
current_node = current_node.next
k -= 1
return current_node3. Reverse Linked List II
Intuition
I love the course “Grokking Coding Interview Pattern”. However, I have a feeling I have been scared. Reverse Linked List turns out to be a subproblem for Reverse Nodes in k-Group. The problem gives you two limits and asks you to reverse the nodes between the limits. The implementation is different from Reverse Nodes in k-Group as I reverse with range now, but the principle is exactly the same.
Algorithm
- Initialize
dummynode. - Iterate the linked list \(left-1\) times to get the node right before the group.
- Get the left node of the group and reverse the connections of \(right-left+1\) nodes in the group.
- Reconnect the new head and tail of the group to the nodes right before and right after.
Complexity
Time complexity: \(O(n)\): I iterate the linked list exactly once.
Space complexity: \(O(1)\): I only need to store pointers.
Code
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseBetween(self, head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]:
# Edge case
if not head or left == right:
return head
dummy = ListNode(0, head)
prev_of_group = dummy
for _ in range(left-1):
prev_of_group = prev_of_group.next
# Reverse node
prev, curr = None, prev_of_group.next
for _ in range(right-left+1):
temp = curr.next
curr.next = prev
prev = curr
curr = temp
# Now prev is the head of the new group, and curr is the node right after the group
prev_of_group.next.next = curr
prev_of_group.next = prev
return dummy.next4. Reorder List
Intuition
As I am on a roll with linked list reversal, it is easy to think of the naive algorithm: just reverse the whole remaining linked list every time as I iterate from the second node to the end. This is of course a bad solution, with a time complexity of \(O(n^2)\). To do better, I realized that the result list looks as if its later half has been reversed, and then merged one by one with the first half. This is interesting as the problem can be divided into 3 subproblems, ones I already know the answer:
- Find the middle of the linked list - fast and slow pointers.
- Reverse a linked list - with 3 pointers.
- Merge two linked list node by node - not mentioned before, but I showed you below (it appears simple in Python).
Algorithm
- Get to the start of the second half of the linked list. For this problem, I need the exact start for linked list with an even number of nodes, and pass the exact middle for odd one.
- Reverse the second half of the linked list.
- Splice the two linked lists into one.
Complexity
Time complexity: \(O(n)\): I iterate the linked list several times, but never nestedly.
Space complexity: \(O(1)\): I only need to store pointers.
Code
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reorderList(self, head: Optional[ListNode]) -> None:
"""
Do not return anything, modify head in-place instead.
"""
# Edge case - the list is guaranteed to be non-empty
if not head.next:
return head
# General case
slow, fast = head, head
while fast.next and fast.next.next:
slow = slow.next
fast = fast.next.next
# I use the Pythonic way to exchange variable. The real one
# going on is included to demonstrate
slow.next, slow = None, slow.next
# temp = slow.next
# slow.next = None
# slow = temp
slow = self._reverseList(slow)
while slow and head:
head.next, head = slow, head.next
slow.next, slow = head, slow.next
# temp = head.next
# head.next = slow
# head = temp
# temp = slow.next
# slow.next = head
# slow = temp
def _reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
pre, cur, nex = None, head, head.next
while nex:
cur.next = pre
pre = cur
cur = nex
nex = nex.next
cur.next = pre
return cur5. Swapping Nodes in a Linked List
Intuition
I got sidetracked into thinking how to do this optimally again, and cannot come up with the obvious solution: transcribe the linked list into an array, swap in the array (which is easier), and then reconstruct the linked list. The memory complexity is \(O(n)\). I want to do so in-place, since we are in the in-place pattern of linked list. Another obvious one is iterating the linked list 2 times: first to find the \(k^{th}\) node from the start and the total number of nodes in the linked list, second to find the \(k^{th}\) node from the end. However, I was informed of an one-pass approach, using which has now came to be obvious: fast and slow pointer. So iterate the array to find the \(k^{th}\) node from the start. Now I have a pair of nodes \(k\)-nodes apart. If one pointer is placed here and one is placed at the beginning, when I increment both of them together, the later will be the \(k^{th}\) node from the end when the former goes to the end. Afterwards, swap the values the nodes (not the connections) and return the original head.
Algorithm
Two pass
- Initialize
kth_from_beginandkth_from_endtohead. - Iterate the linked list with a pointer
tempand count the number of node. Assignkth_from_beginat the correct node. - Subtract
kfrom the node count to get the position ofkth_from_end. Iterate the linked list to assignkth_from_endcorrectly. - Swap just the values and return the head.
One pass
- Iterate the linked list with a pointer
tempand count the number of node. Assignkth_from_beginat the correct node. - Initialize
kth_from_beginto head. - Continue iterating with
tempandkth_from_beginat the same time. Astemphits the first node,kth_from_beginis pointed to the correct node. - Swap just the values and return the head.
Complexity
Time complexity: \(O(n)\): I iterate the linked list several times.
Space complexity: \(O(1)\): I only need to store pointers.
Code
Two pass
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def swapNodes(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# Key: Swap the values
# Edge case - guaranteed linked list non-empty
if not head.next:
return head
count = 0
kth_from_begin = kth_from_end = head
temp = head
while temp:
if count == k - 1:
kth_from_begin = temp
temp = temp.next
count += 1
# count will be 1 larger than the actual number of nodes
node_from_end = count - k
while node_from_end > 0:
node_from_end -= 1
kth_from_end = kth_from_end.next
kth_from_begin.val, kth_from_end.val = kth_from_end.val, kth_from_begin.val
return headOne pass
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def swapNodes(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# Key: Swap the values
# Edge case - guaranteed linked list non-empty
if not head.next:
return head
count = 0
temp = head
while count < k - 1:
temp = temp.next
count += 1
kth_from_begin, kth_from_end = temp, head
while temp.next:
kth_from_end, temp = kth_from_end.next, temp.next
kth_from_begin.val, kth_from_end.val = kth_from_end.val, kth_from_begin.val
return head6. Reverse Nodes in Even Length Groups
Intuition
I immediately thought of using a fast and slow pointers + keeping count of the current number of nodes in the group and the maximum number of nodes in the group. The later was correct, but the former was misguided. The objectives are the position of the last element of the previous group and the last element of the current group, which are needed for reversing. In that sense, it is sliding window, with the one side fixed at the last element of the previous group, while the other is dragged out to the last element of the current group. At each window, we check the oddity of the current number of nodes and reverse if necessary.
Algorithm
- Initialize the left pointer and max group length.
- Iterate the linked list starting at
head.next(the first element does not need to be reversed), one window up to the max group length at a time. - If there are even number of nodes in the window, reverse it. Else, do nothing.
- Return
headafter modification.
Complexity
Time complexity: \(O(n)\): Iterate the linked list less than two times.
Space complexity: \(O(n)\): Store only some integers.
Code
The shorthand reversal in Python is included in comments. While it looks awesome, I think that I should avoid it here as it makes debugging impossible and is certain to confuse my colleagues.
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseEvenLengthGroups(self, head: Optional[ListNode]) -> Optional[ListNode]:
group_prev = head
curr_max_length = 2
while group_prev.next:
curr = group_prev
count = 0
for _ in range(curr_max_length):
if not curr.next:
break
count += 1
curr = curr.next
if count % 2: # Odd, do nothing
group_prev = curr
else: # Even, reverse
group_next = curr.next
curr = group_prev.next
for _ in range(count):
# curr.next, curr, group_next = group_next, curr.next, curr
curr_next = curr.next
curr.next = group_next
group_next = curr
curr = curr_next
# group_prev.next.next, group_prev.next, group_prev = curr, group_next, group_prev.next
prev_next = group_prev.next
group_prev.next = group_next
group_prev = prev_next
curr_max_length += 1
return head7. Swap Nodes in Pairs
Intuition
The problem is straight-forward: I just need to iterate two nodes at a time and swap them. I need the right previous node and right after node to do so, so I keep track of it.
Algorithm
- Initialize a dummy head, and
previous+left_pointerfrom it. - Iterate the linked list and reverse each pair of nodes.
- Once
left_pointerreaches or goes past the end, return.
Complexity
Time complexity: \(O(n)\): Iterate the linked list twice.
Space complexity: \(O(1)\): Store only pointers.
Code
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Edge case
if not head:
return head
# General case
dummy = ListNode(0, head)
prev_to_group, left_pointer = dummy, dummy.next
while left_pointer and left_pointer.next:
right_pointer = left_pointer.next
next_to_group = right_pointer.next
prev_to_group.next = right_pointer
left_pointer.next = next_to_group
right_pointer.next = left_pointer
prev_to_group, left_pointer = left_pointer, left_pointer.next
return dummy.next8. Copy List with Random Pointer
Intuition
Quick intuition: copy nodes first, copy links later. To achieve this, I need a data structure that can tell me the corresponding links that a copied node should have. The obvious one is a hash map. The rest is trivial.
Algorithm
- Initializing a hash map to map old nodes to copied nodes. For convenience, initialize it with the first key-value pair of
None:None. - Traverse the linked list. Create the deep copy of each node.
- Traverse the hash map and create corresponding links for each node.
Complexity
Time complexity: \(O(n)\): Traverse the linked list twice.
Space complexity: \(O(n)\): The hash map needs to store the full linked list.
Code
"""
# Definition for a Node.
class Node:
def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
self.val = int(x)
self.next = next
self.random = random
"""
class Solution:
def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
oldToNew = {None:None}
# 1st pass: construct nodes
cur = head
while cur:
oldToNew[cur] = Node(cur.val)
cur = cur.next
# 2nd pass: construct links
for node in oldToNew:
if node is not None:
oldToNew[node].next = oldToNew[node.next]
oldToNew[node].random = oldToNew[node.random]
return oldToNew[head]9. LRU Cache
Intuition
The data structure needs to store key-value pairs. Easy, that is a hash map. The data structure needs to sort the pairs by recent usage. Okay, a queue can help to maintain the order, with the top of the queue being least recently used and the tail being most recently used. If an element is operated on, that element can just be pulled out of the queue and join back at the end. However, this is a case when an element in the middle can be randomly pulled out and needs inserting back. The normal array queue does not support that in \(O(1)\) time, so a different base data structure is needed i.e., doubly linked list, which supports removing and inserting at \(O(1)\).
Here the sentinel nodes also come in handy to prevent headache associated with capacity of 1. The head and tail pointers of the doubly linked list are initialized at these sentinel nodes instead.
Algorithm
Two helper methods insert and remove needs implementing. I am implementing it as a public method. If they need hiding from the users, they can be changed to hidden methods instead.
ListNode:__init__: Initialize withkey,value,prev, andnextpointer.
LRUCache:__init__: Initialize with hashmapcache,capacity,linkedlistsize(to compare withcapacity) and two sentinel nodes + pointers.remove: To simply remove a node from the linked list, move the destinations of theprevandnextpointers of the previous and the next of a node.insert: Add a node to the end of the linked list. Again, here it is just pointer manipulation.get: If the node is found inside the hash map, before returning it needs removing and inserting into the queue. Else, return -1put:
Complexity
Time complexity: \(O(1)\): For all methods.
Space complexity: \(O(\text{capacity})\): The maximum size of the hash map and the linked list.
Code
class ListNode:
def __init__(self, key, value):
self.key = key
self.value = value
self.prev = self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.cache = {}
self.capacity = capacity
self.linkedlistsize = 0
# head: least recently used, tail: most recently used
self.head, self.tail = ListNode(0, 0), ListNode(0, 0)
self.head.next, self.tail.prev = self.tail, self.head
# Insert node at tail
def insert(self, node):
pre, nxt = self.tail.prev, self.tail
node.prev, node.next = pre, nxt
pre.next = nxt.prev = node
self.linkedlistsize += 1
def remove(self, node):
pre, nxt = node.prev, node.next
pre.next, nxt.prev = nxt, pre
self.linkedlistsize -= 1
def get(self, key: int) -> int:
if key in self.cache:
self.remove(self.cache[key])
self.insert(self.cache[key])
return self.cache[key].value
return -1
def put(self, key: int, value: int) -> None:
if key in self.cache:
self.remove(self.cache[key])
if self.linkedlistsize == self.capacity:
lru = self.head.next
self.remove(lru)
self.cache.pop(lru.key)
self.cache[key] = ListNode(key, value)
self.insert(self.cache[key])
# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)