Leetcode Topic Digest - Linked Lists I
Introduction
Linked lists problems are among the most popular types of problems we often meet in all kinds of interviews. Many low-level programs, such as the dynamic memory allocator in any CPUs, use linked lists as the critical part of their implementation. You can find more details in the malloclab@CSAPP from CMU. Understanding primary problem forms and a certain degree of variant handling ability is essential. In this blog, we will be going over the most commonly asked linked problems.
Remove Linked List Elements
203. Remove Linked List Elements
Problem Description
Given the head
of a linked list and an integer val
, remove all the linked list nodes with Node.val == val
, and return the new head.
Pre-coding thoughts
You may want to iterate over the linked list and do something to remove the node that equals the val
. So we are going to do this.
Coding Details
All the linked list-related problems assumes the following definition of ListNode
1 | struct ListNode { |
1 | struct ListNode* removeElements(struct ListNode* head, int val) { |
Comments
This is a fundamental linked list problem but clearly introduces the intuition behind linked list conditional removal. Also, since in C
language, we don’t have OOP, thus struct is more often used. You may feel confused about the struct definition since you only see struct ListNode
in the definition code; you don’t know how to call or initialize a ListNode. In fact, the struct definition has a clear rubric,
1 | struct [structure tag] { |
The ListNode
above is called a structure tag. You need to define a variable before you can conveniently call a struct variable. Thus, you will find the line above
1 | typedef struct ListNode ListNode; |
very useful. If you don’t add this line, you will need to add struct ListNode
before every initialization, which requires more repetitive work. For more details on defining a struct
in c, you can refer here to a well-stated tutorial.
Design Linked List
Problem Description
This problem is more complicated in implementation than in algorithm. It asks you to design a Linked List Class using an already-defined API.
Split the API
1 | int get(int index) |
Five parts are required for this design. get
is pretty simple and only requires linear time complexity. All you need to do is to loop through the linked list and compare the values to each element.
1 | int myLinkedListGet(MyLinkedList* obj, int index) { |
Since this is a singly linked list, addAtHead
requires only two steps. The first is initializing the node and then attaching the node to the front of the linked list. See one possible implementation below.
1 | void myLinkedListAddAtHead(MyLinkedList* obj, int val) { |
addAtTail
is similar. The first is initializing the new tail node, then attaching the node to the bottom of the linked list. However, there is one difference. You can not directly find the tail of a linked list. Thus you need to loop from the head
node til you hit NULL
.
1 | void myLinkedListAddAtTail(MyLinkedList* obj, int val) { |
addAtIndex
is like an insert operation in linked lists. It requires a search step and an insert step. The search step is of linear time complexity since, in the worst case, you will have to go through every node in the linked list. The insertion step requires constant time complexity. You should initialize the node with its value before insertion.
1 | void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) { |
deleteAtIndex
is the remove operation in the linked lists. It also requires a search step and a delete step. However, you should also consider the case you are asked to remove the head node of a linked list. This would mean you must remove all the nodes by free
them. See implementation for details.
1 | void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) { |
Comments
This problem helps you understand the common operations of a linked list. Its significance is more extensive for you to comprehend than to implement. However, it is a beneficial way to check your knowledge of linked lists in programming.
Reverse Linked List
Problem Description
Given the head
of a singly linked list, reverse the list, and return the reversed list.
Problem Analysis
We would love it if we could reverse our linked list in one traversal. A typical method that comes to my mind when trying to manipulate the order of the linked list is Two Pointers. In this case, we start with two pointers, prev
and curr
. Note that prev
should be initialized as NULL
, because your reversed linked list will finally expect a NULL closeup.
Code Implementation
1 | struct ListNode* reverseList(struct ListNode* head) { |
Comments
It may be a little tricky for you to access the last line of code. Why are we returning prev
instead of curr
? Well, if you look closer at the last iteration, you will see that curr
has been assigned the value of next
, which in this case is NULL
, while prev
comes to the correct place. This is labeled as an easy problem, but you still need to watch out for the details.
Swap Nodes in Pairs
Problem Description
This problem gives you a linked list, and you are asked to swap every pair nodes. See an example below.
Problem Analysis
First, we get the problem straight. It asks us to swap every neighboring pair, not every two adjacent nodes. That means you need to be aware that the step size is 2. It would be helpful to access this problem with spatial imagination. Imagine you are trying to swap two nodes in a linked list; how many steps will you take.
The answer is four steps. Assume a sample with four nodes, prev->pair_node1->pair_node2->next
.
- First, you set the
prev
next node topair_node2
. - Second, you set the
pair_node1
next tonext
- Third, you set the
pair_node2
next topair_node1
- Fourth, you update
prev
with the value ofpair_node1
(step size is 2)
The photo below from programmerCarl demonstrates how these four steps work. Refer here for more explanation in Chinese.
Problem Solution
1 | struct ListNode* swapPairs(struct ListNode* head) { |
Comments
This problem offers a good understanding of linked list manipulation. You will find similar problems easy to handle after you understand this problem. Next time you are asked to solve problems such as swapping nodes in threes, you will find it easy to access.
Remove Nth Node From End of List
19. Remove Nth Node From End of List
Problem Description
Given a linked list head
, you are asked to remove the $N^{th}$ node from the end of the linked list.
Problem Analysis
This problem’s key is locating the $N^{th}$ to the last node in the linked list. There are two ways to do this. The first way is the easy-to-think-of approach. You need two loops to find the node. In the first node, you obtain the linked list’s length and calculate the node’s position to remove. The second way is a classic two pointers application.
Solution
Implementation: Two Iterations
The time complexity of removing a node from a linked list is constant. But the way to obtain the length of the linked list and search for the node in the linked list are both O(n). The total time complexity is O(n).
1 | struct ListNode* removeNthFromEnd(struct ListNode* head, int n) { |
Although it takes two loops, the time complexity is still bounded to O(n). In fact the submission beats 100% in time.
Implementation: Two Pointers
Two-pointers is the more natural way to solve this problem. We use a fastPointer
to step one step in each iteration and a slowPointer
to step ahead only when their gap satisfies n
.
1 | struct ListNode* removeNthFromEnd(struct ListNode* head, int n) { |
Time complexity for searching the node is O(n), removing the node is constant.
Comments
If steps in problems ask you to locate an element or a node, two pointers should be your top choice.
Intersection of Two Linked Lists
02.07. Intersection of Two Linked Lists LCCI
Problem Description
Given two (singly) linked lists, determine if the two lists intersect. Return the intersecting node. Note that the intersection is defined based on reference, not value. That is, if the kth node of the first linked list is the same (by reference) as the jth node of the second linked list, then they are intersecting.
Problem Analysis
One thing in the description is very important. Note that the intersection is defined based on reference, not value. Which means if we look at the following example,
1 | Input: intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3 |
the intersection node is not 1
, instead, it’s 8
. I have read many solutions or explanations, but no one has clarified this detail. Note that there are skipA
and skipB
. They actually label skipA
quantity of nodes to the headA
as index 0
. Similarly, skipB
marks the skipB
quantity of nodes to the headB
as index 0
. Thus 1
is not even in the linked lists that we are comparing.
Problem Implementation
You may find stuck in your first access. I did too. But once you get the idea, everything will be sorted out. First, you need to find the gap between the two linked lists. You can fill the gap to make them aligned with each other. After that, a linear search with a comparison will give you the answer.
1 | struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) { |
Comments
This problem is about the gap-finding method and details of the implementation. It is an easy
problem, but you may find it interesting and challenging to play with.
Closeup
This blog is the first edition of the Linked List Problems analysis. There will be approximately the same number of problems in the next edition. Stay tuned!
Leetcode Topic Digest - Linked Lists I
https://realzza.github.io/leetcode/Leetcode-Topic-Digest-Linked-Lists-I/