DLinked
A fast, lightweight doubly linked list implementation for Ruby.
Features
-
Lightweight: Minimal memory footprint using optimized node class
-
Fast: O(1) operations for insertion/deletion at both ends
-
Ruby-native: Includes
Enumerablefor full integration with Ruby -
Bidirectional: Iterate forward or backward efficiently
-
LRU-Ready: Includes the specialized
DLinked::CacheListsubclass, which integrates a hash map for O(1) LRU cache management (access, insertion, eviction).
Installation
Add this line to your application’s Gemfile:
gem 'dlinked'
Or install it yourself:
gem install dlinked
Test
bundle exec rake test
Usage
1. Basic Initialization and O(1) Operations
Demonstrate creating the list, adding elements to both ends, and removing them quickly.
require 'dlinked'
# 1. Initialization
list = DLinked::List.new
list.size # => 0
# 2. O(1) Prepend (Add to Head)
list.prepend(20).prepend(10) # Using method chaining
# The list now looks like: [10, 20]
# 3. O(1) Append (Add to Tail)
list << 30
list.append(40)
# The list now looks like: [10, 20, 30, 40]
# 4. O(1) Removal
list.shift # => 10 (Removes from head)
list.pop # => 40 (Removes from tail)
# The list now looks like: [20, 30]
list.to_a # => [20, 30]
2. Array-Like Access and Assignment
Show users how the list behaves like a Ruby Array, utilizing the [] and []= methods you implemented.
list = DLinked::List.new
list << 'A' << 'B' << 'C' << 'D'
# 1. Access by Index
list[2] # => 'C'
list[-1] # => 'D' (Accessing from the tail)
# 2. Element Assignment (O(n) but familiar)
list[1] = 'B_NEW'
list.to_a # => ["A", "B_NEW", "C", "D"]
# 3. Slice/Range Access
list[1, 2].to_a # => ["B_NEW", "C"] (start at 1, length 2)
list[0..2].to_a # => ["A", "B_NEW", "C"] (using a Range)
# 4. Slice Replacement (Deletion and Insertion)
list[1, 2] = ['X', 'Y', 'Z'] # Replace two elements with three new elements
list.to_a # => ["A", "X", "Y", "Z", "D"]
list.size # => 5
3. Enumerable and Iteration
Demonstrate how it works seamlessly with standard Ruby collection methods.
list = DLinked::List.new
list << 10 << 20 << 30 << 40
# Standard Enumerable methods work out of the box
list.map { |n| n * 2 } # => [20, 40, 60, 80]
list.select(&:even?) # => [10, 20, 30, 40]
# O(n) Insertion in the middle
list.insert(2, 25)
list.to_a # => [10, 20, 25, 30, 40]
# O(n) Deletion by value
list.delete(20)
list.to_a # => [10, 25, 30, 40]
4. Utility, Inspection, and Conversion Methods
This section covers the basic checks, conversions, and advanced destructive operations.
list = DLinked::List.new
list << 10 << 20 << 30
# --- Basic Inspection ---
list.size # => 3
list.length # => 3 (alias for size)
list.empty? # => false
DLinked::List.new.empty? # => true
list.first # => 10 (O(1))
list.last # => 30 (O(1))
list.to_a # => [10, 20, 30]
list.to_s # => "[10, 20, 30]"
list.inspect # => "[10, 20, 30]"
# --- Lookup ---
list.index(20) # => 1
list.index(99) # => nil (Value not found)
5. Advanced Insertion, Deletion, and Slicing
Demonstrate non-O(1) operations that are useful for list manipulation, including the destructive slice! method.
list = DLinked::List.new
list << 'A' << 'B' << 'C' << 'D' << 'E'
# --- O(n) Insertion ---
# Insert at the middle (index 2)
list.insert(2, 'Z')
list.to_a # => ["A", "B", "Z", "C", "D", "E"]
list.insert(0, 'Start') # Same as prepend (O(1))
list.to_a # => ["Start", "A", "B", "Z", "C", "D", "E"]
# --- Deletion by Value ---
list.delete('Z') # => "Z" (Returns the deleted value)
list.to_a # => ["Start", "A", "B", "C", "D", "E"]
# --- Destructive Slicing (slice!) ---
# Extract and remove a slice of 2 elements, starting at index 1
removed_slice = list.slice!(1, 2)
list.to_a # => ["Start", "C", "D", "E"]
removed_slice.to_a # => ["A", "B"]
# Remove one element using a range (index 2)
list.slice!(2..2)
list.to_a # => ["Start", "C", "E"]
list.size # => 3
6. Concatenation and Arithmetic
Demonstrate how lists can be combined.
list1 = DLinked::List.new << 1 << 2
list2 = DLinked::List.new << 3 << 4
# Non-destructive concatenation (returns a new list)
new_list = list1 + list2
new_list.to_a # => [1, 2, 3, 4]
list1.to_a # => [1, 2] (list1 is unchanged)
# Destructive concatenation (modifies list1)
list1.concat(list2)
list1.to_a # => [1, 2, 3, 4]
7. DLinked::CacheList (LRU Cache Utility)
DLinked::CacheList is a specialized subclass of DLinked::List designed to be the backbone of a Least Recently Used (LRU) cache. It combines a doubly linked list with a hash map to provide O(1)time complexity for all critical LRU cache operations. All core key management methods have O(1) complexity: - #prepend_key(key, value) (Add as MRU) - #move_to_head_by_key(key) (Touch/Access) - #pop_key (Evict LRU) - #remove_by_key(key) (Remove)
-
Most Recently Used (MRU) items are at the head of the list.
-
Least Recently Used (LRU) items are at the tail of the list.
This makes it highly efficient for tracking key access order in a memory-limited cache.
require 'dlinked'
# 1. Initialization
lru_list = DLinked::CacheList.new
lru_list.size # => 0
# 2. Add keys to the cache (as MRU)
# In a real cache, the value might be the cached data itself.
# For key tracking, value can be the same as the key.
lru_list.prepend_key(:key1, :key1)
lru_list.prepend_key(:key2, :key2)
lru_list.prepend_key(:key3, :key3)
# List order (MRU to LRU): [:key3, :key2, :key1]
lru_list.to_a # => [:key3, :key2, :key1]
# 3. "Touch" an existing key, moving it to the head (MRU)
lru_list.move_to_head_by_key(:key1)
# List order is now: [:key1, :key3, :key2]
lru_list.to_a # => [:key1, :key3, :key2]
# 4. Evict the least recently used key (from the tail)
evicted_key = lru_list.pop_key
evicted_key # => :key2
# List order is now: [:key1, :key3]
lru_list.to_a # => [:key1, :key3]
# 5. Remove a specific key (O(1) operation)
lru_list.remove_by_key(:key3)
lru_list.to_a # => [:key1]
# 6. Clear the list and the key map (O(1) operation)
lru_list.clear
lru_list.size # => 0
⚡ Performance Characteristics
This library is designed to offer the guaranteed performance benefits of a Doubly Linked List over a standard Ruby Array for certain operations.
| Operation | Method(s) | Complexity | Notes | | - | - | - | - | | End Insertion | append, prepend, push, unshift, << | $O(1)$ | Constant time, regardless of list size. | | End Deletion | pop, shift | $O(1)$ | Constant time. | | End Access | first, last | $O(1)$ | Constant time access to boundary values. | | Middle Insertion/Deletion | insert, delete (by value), slice!, []= (slice) | $O(n)$ | Requires traversal to find the node. | | Random Access/Search | [] (getter), index | $O(n)$ | Requires traversal; average time is $O(n/2)$. |
💾 Memory Usage
While memory usage is highly dependent on the objects stored, the overhead of the list structure itself is minimal and highly efficient:
-
Node Overhead: Each node in the list uses approximately 40 bytes. This includes the object header, and three necessary pointers:
-
Pointer to the stored value
-
Pointer to the next node
-
Pointer to the previous node
-
-
Efficiency Advantage: This structured overhead is often more memory-efficient than a large Ruby
Arraythat requires constant reallocation and copying when its capacity is exceeded, especially if theArrayis being modified frequently at the head.
License
MIT License. See LICENSE.txt for details.