Skip to content

Commit 09767c0

Browse files
committed
feat(book/linkedlist): linked lists techniques and common patterns
1 parent 0f13f90 commit 09767c0

16 files changed

+333
-13
lines changed

book/content/part02/linked-list.asc

Lines changed: 216 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,18 @@ In a singly linked list, each element or node is *connected* to the next one by
3535
Usually, a Linked List is referenced by the first element called *head* (or *root* node). Let's say that we have a list of strings with the following values: `"art" -> "dog" -> "cat"`. It would look something like the following image.
3636

3737
.Singly Linked List Representation: each node has a reference (blue arrow) to the next one.
38-
image::image19.png[image,width=498,height=97]
38+
image::sllx4.png[]
3939

4040
If you want to get the `cat` element from the example above, then the only way to get there is by using the `next` field on the head node. You would get `art` first, then use the next field recursively until you eventually get the `cat` element.
4141

42+
==== Circular Linked Lists
43+
44+
Circular linked lists happen when the last node points to any node on the list, creating a loop. In the following illustration, you can see two circular linked lists.
45+
46+
image:cll.png[Circular linked lists examples]
47+
48+
One circular linked list happens when the last element points to the first element. Another kind of circular linked list is when the last node points to any node in the middle. There are some efficient algorithms to detect when the list has a loop or not. More on that later in this chapter.
49+
4250
[[doubly-linked-list]]
4351
==== Doubly Linked List
4452

@@ -51,19 +59,14 @@ With a doubly-linked list, you can move not only forward but also backward. If y
5159

5260
Finding an item on the linked list takes O(n) time. Because in the worst-case, you will have to iterate over the whole list.
5361

54-
==== Linked List vs. Array
55-
56-
Arrays give you instant access to data anywhere in the collection using an index. However, Linked List visits nodes in sequential order. In the worst-case scenario, it takes _O(n)_ to get an element from a Linked List. You might be wondering: Isn’t an array always more efficient with _O(1)_ access time? It depends.
57-
58-
We also have to understand the space complexity to see the trade-offs between arrays and linked lists. An array pre-allocates contiguous blocks of memory. If the array fillup, it has to create a larger array (usually 2x) and copy all the elements when it is getting full. That takes _O(n)_ to copy all the items over. On the other hand, LinkedList’s nodes only reserve precisely the amount of memory they need. They don’t have to be next to each other in RAM, nor are large chunks of memory is booked beforehand like arrays. Linked List is more on a "grow as you go" basis. *Linked list wins on memory usage over an array.*
59-
60-
Another difference is that adding/deleting at the beginning of an array takes `O(n)`; however, the linked list is a constant operation `O(1)` as we will implement later. *Linked List has better runtime than an array for inserting items at the beginning.*
6162

6263
==== Implementing a Linked List
6364

6465
We are going to implement a doubly linked list. First, let's start with the constructor.
6566

66-
The only must-have field on the constructor is the `first` or head reference. If you want to insert it to the back of the list in constant time, then `last` pointer is needed. Everything else is complimentary.
67+
TIP: if you want to implement a singly linked list instead, it's the same in most parts, but without the setting the `previous` pointers.
68+
69+
The only must-have field on the constructor is the `first` or head reference. If you want to insert data to the back of the list in constant time, then the `last` pointer is needed. Everything else is complimentary.
6770

