A linked list is a linear colletion of elements in sequential order. Imagine a chain, with a start and a end, with the chain links connected to each other. A linked list can be visualized as below:
A linked list is composed of a smaller data structure usually called a Node. The node object contains the value of the element itself, and also a pointer to the next node in the chain. By utilizing node.next, you can traverse the list. The beginning of a linked list is denoted with head and the last element is tail. The 'tail' of a list may refer either to the rest of the list after the head, or to the last node in the list. We will refer to the last node as the tail, but it is worth remembering that some people, especially those fro mthe functional programming community, consider the rest of the list the tail.
In a singly-linked list, the link is established in one direction only, where the node only keeps track of the next node in the chain. This means that with a singly-linked list, you can only traverse in the direction of the tail. On the other hand, a node in a doubly-linked list keeps track of both next and previous which allows you to traverse in both directions (towards head and tail).
In this exercise, implement the following functions for the LinkedListNode and LinkedList classes:
LinkedListNodeconstructor()- Write a method that instantiates the node.
- The node takes
valueandnext.
LinkedListconstructor()- Write a method that instantiates the list.
- The instance variables
headandtailshould be set tonull.
prepend(value)- Write a method that inserts the
valueat the beginning of the linked list.
- Write a method that inserts the
append(value)- Write a method that inserts the
valueat the end of the linked list.
- Write a method that inserts the
delete(value)- Write a method that deletes the first node that contains the
valuein the linked list.
- Write a method that deletes the first node that contains the
find(value)- Write a method that returns the first
nodethat contains thevalue.
- Write a method that returns the first
insertAfter(value, targetValue)- Write a method that inserts the
nodeafter thenodethat contains thetargetValue.
- Write a method that inserts the
deleteHead()- Write a method that deletes the first element in the linked list.
deleteTail()- Write a method that deletes the last element in the linked list.
The most important operations are prepend/append for adding data, delete for removing data, and find for retrieving data.
Let's start by opening up a terminal window and running npm run watch. This will start our TDD framework that will automatically re-run the tests when you save any changes to your file.
- In your code editor, navigate to
/algorithms/src/data-structures/LinkedListNode.js. - In the constructor method, write
this.value = valueand save the changes. - You will see the tests run automatically in your terminal:
Jest Tip: If you need to filter by the exercise name, press
p. (presswfor the list of available commands).
The output tells you that three tests are failing:
- ✕ should create list node with value (10ms)
- ✕ should create list node with object as a value (2ms)
- ✕ should link nodes together
- ✓ should convert node to string
- ✓ should convert node to string with custom stringifier (1ms)
The code for the last two tests (toString() tests) have been provided for you in the skeleton. Your task is to make the three failing tests to pass.
- The test description says
› should create list node with value - The output tells us where the test is failing:
expect(node.next).toBeNull();. It also tells usexpect(received).toBeNull()andReceived: undefined. - In this case, the test is
expectingnode.nextto benull. However, the test receivedundefined. - Since
nextis undefined, let's update ourconstructor()inLinkedListNode.js:constructor(value, next = null) { this.value = value; this.next = next; } - When you save the file, you should see all your tests for
LinkedListNodepass! - Often, many tests can depend on the same part of the code. In this case, solving for one test case solved all others.
Now, let's open up LinkedList.js in our editor.
- A linked list should always have a
headand atail. - The test tells us:
expect(linkedList.head).toBeNull();. Let's create the instance variables:constructor() { this.head = null; this.tail = null; }
- The
prependmethod inserts the item at the beginning of the list. - Prepending an item is a simple operation:
- Create a new
Node. - Set the new
node's `next to be the current head node. - Update the
headto point at the new node.
- Create a new
- We also have to consider a case where this is the first item stored in the list. In that case, we also set the tail to the same node.
- The
appendmethod inserts the item at the end of the list. - Operations:
- Create a new
Node. - Set the tail's
nextto be the new node. - Update
tailto point at the new node.
- Create a new
- Again, take into consideration when this is the first/last item stored in the list.
-
The
findmethod returns the node with the target value. -
A node can be visited by utilizing a loop and
node.next. -
The following snippet traverses the entire list:
let currentNode = node; while (currentNode.next) { currentNode = node.next console.log(currentNode.value) } -
Think about how the above concept can be applied to find the target node.
A simple implementation of find would take a single argument, the value to be found. However, sometimes we want to find an object, and know only the value of ones of its slots. We might have a list of employee objects with slots name, address, and employee-id. Our find method should be able to return an employee object just by taking the id. We enable this by making the single argument to find and object with two slots, value: and callback:. If the value: slot is set, we use it, as normal. If the callback: slot if set, it must be a function that can be applied to the nodes of the list, and which returns an object. It should return true for the object that should be returned. This use of callbacks is very common in javascript, so is worth practising.
- The
insertAftermethod stores theinsertValueright after the node with thevalue. - Operation:
- Create a new
Node. - Find the node with the target value.
- Set the found node's
nextto the new node. - Set new node's
nextto the found node's `next. - If the node is inserted at the end, the tail needs to be set correctly.
- Create a new
- Utilize the previously written
findmethod to find the node.
- The
deletemethod removes the first node with the specified value. - Operations:
- Traverse the list and locate the node before the target node.
- Remove the target node that is placed after the found node.
- In a linked list, you don't have to delete the actual node.
- Deleting means removing the reference to that node from the list.
- The above diagram shows that all we have to do is set the target node's previous node's
nextto point at the target node'snext. - This means that we must locate the node right before the target node. Think about how you would be able to tell when the next node will be the target node.
- Also, take into consideration if the
headortailnode is the node to be deleted.
- This is the most complex operation in a linked list. Drawing the operation on paper can help you visualize the problem. Don't hesitate to reach out to #basic-algorithms channel on Slack if you are stuck.
- You might try to use
findto get to the node you want to delete. This does not work. Can you see why?
- You might try to use
- The
deleteHead/Tailmethods are utility methods for common operations. ThedeleteTail()function is slow. It needs to traverse the entire list.
Some of the operations on linked lists take a fixed, constant amount of instructions, no matter the length of the list. prepend, append, inserAfter, and deleteHead obviously just touch at most two nodes. Other functions may need to look at every node in the data structure. find and delete needs to look at every node if they are asked to find or delete a vlaue that is not there. The last function deleteTail needs to traverse the entire list, as it needs to set the tail to the second last node. As we can only go forwards, we need to travel down the entire list.
Find a loop in a list
Given a list, we can ask whether there is a loop in it. One obvious way to check is to remember each node that we visit in a hash table, and use this to see if there is a loop. There is another way that only requires a constant amount of extra storage (in fact, it just needs to remember two nodes). Floyd's cycle finding algorithm, as this is called, is not obvious, but it is a trick that many interviewers expect you to know. The idea is simple. You travel through the list at two speeds. One traversal goes two steps for each step the other takes. The fast traversal will eventually overtake the slow traversal if there is a loop. If there is no loop, the fast traversal will hit the end of the list.
Reverse a list in place
A trick that people expect you to know is how to reverse a list in place. When you traverse a list, from a given node, you can find the next node. Once you have two nodes in a row, you can find the third node, and make the second node point to the first. You can continue on, flipping the pointers backwards, so they point to the previous node, rather than the next. This is unwise in any production system, especially if there is concurrency, and is very bad coding practice anywhere. Despite this, interviewers love asking about it.
Palidromes
A palidrome is a word that reads the same backwards as forwards. "redivider" is a palidrome. If we store each letter in a different node in a singly linked list, the problem is, how can we tell if a list represents a palindrome. We can reverse the list, but there are ways to do this without making a new reversed copy of the list. First try to check for a palindrome with an extra reversed copy.
Now try to find the length of a list, then find the middle element, then reverse the second half of the list. You now have two lists, the original first half, and the reversed second half. If these two lists are equal, the list is a palindrome. You should re-reverse the second half to return the list to its original form.

