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
= ListNode(None)
dummy = dummy
head # Do a lot of stuff with head
return dummy.next
This 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:
= None, head, head.next
pre, cur, nex while nex:
next = pre
cur.= cur
pre = nex
cur = nex.next
nex next = pre
cur.return cur
2. 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
dummy
node 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_group
for the next iteration.
- Return
dummy.next
i.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:
= ListNode(0, head)
dummy = dummy # Node right before the group
previous_to_group
while True:
= self.getKthNode(previous_to_group, k)
kth_node if not kth_node:
break
= kth_node.next
next_to_group
# Reverse k nodes
= kth_node.next, previous_to_group.next
prev, curr
while curr != next_to_group:
= curr.next
temp next = prev
curr.= curr
prev = temp
curr
= previous_to_group.next
temp next = kth_node
previous_to_group.= temp
previous_to_group return dummy.next
def getKthNode(self, current_node, k):
while current_node and k > 0:
= current_node.next
current_node -= 1
k return current_node
3. 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
dummy
node. - 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
= ListNode(0, head)
dummy = dummy
prev_of_group for _ in range(left-1):
= prev_of_group.next
prev_of_group
# Reverse node
= None, prev_of_group.next
prev, curr for _ in range(right-left+1):
= curr.next
temp next = prev
curr.= curr
prev = temp
curr # Now prev is the head of the new group, and curr is the node right after the group
next.next = curr
prev_of_group.next = prev
prev_of_group.return dummy.next
4. 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
= head, head
slow, fast while fast.next and fast.next.next:
= slow.next
slow = fast.next.next
fast
# I use the Pythonic way to exchange variable. The real one
# going on is included to demonstrate
next, slow = None, slow.next
slow.# temp = slow.next
# slow.next = None
# slow = temp
= self._reverseList(slow)
slow
while slow and head:
next, head = slow, head.next
head.next, slow = head, slow.next
slow.# temp = head.next
# head.next = slow
# head = temp
# temp = slow.next
# slow.next = head
# slow = temp
def _reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
= None, head, head.next
pre, cur, nex
while nex:
next = pre
cur.= cur
pre = nex
cur = nex.next
nex next = pre
cur.return cur
5. 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_begin
andkth_from_end
tohead
. - Iterate the linked list with a pointer
temp
and count the number of node. Assignkth_from_begin
at the correct node. - Subtract
k
from the node count to get the position ofkth_from_end
. Iterate the linked list to assignkth_from_end
correctly. - Swap just the values and return the head.
One pass
- Iterate the linked list with a pointer
temp
and count the number of node. Assignkth_from_begin
at the correct node. - Initialize
kth_from_begin
to head. - Continue iterating with
temp
andkth_from_begin
at the same time. Astemp
hits the first node,kth_from_begin
is 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
= 0
count = kth_from_end = head
kth_from_begin = head
temp while temp:
if count == k - 1:
= temp
kth_from_begin = temp.next
temp += 1
count
# count will be 1 larger than the actual number of nodes
= count - k
node_from_end while node_from_end > 0:
-= 1
node_from_end = kth_from_end.next
kth_from_end = kth_from_end.val, kth_from_begin.val
kth_from_begin.val, kth_from_end.val return head
One 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
= 0
count = head
temp
while count < k - 1:
= temp.next
temp += 1
count
= temp, head
kth_from_begin, kth_from_end while temp.next:
= kth_from_end.next, temp.next
kth_from_end, temp
= kth_from_end.val, kth_from_begin.val
kth_from_begin.val, kth_from_end.val return head
6. 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
head
after 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]:
= head
group_prev = 2
curr_max_length
while group_prev.next:
= group_prev
curr = 0
count
for _ in range(curr_max_length):
if not curr.next:
break
+= 1
count = curr.next
curr
if count % 2: # Odd, do nothing
= curr
group_prev else: # Even, reverse
= curr.next
group_next = group_prev.next
curr
for _ in range(count):
# curr.next, curr, group_next = group_next, curr.next, curr
= curr.next
curr_next next = group_next
curr.= curr
group_next = curr_next
curr # group_prev.next.next, group_prev.next, group_prev = curr, group_next, group_prev.next
= group_prev.next
prev_next next = group_next
group_prev.= prev_next
group_prev += 1
curr_max_length return head
7. 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_pointer
from it. - Iterate the linked list and reverse each pair of nodes.
- Once
left_pointer
reaches 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
= ListNode(0, head)
dummy = dummy, dummy.next
prev_to_group, left_pointer while left_pointer and left_pointer.next:
= left_pointer.next
right_pointer = right_pointer.next
next_to_group next = right_pointer
prev_to_group.next = next_to_group
left_pointer.next = left_pointer
right_pointer.= left_pointer, left_pointer.next
prev_to_group, left_pointer return dummy.next
8. 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]':
= {None:None}
oldToNew
# 1st pass: construct nodes
= head
cur while cur:
= Node(cur.val)
oldToNew[cur] = cur.next
cur
# 2nd pass: construct links
for node in oldToNew:
if node is not None:
next = oldToNew[node.next]
oldToNew[node].= 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
, andnext
pointer.
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 theprev
andnext
pointers 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):
= self.tail.prev, self.tail
pre, nxt next = pre, nxt
node.prev, node.next = nxt.prev = node
pre.self.linkedlistsize += 1
def remove(self, node):
= node.prev, node.next
pre, nxt next, nxt.prev = nxt, pre
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:
= self.head.next
lru 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)