刷题总结 - Linked List
In this card, we are going to introduce another data structure - Linked List
.
Similar to the array, the linked list is also a linear
data structure. Here is an example:
As you can see, each element in the linked list is actually a separate object while all the objects are linked together by the reference field
in each element.
There are two types of linked list: singly linked
list and doubly linked
list. The example above is a singly linked list and here is an example of doubly linked list:
We will introduce more in later chapters. After this card, you will:
- Understand the structure of singly linked list and doubly linked list;
- Implement traversal, insertion, deletion in a singly or doubly linked list;
- Analyze the complexity of different operations in a singly or doubly linked list;
- Use two-pointer technique (fast-pointer-slow-pointer technique) in the linked list;
- Solve classic problems such as reverse a linked list;
- Analyze the complexity of the algorithms you designed;
- Accumulate experience in designing and debugging.
Introduction - Singly Linked List
Each node in a singly-linked list contains not only the value
but also a reference field
to link to the next node. By this way, the singly-linked list organizes all the nodes in a sequence.
Here is an example of a singly-linked list:
The blue arrows show how nodes in a singly linked list are combined together.
Node Strucutre
Here is the typical definition of a node in a singly-linked list:
1 | // Definition for singly-linked list. |
In most cases, we will use the head
node (the first node) to represent the whole list.
Operations
Unlike the array, we are not able to access a random element in a singly-linked list
in constant time. If we want to get the ith
element, we have to traverse from the head node one by one. It takes us time on average to visit an element by index, where N is the length of the linked list.
For instance, in the example above, the head is the node 23. The only way to visit the 3rd node is to use the next
field of the head node to get to the 2nd node (node 6); Then with the next
field of node 6, we are able to visit the 3rd node.
You might wonder why the linked list is useful though it has such a bad performance (compared to the array) in accessing data by index. We will introduce the insert and delete operations in next two articles and you will realize the benefit of the linked list.
After that, we provide an exercise for you to design your own singly linked list.
Add Operation - Singly Linked List
If we want to add a new value after a given node prev
, we should:
- Initialize a new node
cur
with the given value;
- Link the
next
field of cur to prev's next nodenext
;
- Link the
next
field inprev
tocur
.
Unlike an array, we don’t need to move all elements past the inserted element. Therefore, you can insert a new node into a linked list in time complexity, which is very efficient.
An Example
Let's insert a new value 9 after the second node 6.
We will first initialize a new node with value 9. Then link node 9 to node 15. Finally, link node 6 to node 9.
After insertion, our linked list will look like this:
Add a Node at the Beginning
As we know, we use the head
node head to represent the whole list.
So it is essential to update head
when adding a new node at the beginning of the list.
- Initialize a new node
cur
; - Link the new node to our original head node
head
. - Assign
cur
tohead
.
For example, let's add a new node 9 at the beginning of the list.
- We initialize a new node 9 and link node 9 to current head node 23.
- Assign node 9 to be our new head.
1 | public void push(int new_data) { |
What about adding a new node at the end of the list? Can we still use similar strategy?
Add a node after a given node
1 | public void insertAfter(Node prev_node, int new_data) { |
Add a node at the end
Since a Linked List
is typically represented by the head of it, we have to traverse the list till end and then change the next of last node to new node.
1 | public void append(int new_data) { |
Delete Operation - Singly Linked List
If we want to delete an existing node cur
from the singly linked list, we can do it in two steps:
- Find cur's previous node
prev
and its next nodenext
;
Link prev
to cur's next node next
.
In our first step, we need to find out prev
and next
. It is easy to find out next using the reference field of cur
. However, we have to traverse the linked list from the head node to find out prev which will take time on average, where is the length of the linked list. So the time complexity of deleting a node will be .
The space complexity is because we only need constant space to store our pointers.
1 | /* Given a key, deletes the first occurrence of key in linked list */ |
An Example
Let's try to delete node 6 from the singly linked list above.
-
Traverse the linked list from the head until we find the previous node
prev
which is node 23 -
Link
prev
(node 23) withnext
(node 15)
Node 6 is not in our singly linked list now.
Delete the First Node
If we want to delete the first node, the strategy will be a little different.
As we mentioned before, we use the head node head
to represent a linked list. Our head is the black node 23 in the example below.
If we want to delete the first node, we can simply assign the next node to head. That is to say, our head will be node 6 after deletion.
The linked list begins at the head node, so node 23 is no longer in our linked list.
What about deleting the last node? Can we still use similar strategy?
Question - Design Linked List
1 | class MyLinkedList { |
Design Singly Linked List - Solution
Two-Pointer in Linked List
Let's start with a classic problem:
Given a linked list, determine if it has a cycle in it.
You might have come up with the solution using the hash table
. But there is a more efficient solution using the two-pointer technique
. Try to think it over by yourself before reading the remaining content.
Imagine there are two runners with different speed. If they are running on a straight path, the fast runner will first arrive at the destination. However, if they are running on a circular track, the fast runner will catch up with the slow runner if they keep running.
-
If there is no cycle, the fast pointer will stop at the end of the linked list.
-
If there is a cycle, the fast pointer will eventually meet with the slow pointer.
So the only remaining problem is:
What should be the proper speed for the two pointers?
It is a safe choice to move the slow pointer one step at a time while moving the fast pointer two steps at a time. For each iteration, the fast pointer will move one extra step. If the length of the cycle is M, after M iterations, the fast pointer will definitely move one more cycle and catch up with the slow pointer.
What about other choices? Do they work? Would they be more efficient?
Question - Linked List Cycle
Given a linked list, determine if it has a cycle in it.
Follow up:
Can you solve it without using extra space?
1 | /** |
1 | public class Solution { |
Question - Linked List Cycle II
快慢指针解决成环问题
Given a linked list, return the node where the cycle begins. If there is no cycle, return null.
1 | /** |
Question - Intersection of Two Linked Lists
快慢指针解决相交问题
Write a program to find the node at which the intersection of two singly linked lists begins.
1 | A: a1 → a2 |
begin to intersect at node c1.
1 | /** |
Question - Remove Nth Node From End of List
快慢指针解决从末尾开始的问题
Given a linked list, remove the n-th node from the end of list and return its head.
1 | Given linked list: 1->2->3->4->5, and n = 2. |
1 | /** |
Summary - Two-Pointer in Linked List
Here we provide a template for you to solve the two-pointer problem in the linked list.
1 | // Initialize slow & fast pointers |
Tips
It is similar to what we have learned in an array. But it can be trickier and error-prone. There are several things you should pay attention:
- Always examine if the node is null before you call the next field.
Getting the next node of a null node will cause the null-pointer error. For example, before we run fast = fast.next.next
, we need to examine both fast and fast.next
is not null.
- Carefully define the end conditions of your loop.
Run several examples to make sure your end conditions will not result in an endless loop. And you have to take our first tip into consideration when you define your end conditions.
Complexity Analysis
It is easy to analyze the space complexity. If you only use pointers without any other extra space, the space complexity will be . However, it is more difficult to analyze the time complexity. In order to get the answer, we need to analyze how many times we will run our loop.
In our previous finding cycle example, let's assume that we move the faster pointer 2 steps each time and move the slower pointer 1 step each time.
- If there is no cycle, the fast pointer takes N/2 times to reach the end of the linked list, where N is the length of the linked list.
- If there is a cycle, the fast pointer needs M times to catch up the slower pointer, where M is the length of the cycle in the list.
Obviously, M <= N. So we will run the loop up to N times. And for each loop, we only need constant time. So, the time complexity of this algorithm is in total.
Analyze other problems by yourself to improve your analysis skill. Don't forget to take different conditions into consideration. If it is hard to analyze for all situations, consider the worst one.
Reverse Linked List
Let's start with a classic problem:
Reverse a singly linked list.
One solution is to iterate the nodes in original order and move them to the head of the list one by one. It seems hard to understand. We will first use an example to go through our algorithm.
Algorithm Overview
Let's look at an example:
Keep in mind that the black node 23 is our original head node.
- First, we move the next node of the black node, which is node 6, to the head of the list:
- Then we move the next node of the black node, which is node 15, to the head of the list:
- The next node of the black node now is null. So we stop and return our new head node 15.
In this algorithm, each node will be moved exactly once.
Therefore, the time complexity is , where N is the length of the linked list. We only use constant extra space so the space complexity is .
This problem is the foundation of many linked-list problems you might come across in your interview. If you are still stuck, our next article will talk more about the implementation details.
There are also many other solutions. You should be familiar with at least one solution and be able to implement it.
Question - Reverse Linked List
1 | /** |
1 | // 思路: |
Reverse Linked List - Solution
In this article, we will talk more about details of our algorithm to reverse the linked list.
In the solution we mentioned previously, there are two nodes which we should keep track of: the original head node and the new head node.
Therefore, we need to use two pointers in one linked list at the same time. One pointer head
always points at our original head node while another pointer curHead
always points at our newest head node.
Let's focus on a single step (the 2nd step in the previous article). Our goal is to move the next node of head, which is 15, to the head of the list.
- First, we use a temporary pointer
p
to indicate the next node of the head node. And link thenext
field of head to thenext
field ofp
.
- Then, we link the
next
field ofp
to thecurHead
.
- Now our linked list actually looks like the picture below. And we set
curHead
to bep
.
By this way, we successfully move node 15 to the head of the list. And we can repeat this process until the next node of head
is null
.
Question - Remove Linked List Elements
Remove all elements from a linked list of integers that have value val.
1 | Input: 1->2->6->3->4->5->6, val = 6 |
1 | /** |