Linked Lists are one approach to producing a dynamically sized collection of elements. Array Lists accomplish this as well, but they do it differently.
In a Linked List, a collection of elements is represented as a chain of Nodes.
The most basic Node has two things:
- A value (also known as an element)
- A pointer to another Node
In other words, Linked Lists are lists that are composed of many Nodes each referencing the next.
Linked Lists are useful when you need a dynamically sized collection. Unlike a fixed-array, you can expand a Linked List by simple pointing its last Node to a new Node.
Linked Lists are also efficient when inserting nodes in the middle of a collection. We'll see why that is as we build out our LinkedList
implementation.
In the following releases do not use Ruby's built in data structures, including Arrays, Hashes or Sets in your implementation. The only structures you should use are objects from classes you define. You may also use the FixedArray class if you think you need to.
The elements (aka values) you store in your list may be any kind of object, the type of values you store in your list shouldn't affect the implementation.
Since Linked Lists are made up of Nodes, let's start by creating a Node
class.
Implement and write RSpec tests for the Node
class, supporting the following interface:
Node#new(element)
: Instantiate a new node containingelement
Node#insert_after(other_node)
: Insertother_node
after this node. In other words,other_node
becomes the node that this instance points to.Node#remove_after()
: Remove the node that this node points to (aka the node "after" this node)
Now that you've implemented and tested Node
let's build up our LinkedList
class.
Implement and write RSpec tests for the LinkedList
class, supporting the following interface.
LinkedList#new
: Instantiate a new linked listLinkedList#insert_first(element)
: Insert an element at the front of the listLinkedList#remove_first
: Remove the element at the front of the list ornil
if absentLinkedList#insert_last(element)
: Insert an element at the back of the listLinkedList#remove_last
: Remove the element at the back of the list ornil
if absent
Now you have a basic LinkedList
class implemented. Let's expand it to make it more useful as a collection.
Implement and write RSpec tests for the LinkedList
class, expanding to support the following interface.
LinkedList#get(index)
: Get the element in the list atindex
LinkedList#set(index, element)
: Set the element in the list atindex
LinkedList#insert(index, element)
: Insert the valueelement
in the List at positionindex
LinkedList#size
: Return the size of the list
By now you have the following methods on your LinkedList class:
LinkedList#new
LinkedList#insert_first(element)
LinkedList#remove_first(element)
LinkedList#insert_last(element)
LinkedList#remove_last(element)
LinkedList#get(index)
LinkedList#set(index, element)
LinkedList#insert(index, element)
LinkedList#size
For each of these methods, determine the big-O complexity of the method. Create a file complexity.md
and write the big-O for each method, explaining why.
For example, LinkedList#new
is O(1)
— whether our list ends up containing 0 elements or 1000, #new
will always take the same amount of time.
After you have figured out the big-O for each method, answer the following question in complexity.md
:
- Why is inserting a value into a LinkedList potentially faster than inserting a value into an ArrayList?
The questions posed in the stretches below might end up being interview questions. Focus on getting your cores done first, but consider returning to these stretches when you have time.
After the last release, you should have a sense of which methods in your Linked List implementation are fast and which are slow.
Let's revisit our implementation and speed it up.
In this release, ensure that remove_first
and remove_last
run in constant (O(1)
) time. Remember, that means the remove_*
methods should run just as fast whether it's a list of 2 nodes or 1000 nodes.
Think about what state the list needs to keep track of to accomplish this. What design trade offs must you make to achieve this result?
Since you've already written tests for these methods, ensure that they still run. You're using your tests as a safety net in this refactor.
Change the implementation of you class to ensure that the size
method runs in constant time as well. What did you have to do to make
What changes did you need to make to complete releases 4 and 5? What are the downsides to the change you made? Add your thoughts to complexity.md
.