Tackle down most of linked list problems easily with basic concepts from functional programming.
Think Recursively
Linked List is Defined Recursively
A linked list is either:
- An empty list.
- A node that contains a value and a reference to a linked list.
The definition of linked list uses the definition itself, making it a recursive data structure.
Take a closer look at the single-linked list definition that you could see on every linked list problem in LeetCode. (C++ ver.)
1 | struct ListNode { |
int val;
is the value that a node contains.ListNode*
refers to a linked list that a node holds.nullptr
is the empty list.
Length As an Example
How would you get the length of a linked list?
Iteratively
- Start with a pointer points to the head and a counter set as 0.
- Make the pointer move along the list and increase the counter at the same time.
- When the pointer points to nullptr, we stop here and return the result.
1 | int Length(ListNode* head) { |
Iteratively, we consider the linked list as a whole. It is a sequence of nodes that we can be iterative on. The sequence is marked with head
as begin and next == nullptr
as the end.
1 | ┌────┐ ┌►┌────┐ ┌►┌────┐ ┌►┌────┐ ┌►┌───────┐ |
Recursively
To think recursively, we have to change our way of thinking. Take these three points in mind, and you could be a recursive guy.
- Stop considering the list as a sequence that you can iterate on. You should refrain from making the pointer move forward and backward in a loop.
- Stop thinking about the list as a whole. Narrow your focus to the current list. It could be either
- an empty list.
- a node with a value and the list it holds a reference to.
- While implementing a recursive function, assuming it works as expected when calling it recursively. Do not try to get to the bottom of the recursive stack to find out why the recursion works. Function is born to be a way of black-box abstraction.
Let us implement Length
recursively.
We first narrow our focus on the current node and the list it holds references to.
- If the current list is empty, its length is 0.
- If the current list is not empty, we should consider how to get the length from the current node and the list it holds the reference to.
- the current node contribute 1 to the length of the list
- We could get the length of the list it holds the reference to by calling
Length
recursively on it. At this point, the functionLength
has been not finished yet. However, persuade yourself into thinking that theLength
you are calling is a pretty pretty function that works just as you expected. - We add them together, and that is our result.
1 | int Length(ListNode* head) { |
Nice, we can think recursively now!
Build our Util Library
Basic Building Blocks
IMMUTABILITY is Not Our Concern!
Foldr
How do we get the length of our list recursively?
1 | int Length(ListNode* head) { |
How do we get the product of values in list nodes recursively?
1 | int Mul(ListNode* head) { |
Length
and Mul
are similar, aren’t they? To find the common part of them, let us rewrite them with Add1
and Mul2
to avoid using infix operators operator+
and operator*
.
1 | int Add1(ListNode*, int len) { |
(Notice that the first argument ListNode*
is an unused variable.)
1 | int Mul2(ListNode* x, int product) { |
They are similar because
Length
andMul
both have an initial value,0
and1
respectively, returned when the list is empty.- They both call the function with two parameters: the node itself and the recursive result of the rest of the list.
- They both return the result of the function.
The function calling trees for them are like
1 | add1(t0, |
1 | Mul2(t0, |
Let’s take the list [1,2,3,4,5]
as an example. we are computing the following
1 | (1 * (2 * (3 * (4 * (5 * 1))))) |
Let’s make it more general. Assume the value held in the list node has type T
. We have a function f
that accepts two parameters of type ListNode*
and U
and returns a value of type U
. Given an initial value i
of type U
, we want to compute
1 | f(t0, |
This operation on the list is always called Foldr
for “fold right.”
For Length
, i
is 0
. The function f
is add1
.
For Mul
, i
is 1
. The function f
is Mul2
.
Now, Let us implement Foldr
1
2
3
4
5
6
7
8template<typename Func, typename Init>
Init Foldr(Func func, Init init, ListNode* head) {
if(!head) {
return init;
}
return func(head, Foldr(func, init, head->next));
}
It is similar to Length
and Mul
, right? Now, we could rewrite Length
and Mul
with Foldr
.
1 | int Length(ListNode* head) { |
Foldl
Foldl
is similar to Foldr
. However, Foldl
benefits from recursion, which be optimized by most compilers.
Assume the value held in the list node has type T
. We have a function f
that accepts two parameters of type ListNode*
and U
and returns a value of type U
. Given an initial value i' of type
U,
Foldl` computes
1 | f( ... f(f(f(init, t0), t1), t2),t3) ... , t(N-1)) |
Another difference between Foldr
and Foldl
is using the initial value. Foldr
combines the initial value with the last element. However, Foldl` combines it with the first element of the list.
To make it safe to modify ListNode::next
while calling Foldl
, we should copy the head->next
before we call func
.
1 | template<typename Func, typename Init> |
SplitAt
SplitAt(n, head)
splits a list into two. All nodes before the n-index node go to the first half. The others go to the second half. For example:
1 | SplitAt(-1, [1,2,3,4,5]) --> ([] , [1,2,3,4,5]) |
Splitting at an index equal to zero or less than zero does not split anything. The original list will always go to the second half of the result. The first half of the result will be empty.
If we split an empty list, we get two empty lists in return.
Let’s split a non-empty list head
at index n > 0
recursively. Look what we have here!
head
, the nodehead->next
, the rest of the listSplitAt
, the function
The nth node in the current list is the (n-1)th node in the rest of the list. Take the following drawing as an example.
1 | head |
Therefore, we could call SplitAt(n - 1, head->next)
to split the rest of the list. The result of SplitAt(n - 1, head->next)
will be a pair of lists. Finally, we prepend head
on the first half of the list. Then, we get a recursive SplitAt
!
1 | using ListNodePair = std::pair<ListNode*, ListNode*>; |
Append
Append
an empty list onto another list does nothing.Append
a list onto an empty list does nothing.Append
a list with more than one node is append the currenthead
to the head of theAppend(head->next, r)
.
1 | ListNode* Append(ListNode* l, ListNode* r) { |
ZipWith
ZipWith
is like Foldr
but for folding two lists at the same time. The caller is responsible for deallocating the memory. Unlike most implementations, this ZipWith
continues on the longer list when the shorter list is depleted. When this is happening, the func will be called with the shorter list node filled with nullptr
.
1 | template<typename Func> |
Example:
1 | // list1 : [1,2,3,4] |
Utils
Length
We could build Length
with Foldl
.
1 | int Length(ListNode* head) { |
Reverse
We could reverse the list by prepending all nodes we visit in Foldl
to the initial value nullptr
.
1 | ListNode* Reverse(ListNode* head) { |
Why do we not prefer Foldr
? Foldr
visits all nodes in the list in reverse order, which means we have to Append
the node we visit at the end of the result. Remember, we are dealing with a linked list. Appending a node at the end of a singly-linked list requires traversing to the end of the list. Traversing the whole list is not fast.
1 | ListNode* Reverse(ListNode* head) { |
Filter
Just Foldr
. Built a new list with nodes of interest.
1 | template<typename UnaryFunction> |
Partition
Just Foldr
. Build a new pair of lists.
1 | using ListNodePair = std::pair<ListNode*, ListNode*>; |
Linked List Made Easy
Equip yourself with the library we have just built. Tackle down linked list challenging problems in no time.
Delete, Insert Nodes in a Linked List
LC2095 - Delete the Middle Node of a Linked List
LC2095 Delete the Middle Node of a Linked List
- Get the
length
of the list - Split the list at
length / 2
- Split the second half of previous result at
1
Append
them together
1 | ListNode* deleteMiddle(ListNode* head) { |
LC1669 - Merge In Between Linked Lists
SplitAt
twice, Append
twice
1 | ListNode* mergeInBetween(ListNode* list1, int a, int b, ListNode* list2) { |
Reverse a Linked List
LC25 - Reverse Nodes in k-Group
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.
Solving this problem is just translating the sentence into our util library’s language.
Reversing an empty linked list with whatever k provided will give us an empty linked list.
1 | ListNode* reverseKGroupRec(int k, ListNode* head) { |
If the number of nodes is not a multiple of k, then left-out nodes, in the end, should remain as it is.
This statement indicates that if the length of a list is less than k, it should remain untouched.
1 | ListNode* reverseKGroupRec(int k, ListNode* head) { |
reverse the nodes of the list k at a time
If the length of a list is no less than k, “k at a time” requires us to SplitAt
k.
1 | ListNode* reverseKGroupRec(int k, ListNode* head) { |
Then we Reverse
the firstHalf
.
1 | ListNode* reverseKGroupRec(int k, ListNode* head) { |
We do recursion on the secondHalf
and Append
the firstHalf
to the result of the recursion.
1 | ListNode* reverseKGroupRec(int k, ListNode* head) { |
Let’s refactor the code to eliminate some unnecessary assignments.
1 | ListNode* reverseKGroupRec(int k, ListNode* head) { |
Thanks to the Util Library and Recursion, this complicated problem has been solved with only three lines of code!
Reorder a Linked List
LC143 - Reorder List
You are given the head of a singly-linked list. The list can be represented as:
1 | L0 → L1 → … → Ln - 1 → Ln |
Reorder the list to be on the following form:
1 | L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → … |
Four easy steps.
- Get the length of the list.
- Split the list at the half.
- Reverse the second half of the list.
- Interweave the list.
1 | ListNode* Interweave(ListNode* l, ListNode* r) { |
Big Integer as Linked List
LC369 Plus One Linked List
Foldr
on the list with initial value1
.- If
Foldr
returns a1
, we have to create a new node as the new most significant digit.
1 | ListNode* plusOne(ListNode* head) { |
LC2 Add Two Numbers
ZipWith
two lists by add their values.- Carry each digits of the result of
ZipWith
, just like in LC369 Plus One Linked List
1 | int Carry(ListNode* head) { |
LC445 Add Two Numbers II
LC445 is almost the same as LC2 Add Two Numbers except that we have to reverse the two number lists at first.
1 | class Solution { |