6871
.Linked List's constructor
6972
[source, javascript]
@@ -201,7 +204,7 @@ As you can see, when we want to remove the first node, we make the 2nd element (
201204

202205
===== Deleting element from the tail
203206

204-
Removing the last element from the list would require iterate from the head until we find the last one, that’s `O(n)`. But, since we referenced the last element, we can do it in _O(1)_ instead!
207+
Removing the last element from the list would require iterate from the head until we find the last one: `O(n)`. But, since we referenced the last element, we can do it in _O(1)_ instead!
205208

206209
.Removing the last element from the list.
207210
image::dll-remove-last.png[]
@@ -239,7 +242,13 @@ Notice that we are using the `get` method to get the node at the current positio
239242

240243
(((Tables, Linear DS, Array/Lists complexities)))
241244

242-
==== Linked List Complexity vs. Array Complexity
245+
==== Linked List vs. Array
246+
247+
Arrays give you instant access to data anywhere in the collection using an index. However, Linked List visits nodes in sequential order. In the worst-case scenario, it takes _O(n)_ to get an element from a Linked List. You might be wondering: Isn’t an array always more efficient with _O(1)_ access time? It depends.
248+
249+
We also have to understand the space complexity to see the trade-offs between arrays and linked lists. An array pre-allocates contiguous blocks of memory. If the array fillup, it has to create a larger array (usually 2x) and copy all the elements when it is getting full. That takes _O(n)_ to copy all the items over. On the other hand, LinkedList’s nodes only reserve precisely the amount of memory they need. They don’t have to be next to each other in RAM, nor are large chunks of memory is booked beforehand like arrays. Linked List is more on a "grow as you go" basis. *Linked list wins on memory usage over an array.*
250+
251+
Another difference is that adding/deleting at the beginning of an array takes `O(n)`; however, the linked list is a constant operation `O(1)` as we will implement later. *Linked List has better runtime than an array for inserting items at the beginning.*
243252

244253
// tag::table[]
245254
.Big O cheat sheet for Linked List and Array
@@ -269,6 +278,202 @@ Comparing an array with a doubly-linked list, both have different use cases:
269278

270279
For the next two linear data structures <<part02-linear-data-structures#stack>> and <<part02-linear-data-structures#queue>>, we are going to use a doubly-linked list to implement them. We could use an array as well, but since inserting/deleting from the start performs better with linked-lists, we will use that.
271280

281+
==== Linked List patterns for Interview Questions
282+
283+
Most linked list problems are solved using 1 to 3 pointers. Sometimes we move them in tandem or individually.
284+
285+
.Examples of problems that can be solved using multiple pointers:
286+
- Detecting if the linked list is circular (has a loop).
287+
- Finding the middle node of a linked list in 1-pass without any auxiliary data structure.
288+
- Reversing the linked list in 1-pass without any auxiliary data structure. e.g. `1->2->3` to `3->2->1`.
289+
290+
Let's do some examples!
291+
292+
===== Fast/Slow Pointers
293+
294+
One standard algorithm to detect loops in a linked list is fast/slow runner pointers (a.k.a The Tortoise 🐢 and the Hare🐇 or Floyd’s Algorithm). The slow pointer moves one node per iteration, while the fast pointer moves two nodes every time. You can see an example code below:
295+
296+
.Fast/Slow pointers
297+
[source, javascript]
298+
----
299+
let fast = head, slow = head;
300+
while (fast && fast.next) {
301+
slow = slow.next; // slow moves 1 by 1.
302+
fast = fast.next.next; // slow moves 2 by 2.
303+
}
304+
----
305+
306+
If the list has a loop, then at some point, both pointers will point to the same node. Take a look at the following image; take notice that both points to `node I` on the 8th iteration.
307+
308+
image:cll-fast-slow-pointers.png[fast/slow pointer in a circular linked list]
309+
310+
.You can detect the intersection point (`node D` on the example) by using this algorithm:
311+
- When `fast` and `slow` are the same, then create a (3rd) new pointer from the start.
312+
- Keep moving the 3rd pointer and the `slow` simultaneously one by one.
313+
- Where slow and 3rd pointer meets, that's the beginning of the loop or intersection (e.g., `node D`).
314+
315+
Fast/slow pointer has essential properties, even if the list doesn't have a loop!
316+
317+
If you don't have a loop, then fast and slow will never meet. However, by the time the `fast` pointer reaches the end, the `slow` pointer would be precisely in the middle!
318+
319+
image:sll-fast-slow-pointers.png[fast/slow pointer in a singly linked list]
320+
321+
This technique is useful for getting the middle element of a singly list in one pass without using any auxiliary data structure (like array or map).
322+
323+
324+
*LL-A*) _Find out if a linked list has a cycle and, if so, return the intersection node (where the cycle begins)._
325+
326+
.Signature
327+
[source, javascript]
328+
----
329+
/**
330+
* Find the node where the cycle begins or null.
331+
* @param {Node} head
332+
* @returns {Node|null}
333+
*/
334+
function findCycleStart(head) {
335+
336+
};
337+
----
338+
339+
.Examples
340+
[source, javascript]
341+
----
342+
findCycleStart(1 -> 2 -> 3); // null // no loops
343+
findCycleStart(1 -> 2 -> 3 -> *1); // 1 // node 3 loops back to 1
344+
findCycleStart(1 -> 2 -> 3 -> *2); // 2 // node 3 loops back to 2
345+
----
346+
347+
*Solution*
348+
349+
One solution is to find a loop using a HashMap (`Map`) or HashSet (`Set`) to track the visited nodes. If we found a node that is already on `Set`, then that's where the loop starts.
350+
351+
.Solution 1: Map/Set for detecting loop
352+
[source, javascript]
353+
----
354+
include::../../interview-questions/linkedlist-find-cycle-start.js[tag=brute]
355+
----
356+
357+
.Complexity Analysis
358+
- Time Complexity: `O(n)`. We might visit all nodes on the list (e.g., no loops).
359+
- Space complexity: `O(n)`. In the worst-case (no loop), we store all the nodes on the Set.
360+
361+
Can we improve anything here? We can solve this problem without using any auxiliary data structure using the fast/slow pointer.
362+
363+
.Solution 2: Fast/Slow pointer
364+
[source, javascript]
365+
----
366+
include::../../interview-questions/linkedlist-find-cycle-start.js[tag=fn]
367+
----
368+
369+
370+
.Complexity Analysis
371+
- Time Complexity: `O(n)`. In the worst case (no loop), we visit every node.
372+
- Space complexity: `O(1)`. We didn't use any auxiliary data structure.
373+
374+
375+
===== Multiple Pointers
376+
377+
378+
*LL-B*) _Determine if a singly linked list is a palindrome. A palindrome is a sequence that reads the same backward as forward._
379+
380+
.Signature
381+
[source, javascript]
382+
----
383+
/**
384+
include::{codedir}/data-structures/linked-lists/node.js[tag=singly, indent=2]
385+
*/
386+
387+
/**
388+
* Determine if a list is a palindrome
389+
* @param {Node} head
390+
* @returns {boolean}
391+
*/
392+
function isPalindrome(head) {
393+
// you code goes here!
394+
}
395+
----
396+
397+
.Examples
398+
[source, javascript]
399+
----
400+
const toList = (arr) => new LinkedList(arr).first;
401+
isPalindrome(toList([1, 2, 3])); // false
402+
isPalindrome(toList([1, 2, 3, 2, 1])); // true
403+
isPalindrome(toList([1, 1, 2, 1])); // false
404+
isPalindrome(toList([1, 2, 2, 1])); // true
405+
----
406+
407+
*Solution*
408+
409+
To solve this problem, we have to check if the first and last node has the same value. Then we check if the second node and second last are the same, and so on. If we found any that's not equal; then it's not a palindrome. We can use two pointers, one at the start and the other at the end, and move them until they meet in the middle.
410+
411+
The issue is that with a singly linked list, we can't move backward! We could either convert it into a doubly-linked list (with the last pointer) or copy the nodes into an array. Let's do the latter as a first approach.
412+
413+
.Solution 1: List to array
414+
[source, javascript]
415+
----
416+
function isPalindrome(head) {
417+
const arr = [];
418+
for (let i = head; i; i = i.next) arr.push(i.value);
419+
let lo = 0, hi = arr.length - 1;
420+
while (lo < hi) if (arr[lo++] !== arr[hi++]) return false;
421+
return true;
422+
}
423+
----
424+
425+
What's the time complexity?
426+
427+
.Complexity Analysis
428+
- Time Complexity: `O(n)`. We do two passes, one on the for-loop and the other in the array.
429+
- Space complexity: `O(n)`. We are using auxiliary storage with the array O(n).
430+
431+
That's not bad, but can we do it without using any auxiliary data structure, O(1) space?
432+
433+
.Here's another algorithm to solve this problem in O(1) space:
434+
- Find the middle node of the list (using fast/slow pointers).
435+
- Reverse the list from the middle to the end.
436+
- Have two new pointers, one at the beginning of the list and the other at the head of the reversed list.
437+
- If all nodes have the same value, then we have a palindrome, otherwise, we don't.
438+
439+
.Solution 2: Reverse half of the list
440+
[source, javascript]
441+
----
442+
include::../../interview-questions/linkedlist-is-palindrome.js[tag=fn]
443+
----
444+
445+
This solution is a little longer, but it's more space-efficient since it doesn't use any auxiliary data structure to hold the nodes.
446+
447+
.Complexity Analysis
448+
- Time Complexity: `O(n)`. We visit every node once.
449+
- Space complexity: `O(1)`. We didn't use any auxiliary data structure. We changed data in-place.
450+
451+
452+
// https://leetcode.com/problems/remove-nth-node-from-end-of-list/
453+
454+
// https://leetcode.com/problems/palindrome-linked-list/solution/
455+
456+
457+
===== Reverse a Linked List
458+
459+
WIP
460+
// https://leetcode.com/problems/reverse-linked-list/
461+
462+
// https://leetcode.com/problems/add-two-numbers-ii/
463+
464+
===== Multi-level Linked Lists
465+
466+
WIP
467+
// https://leetcode.com/problems/flatten-a-multilevel-doubly-linked-list/
468+
469+
===== Intersections of linked lists
470+
471+
WIP
472+
// https://leetcode.com/problems/intersection-of-two-linked-lists/
473+
474+
// https://leetcode.com/problems/linked-list-cycle/
475+
476+
272477
==== Practice Questions
273478
(((Interview Questions, Linked Lists)))
274479

