dlinked 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- data/README.md +55 -3
- data/lib/d_linked/cache_list.rb +182 -0
- data/lib/d_linked/list.rb +25 -13
- data/lib/d_linked/version.rb +1 -1
- data/lib/dlinked.rb +6 -3
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de491a4898093da5aac0b68079ebc40d001a60234ae752ec39250fa3d56d09d2
|
|
4
|
+
data.tar.gz: cdaf5870bba85269d949a0471a8f4481ad1786a18cf1e1f14159439b5d6235f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57666858cfbccc763cc732022f017fe3739292ec72b58c4c87d699bb65ea25154c5ba04458346fed4fdf53c7fa522801e2ba1c29e8bc53ea880d6d4f31109cea
|
|
7
|
+
data.tar.gz: 2ba4f2c83fff58be2e103968482f402ac8d6c6ce1f60ca5b4256e2e9337b30248afb1de181227b2a102872307d6ea60e4faaa3d30254942b65b9bf9bb6cb92da
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -6,9 +6,9 @@ A fast, lightweight doubly linked list implementation for Ruby.
|
|
|
6
6
|
|
|
7
7
|
- **Lightweight**: Minimal memory footprint using optimized node class
|
|
8
8
|
- **Fast**: O(1) operations for insertion/deletion at both ends
|
|
9
|
-
- **Ruby-native**: Includes Enumerable for full integration with Ruby
|
|
9
|
+
- **Ruby-native**: Includes `Enumerable` for full integration with Ruby
|
|
10
10
|
- **Bidirectional**: Iterate forward or backward efficiently
|
|
11
|
-
|
|
11
|
+
- **LRU-Ready**: Includes the specialized `DLinked::CacheList` subclass, which integrates a hash map for O(1) LRU cache management (access, insertion, eviction).
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
@@ -27,7 +27,7 @@ gem install dlinked
|
|
|
27
27
|
## Test
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
bundle exec
|
|
30
|
+
bundle exec rake test
|
|
31
31
|
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -189,6 +189,58 @@ list1.concat(list2)
|
|
|
189
189
|
list1.to_a # => [1, 2, 3, 4]
|
|
190
190
|
```
|
|
191
191
|
|
|
192
|
+
|
|
193
|
+
### 7. DLinked::CacheList (LRU Cache Utility)
|
|
194
|
+
`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:
|
|
195
|
+
- #prepend_key(key, value) (Add as MRU)
|
|
196
|
+
- #move_to_head_by_key(key) (Touch/Access)
|
|
197
|
+
- #pop_key (Evict LRU)
|
|
198
|
+
- #remove_by_key(key) (Remove)
|
|
199
|
+
|
|
200
|
+
- **Most Recently Used (MRU)** items are at the **head** of the list.
|
|
201
|
+
- **Least Recently Used (LRU)** items are at the **tail** of the list.
|
|
202
|
+
|
|
203
|
+
This makes it highly efficient for tracking key access order in a memory-limited cache.
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
require 'dlinked'
|
|
207
|
+
|
|
208
|
+
# 1. Initialization
|
|
209
|
+
lru_list = DLinked::CacheList.new
|
|
210
|
+
lru_list.size # => 0
|
|
211
|
+
|
|
212
|
+
# 2. Add keys to the cache (as MRU)
|
|
213
|
+
# In a real cache, the value might be the cached data itself.
|
|
214
|
+
# For key tracking, value can be the same as the key.
|
|
215
|
+
lru_list.prepend_key(:key1, :key1)
|
|
216
|
+
lru_list.prepend_key(:key2, :key2)
|
|
217
|
+
lru_list.prepend_key(:key3, :key3)
|
|
218
|
+
|
|
219
|
+
# List order (MRU to LRU): [:key3, :key2, :key1]
|
|
220
|
+
lru_list.to_a # => [:key3, :key2, :key1]
|
|
221
|
+
|
|
222
|
+
# 3. "Touch" an existing key, moving it to the head (MRU)
|
|
223
|
+
lru_list.move_to_head_by_key(:key1)
|
|
224
|
+
|
|
225
|
+
# List order is now: [:key1, :key3, :key2]
|
|
226
|
+
lru_list.to_a # => [:key1, :key3, :key2]
|
|
227
|
+
|
|
228
|
+
# 4. Evict the least recently used key (from the tail)
|
|
229
|
+
evicted_key = lru_list.pop_key
|
|
230
|
+
evicted_key # => :key2
|
|
231
|
+
|
|
232
|
+
# List order is now: [:key1, :key3]
|
|
233
|
+
lru_list.to_a # => [:key1, :key3]
|
|
234
|
+
|
|
235
|
+
# 5. Remove a specific key (O(1) operation)
|
|
236
|
+
lru_list.remove_by_key(:key3)
|
|
237
|
+
lru_list.to_a # => [:key1]
|
|
238
|
+
|
|
239
|
+
# 6. Clear the list and the key map (O(1) operation)
|
|
240
|
+
lru_list.clear
|
|
241
|
+
lru_list.size # => 0
|
|
242
|
+
|
|
243
|
+
```
|
|
192
244
|
## ⚡ Performance Characteristics
|
|
193
245
|
This library is designed to offer the guaranteed performance benefits of a Doubly Linked List over a standard Ruby `Array` for certain operations.
|
|
194
246
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DLinked
|
|
4
|
+
# DLinked::CacheList
|
|
5
|
+
#
|
|
6
|
+
# A specialized doubly linked list subclass used as the central key management
|
|
7
|
+
# component for an in-memory LRU cache.
|
|
8
|
+
#
|
|
9
|
+
# It integrates a hash map (`@lru_nodes`) to provide **O(1)** operations for:
|
|
10
|
+
# - Prepending/Adding a new key (MRU).
|
|
11
|
+
# - Touching/Moving an existing key to the head (MRU).
|
|
12
|
+
# - Evicting the least recently used key (LRU).
|
|
13
|
+
# - Removing an item by key.
|
|
14
|
+
#
|
|
15
|
+
# It maintains the standard list behavior of the superclass {DLinked::List}
|
|
16
|
+
# but focuses on key-based operations rather than simple value manipulation.
|
|
17
|
+
class CacheList < DLinked::List
|
|
18
|
+
class Node < DLinked::List::Node
|
|
19
|
+
# Add an attribute to hold the cache key
|
|
20
|
+
attr_accessor :key
|
|
21
|
+
|
|
22
|
+
# IMPORTANT: Accept the required three arguments from the base List#prepend,
|
|
23
|
+
# but only initialize the base class with value, prev, and next.
|
|
24
|
+
def initialize(value, prev = nil, next_node = nil, key = nil)
|
|
25
|
+
# Call the parent Node initializer using the first three arguments
|
|
26
|
+
super(value, prev, next_node)
|
|
27
|
+
|
|
28
|
+
# Initialize the new, specialized attribute
|
|
29
|
+
@key = key
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
# @!attribute [r] lru_nodes
|
|
33
|
+
# @return [Hash] The internal hash map storing {key => DLinked::CacheList::Node} references.
|
|
34
|
+
attr_reader :lru_nodes
|
|
35
|
+
# # --- 1. OVERRIDE THE CONSTANT ---
|
|
36
|
+
Node = DLinked::CacheList::Node
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Initializes a new CacheList instance.
|
|
40
|
+
# @return [void]
|
|
41
|
+
def initialize
|
|
42
|
+
super
|
|
43
|
+
@lru_nodes = {}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# --- LRU Management Methods (O(1)) ---
|
|
47
|
+
|
|
48
|
+
# 2. Expose a key-based prepend method
|
|
49
|
+
#
|
|
50
|
+
# Adds a new key-value pair to the head of the list (Most Recently Used / MRU).
|
|
51
|
+
# This operation is atomic, adding the node to the list and storing the
|
|
52
|
+
# node reference in the internal hash map.
|
|
53
|
+
#
|
|
54
|
+
# @param key [Object] The cache key to be managed by the LRU list.
|
|
55
|
+
# @param value [Object] The value to store inside the list node (usually the same as the key in an LRU list).
|
|
56
|
+
# @raise [RuntimeError] If the key already exists in the LRU map.
|
|
57
|
+
# @return [true] Returns true on successful insertion.
|
|
58
|
+
def prepend_key(key, value)
|
|
59
|
+
if @lru_nodes.key?(key)
|
|
60
|
+
raise "Key #{key} already exists in the LRU list."
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The specialized Node creation must happen here
|
|
64
|
+
node = Node.new(value, nil, @head, key)
|
|
65
|
+
|
|
66
|
+
# Use the base class's list logic
|
|
67
|
+
list_prepend_logic(node)
|
|
68
|
+
|
|
69
|
+
# Store the node reference in the map (O(1))
|
|
70
|
+
@lru_nodes[key] = node
|
|
71
|
+
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 3. Expose a key-based pop method (for eviction)
|
|
76
|
+
#
|
|
77
|
+
# Removes the Least Recently Used (LRU) item from the tail of the list.
|
|
78
|
+
# This performs an O(1) removal and updates the internal hash map.
|
|
79
|
+
#
|
|
80
|
+
# @return [Object, nil] Returns the key of the removed LRU item, or nil if the list is empty.
|
|
81
|
+
def pop_key
|
|
82
|
+
# O(1) - Base class's pop operation on the tail node
|
|
83
|
+
node = list_pop_logic
|
|
84
|
+
return nil unless node
|
|
85
|
+
|
|
86
|
+
@lru_nodes.delete(node.key)
|
|
87
|
+
return node.key
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# 4. Expose the O(1) touch operation
|
|
91
|
+
#
|
|
92
|
+
# Moves an existing key from its current position to the head (MRU).
|
|
93
|
+
# This is a core O(1) LRU "touch" operation.
|
|
94
|
+
#
|
|
95
|
+
# @param key [Object] The key of the item to move.
|
|
96
|
+
# @return [true, false] Returns true if the move was successful, false if the key was not found.
|
|
97
|
+
def move_to_head_by_key(key)
|
|
98
|
+
node = @lru_nodes[key]
|
|
99
|
+
return false unless node
|
|
100
|
+
|
|
101
|
+
# Calls the internal O(1) relocation method
|
|
102
|
+
_move_to_head(node)
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# 5. Expose O(1) removal
|
|
107
|
+
#
|
|
108
|
+
# Removes an item from the list by its key.
|
|
109
|
+
# This is an O(1) operation using the stored node reference.
|
|
110
|
+
#
|
|
111
|
+
# @param key [Object] The key of the item to remove.
|
|
112
|
+
# @return [true, false] Returns true if the key was removed, false if the key was not found.
|
|
113
|
+
def remove_by_key(key)
|
|
114
|
+
node = @lru_nodes.delete(key)
|
|
115
|
+
return false unless node
|
|
116
|
+
|
|
117
|
+
# Calls the internal O(1) node deletion method
|
|
118
|
+
_remove_node(node)
|
|
119
|
+
true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Clears both the doubly linked list and the internal LRU map.
|
|
123
|
+
# @return [void]
|
|
124
|
+
def clear
|
|
125
|
+
@lru_nodes.clear
|
|
126
|
+
super
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# --- PROTECTED / INTERNAL O(1) NODE METHODS ---
|
|
130
|
+
|
|
131
|
+
protected
|
|
132
|
+
|
|
133
|
+
# New helper: Returns the node that was popped/shifted
|
|
134
|
+
# This bypasses the base list's public #pop to allow access to the Node object.
|
|
135
|
+
# @return [DLinked::CacheList::Node, nil] The node object from the tail.
|
|
136
|
+
def list_pop_logic
|
|
137
|
+
return nil if empty?
|
|
138
|
+
|
|
139
|
+
node = @tail
|
|
140
|
+
@tail = @tail.prev
|
|
141
|
+
@tail ? @tail.next = nil : @head = nil
|
|
142
|
+
@size -= 1
|
|
143
|
+
return node
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# O(1) Deletion from a known node reference.
|
|
147
|
+
# Used internally by {#remove_by_key} and {#_move_to_head}.
|
|
148
|
+
# @param node [DLinked::CacheList::Node] The node object to remove.
|
|
149
|
+
# @return [Object] The value of the removed node.
|
|
150
|
+
def _remove_node(node)
|
|
151
|
+
# 1. Update the pointers of the surrounding nodes
|
|
152
|
+
node.prev.next = node.next if node.prev
|
|
153
|
+
node.next.prev = node.prev if node.next
|
|
154
|
+
|
|
155
|
+
# 2. Update list head/tail if necessary
|
|
156
|
+
@head = node.next if node == @head
|
|
157
|
+
@tail = node.prev if node == @tail
|
|
158
|
+
|
|
159
|
+
@size -= 1
|
|
160
|
+
node.value
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# O(1) Move to Head using O(1) removal and base class prepend logic.
|
|
164
|
+
# @param node [DLinked::CacheList::Node] The node object to move to the head.
|
|
165
|
+
# @return [void]
|
|
166
|
+
def _move_to_head(node)
|
|
167
|
+
return if node == @head
|
|
168
|
+
|
|
169
|
+
# 1. Remove from current spot (O(1))
|
|
170
|
+
_remove_node(node)
|
|
171
|
+
|
|
172
|
+
# 2. Re-establish forward link and clear backward link for clean insertion
|
|
173
|
+
node.next = @head
|
|
174
|
+
node.prev = nil
|
|
175
|
+
|
|
176
|
+
# 3. Use the base class logic to insert at head. Size counter is correct
|
|
177
|
+
# since _remove_node decremented and list_prepend_logic increments.
|
|
178
|
+
list_prepend_logic(node)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
end
|
|
182
|
+
end
|
data/lib/d_linked/list.rb
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'list/node'
|
|
4
|
+
|
|
4
5
|
module DLinked
|
|
5
6
|
# A fast, lightweight doubly linked list implementation
|
|
6
7
|
class List
|
|
7
8
|
include Enumerable
|
|
8
9
|
|
|
9
10
|
# PURE PERFORMANCE: We now use the dedicated Class for Node,
|
|
10
|
-
Node = DLinked::List::Node
|
|
11
|
-
private_constant :Node
|
|
11
|
+
# Node = DLinked::List::Node
|
|
12
12
|
|
|
13
13
|
attr_reader :size
|
|
14
14
|
alias length size
|
|
@@ -29,11 +29,7 @@ module DLinked
|
|
|
29
29
|
# @return [self] Returns the list instance, allowing for method chaining (e.g., list.prepend(1).prepend(2)).
|
|
30
30
|
def prepend(value)
|
|
31
31
|
node = Node.new(value, nil, @head)
|
|
32
|
-
|
|
33
|
-
@head = node
|
|
34
|
-
@tail ||= node
|
|
35
|
-
@size += 1
|
|
36
|
-
self
|
|
32
|
+
list_prepend_logic(node)
|
|
37
33
|
end
|
|
38
34
|
alias unshift prepend
|
|
39
35
|
|
|
@@ -45,11 +41,7 @@ module DLinked
|
|
|
45
41
|
# @return [DLinked::List] Returns the list instance for method chaining.
|
|
46
42
|
def append(value)
|
|
47
43
|
node = Node.new(value, @tail, nil)
|
|
48
|
-
|
|
49
|
-
@tail = node
|
|
50
|
-
@head ||= node
|
|
51
|
-
@size += 1
|
|
52
|
-
self
|
|
44
|
+
list_append_logic(node)
|
|
53
45
|
end
|
|
54
46
|
alias push append
|
|
55
47
|
alias << append
|
|
@@ -366,10 +358,12 @@ module DLinked
|
|
|
366
358
|
return prepend(value) if index <= 0
|
|
367
359
|
return append(value) if index >= @size
|
|
368
360
|
|
|
361
|
+
# Find the node to insert BEFORE
|
|
369
362
|
current = find_node_at_index(index)
|
|
370
363
|
|
|
371
|
-
# Insert before current node (O(1) linking)
|
|
372
364
|
new_node = Node.new(value, current.prev, current)
|
|
365
|
+
|
|
366
|
+
# Insert before current node (O(1) linking)
|
|
373
367
|
current.prev.next = new_node
|
|
374
368
|
current.prev = new_node
|
|
375
369
|
|
|
@@ -543,6 +537,24 @@ module DLinked
|
|
|
543
537
|
@size -= removed_count
|
|
544
538
|
result.empty? ? nil : result
|
|
545
539
|
end
|
|
540
|
+
protected
|
|
541
|
+
|
|
542
|
+
# This method handles the actual pointer manipulation, which is constant across all subclasses
|
|
543
|
+
def list_prepend_logic(node)
|
|
544
|
+
@head.prev = node if @head
|
|
545
|
+
@head = node
|
|
546
|
+
@tail ||= node
|
|
547
|
+
@size += 1
|
|
548
|
+
self
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def list_append_logic(node)
|
|
552
|
+
@tail.next = node if @tail
|
|
553
|
+
@tail = node
|
|
554
|
+
@head ||= node
|
|
555
|
+
@size += 1
|
|
556
|
+
self
|
|
557
|
+
end
|
|
546
558
|
|
|
547
559
|
private
|
|
548
560
|
|
data/lib/d_linked/version.rb
CHANGED
data/lib/dlinked.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'd_linked/version'
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
require_relative 'd_linked/list'
|
|
6
|
+
|
|
7
|
+
require_relative 'd_linked/cache_list'
|
|
5
8
|
|
|
6
9
|
module DLinked
|
|
7
|
-
#
|
|
8
|
-
end
|
|
10
|
+
# Namespace definition is fine here, it just re-opens the module.
|
|
11
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dlinked
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniele Frisanco
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-12-
|
|
11
|
+
date: 2025-12-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -127,6 +127,7 @@ files:
|
|
|
127
127
|
- benchmark.rb
|
|
128
128
|
- dlinked.gemspec
|
|
129
129
|
- example.rb
|
|
130
|
+
- lib/d_linked/cache_list.rb
|
|
130
131
|
- lib/d_linked/list.rb
|
|
131
132
|
- lib/d_linked/list/node.rb
|
|
132
133
|
- lib/d_linked/version.rb
|