Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
39: Implementing a Linked List in Ruby
A few years ago, I was reviewing a colleague's code for a task scheduler. They were using a standard Ruby Array to manage a queue of pending jobs, and whenever a high-priority job came in, they used unshift to push it to the front. It worked fine in development, but once we hit production with thousands of concurrent tasks, the performance tanked. The problem is that arrays are contiguous blocks of memory; every time you unshift, Ruby has to physically move every other element in the array one slot to the right. It's a silent killer of performance. I spent an afternoon showing them how to implement a Linked List, and the "aha!" moment happened when they realized we could just change one pointer instead of shifting ten thousand elements.
Building the Node Foundation
To build a linked list, you first have to stop thinking about a "list" as a single container. Instead, think of it as a series of independent objects—Nodes—that happen to know who their neighbor is. In Ruby, this is straightforward. We create a Node class that holds two things: the actual data (the value) and a reference to the next node in the chain.
class Node
attr_accessor :value, :next_node
def initialize(value)
@value = value
@next_node = nil
end
end
Notice that @next_node defaults to nil. This is crucial because the very last node in your list—the tail—has nowhere else to go. It's the "stop" sign for any loop traversing the list.
Orchestrating the List
Now we need a wrapper class to manage these nodes. The LinkedList class doesn't actually hold the data itself; it only needs to know where the list starts. We call this the head. If the head is nil, the list is empty. If you have a head, you have a thread to pull that leads you to every other piece of data in the sequence.
One of the biggest advantages here is how we handle insertions. To add an item to the front (the equivalent of unshift), we don't move existing data. We simply create a new node, tell it that its next_node is the current head, and then declare this new node as the new head.
class LinkedList
attr_reader :head
def initialize
@head = nil
end
def prepend(value)
new_node = Node.new(value)
new_node.next_node = @head
@head = new_node
end
def append(value)
new_node = Node.new(value)
if @head.nil?
@head = new_node
return
end
current = @head
current = current.next_node while current.next_node
current.next_node = new_node
end
end
I'll be honest: append is slower than prepend in a singly linked list because you have to walk the entire chain to find the end. If you find yourself appending constantly, you might eventually want to keep a reference to a @tail node to make it an O(1) operation, but for now, let's keep it simple.
Traversing the Chain
Since you can't access a linked list element by index (like list[5]), you have to iterate. You start at the head and follow the next_node pointers until you hit nil. This is a classic pattern in software engineering: the "while" loop traversal.
class LinkedList
# ... previous methods ...
def print_list
current = @head
while current
print "#{current.value} -> "
current = current.next_node
end
puts "nil"
end
end
# Usage:
list = LinkedList.new
list.append("Clean room")
list.append("Buy groceries")
list.prepend("Fix critical bug")
list.print_list
# Output: Fix critical bug -> Clean room -> Buy groceries -> nil
It feels a bit primitive compared to Ruby's powerful Array methods, but understanding this structure is vital. It's the foundation for more complex structures like Queues, Stacks, and Graphs. You're no longer relying on the language's magic; you're managing memory references yourself.
📋 Practical Task
Exercise: Build a Music Playlist Sequencer
Imagine you are building a simple music player. You need a way to manage a queue of songs where you can easily add a "Next Up" song to the end of the list or "Jump the Queue" by adding a song to the very front.
Your task:
Implement a Playlist class based on the Linked List logic learned in this lesson. Your class must include the following:
- A
Nodeclass to store the song title. - A
add_to_end(song_title)method to queue a song. - A
play_next(song_title)method that prepends a song to the front of the list. - A
show_queuemethod that prints the songs in the order they will be played, formatted as:"Playing: [Song A], Next: [Song B], Next: [Song C]..."
Test your implementation by adding three songs to the end and one song to the front, then printing the queue to verify the order.
There are no comments for now.