book/images/Find-the-largest-sum.png

-418 Bytes
Loading
Loading

book/images/Words-Permutations.png

4.2 KB
Loading
137 KB
Loading

book/images/cll.png

70.5 KB
Loading
-3.69 KB
Loading
-1.42 KB
Loading
-1.38 KB
Loading
23.5 KB
Loading

book/images/sllx4.png

5.28 KB
Loading
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// tag::fn[]
2+
/**
3+
* Find where the cycle starts or null if no loop.
4+
* @param {Node} head - The head of the list
5+
* @returns {Node|null}
6+
*/
7+
function findCycleStart(head) {
8+
let slow = head;
9+
let fast = head;
10+
while (fast && fast.next) {
11+
slow = slow.next; // slow moves 1 by 1.
12+
fast = fast.next.next; // slow moves 2 by 2.
13+
if (fast === slow) { // detects loop!
14+
slow = head; // reset pointer to begining.
15+
while (slow !== fast) { // find intersection
16+
slow = slow.next;
17+
fast = fast.next; // move both pointers one by one this time.
18+
}
19+
return slow; // return where the loop starts
20+
}
21+
}
22+
return null; // not found.
23+
}
24+
// end::fn[]
25+
26+
// tag::brute[]
27+
function findCycleStartBrute(head) {
28+
const visited = new Set();
29+
let curr = head;
30+
while (curr) {
31+
if (visited.has(curr)) return curr;
32+
visited.add(curr);
33+
curr = curr.next;
34+
}
35+
return null;
36+
}
37+
// end::brute[]
38+
39+
module.exports = { findCycleStart, findCycleStartBrute };
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const { findCycleStart, findCycleStartBrute } = require('./linkedlist-find-cycle-start');
2+
const { LinkedList } = require('../../src/index');
3+
4+
[findCycleStart, findCycleStartBrute].forEach((fn) => {
5+
describe(`findCycleStart: ${fn.name}`, () => {
6+
it('should work without loop', () => {
7+
const head = new LinkedList([1, 2, 3]).first;
8+
expect(fn(head)).toEqual(null);
9+
});
10+
11+
it('should work with loop on first', () => {
12+
const list = new LinkedList([1, 2, 3]);
13+
const n1 = list.first;
14+
list.last.next = n1;
15+
expect(fn(list.first)).toEqual(n1);
16+
});
17+
18+
it('should work with loop on second', () => {
19+
const list = new LinkedList([1, 2, 3]);
20+
const n2 = list.first.next;
21+
list.last.next = n2;
22+
expect(fn(list.first)).toEqual(n2);
23+
});
24+
});
25+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// tag::fn[]
2+
function isPalindrome(head) {
3+
let slow = head;
4+
let fast = head;
5+
while (fast) { // use slow/fast pointers to find the middle.
6+
slow = slow.next;
7+
fast = fast.next && fast.next.next;
8+
}
9+
10+
const reverseList = (node) => { // use 3 pointers to reverse a linked list
11+
let prev = null;
12+
let curr = node;
13+
while (curr) {
14+
const { next } = curr;
15+
curr.next = prev;
16+
prev = curr;
17+
curr = next;
18+
}
19+
return prev;
20+
};
21+
22+
const reversed = reverseList(slow); // head of the reversed half
23+
for (let i = reversed, j = head; i; i = i.next, j = j.next) if (i.value !== j.value) return false;
24+
return true;
25+
}
26+
// end::fn[]
27+
28+
module.exports = { isPalindrome };
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const { isPalindrome } = require('./linkedlist-is-palindrome');
2+
const { LinkedList } = require('../../src');
3+
4+
const toList = (arr) => new LinkedList(arr).first;
5+
6+
[isPalindrome].forEach((fn) => {
7+
describe(`isPalindrome: ${fn.name}`, () => {
8+
it('should work', () => {
9+
expect(fn()).toEqual(true);
10+
});
11+
12+
it('should work different cases', () => {
13+
expect(fn(toList([1, 2, 3]))).toEqual(false);
14+
expect(fn(toList([1, 2, 3, 2, 1]))).toEqual(true);
15+
expect(fn(toList([1, 1, 2, 1]))).toEqual(false);
16+
expect(fn(toList([1, 2, 2, 1]))).toEqual(true);
17+
});
18+
});
19+
});

0 commit comments

Comments
 (0)