You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -35,10 +35,18 @@ In a singly linked list, each element or node is *connected* to the next one by
35
35
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.
36
36
37
37
.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[]
39
39
40
40
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.
41
41
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
+
42
50
[[doubly-linked-list]]
43
51
==== Doubly Linked List
44
52
@@ -51,19 +59,14 @@ With a doubly-linked list, you can move not only forward but also backward. If y
51
59
52
60
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.
53
61
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.*
61
62
62
63
==== Implementing a Linked List
63
64
64
65
We are going to implement a doubly linked list. First, let's start with the constructor.
65
66
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.
67
70
68
71
.Linked List's constructor
69
72
[source, javascript]
@@ -201,7 +204,7 @@ As you can see, when we want to remove the first node, we make the 2nd element (
201
204
202
205
===== Deleting element from the tail
203
206
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!
205
208
206
209
.Removing the last element from the list.
207
210
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
239
242
240
243
(((Tables, Linear DS, Array/Lists complexities)))
241
244
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.*
243
252
244
253
// tag::table[]
245
254
.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:
269
278
270
279
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.
271
280
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.
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.
0 commit comments