dlinked 0.1.8 → 0.1.9
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/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +88 -0
- data/README.md +88 -0
- data/benchmark.rb +60 -36
- data/dlinked.gemspec +1 -0
- data/lib/d_linked/cache_list.rb +122 -93
- data/lib/d_linked/list.rb +167 -146
- data/lib/d_linked/version.rb +1 -1
- data/lru_cache_example.rb +152 -0
- metadata +19 -2
data/lib/d_linked/cache_list.rb
CHANGED
|
@@ -1,43 +1,69 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module DLinked
|
|
4
|
-
#
|
|
4
|
+
# A specialized doubly linked list for managing keys in a Least Recently Used (LRU) cache.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# This class extends {DLinked::List} by integrating a `Hash` map (`@lru_nodes`)
|
|
7
|
+
# to provide **O(1)** time complexity for all critical cache operations:
|
|
8
|
+
# adding, accessing (touching), evicting, and removing keys.
|
|
8
9
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# - Touching/Moving an existing key to the head (MRU).
|
|
12
|
-
# - Evicting the least recently used key (LRU).
|
|
13
|
-
# - Removing an item by key.
|
|
10
|
+
# The list maintains items in order from Most Recently Used (MRU) at the
|
|
11
|
+
# head to Least Recently Used (LRU) at the tail.
|
|
14
12
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
13
|
+
# @example Implementing a simple LRU Cache
|
|
14
|
+
# class MyLRUCache
|
|
15
|
+
# def initialize(capacity)
|
|
16
|
+
# @capacity = capacity
|
|
17
|
+
# @list = DLinked::CacheList.new
|
|
18
|
+
# @data = {}
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# def get(key)
|
|
22
|
+
# return nil unless @data.key?(key)
|
|
23
|
+
# @list.move_to_head_by_key(key) # Mark as recently used
|
|
24
|
+
# @data[key]
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# def set(key, value)
|
|
28
|
+
# if @data.key?(key)
|
|
29
|
+
# @list.move_to_head_by_key(key)
|
|
30
|
+
# else
|
|
31
|
+
# @list.prepend_key(key, value)
|
|
32
|
+
# evict if @list.size > @capacity
|
|
33
|
+
# end
|
|
34
|
+
# @data[key] = value
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# private
|
|
38
|
+
#
|
|
39
|
+
# def evict
|
|
40
|
+
# lru_key = @list.pop_key
|
|
41
|
+
# @data.delete(lru_key)
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
17
44
|
class CacheList < DLinked::List
|
|
18
|
-
|
|
19
|
-
|
|
45
|
+
# A specialized node for the `CacheList` that includes a `key`.
|
|
46
|
+
class Node < DLinked::List::Node
|
|
47
|
+
# @!attribute [rw] key
|
|
48
|
+
# @return [Object] The cache key associated with this node.
|
|
20
49
|
attr_accessor :key
|
|
21
50
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
51
|
+
# Initializes a new CacheList Node.
|
|
52
|
+
# @param value [Object] The value to store in the node.
|
|
53
|
+
# @param prev [Node, nil] The preceding node.
|
|
54
|
+
# @param next_node [Node, nil] The succeeding node.
|
|
55
|
+
# @param key [Object] The cache key for this node.
|
|
24
56
|
def initialize(value, prev = nil, next_node = nil, key = nil)
|
|
25
|
-
|
|
26
|
-
super(value, prev, next_node)
|
|
27
|
-
|
|
28
|
-
# Initialize the new, specialized attribute
|
|
57
|
+
super(value, prev, next_node)
|
|
29
58
|
@key = key
|
|
30
59
|
end
|
|
31
60
|
end
|
|
61
|
+
|
|
32
62
|
# @!attribute [r] lru_nodes
|
|
33
|
-
# @return [Hash
|
|
63
|
+
# @return [Hash{Object => DLinked::CacheList::Node}] The internal hash map.
|
|
34
64
|
attr_reader :lru_nodes
|
|
35
|
-
# # --- 1. OVERRIDE THE CONSTANT ---
|
|
36
|
-
Node = DLinked::CacheList::Node
|
|
37
|
-
|
|
38
65
|
|
|
39
|
-
# Initializes a new CacheList
|
|
40
|
-
# @return [void]
|
|
66
|
+
# Initializes a new, empty `CacheList`.
|
|
41
67
|
def initialize
|
|
42
68
|
super
|
|
43
69
|
@lru_nodes = {}
|
|
@@ -45,94 +71,106 @@ module DLinked
|
|
|
45
71
|
|
|
46
72
|
# --- LRU Management Methods (O(1)) ---
|
|
47
73
|
|
|
48
|
-
#
|
|
74
|
+
# Adds a new key-value pair to the head of the list (making it the MRU item).
|
|
49
75
|
#
|
|
50
|
-
#
|
|
51
|
-
# This operation is atomic, adding the node to the list and storing the
|
|
52
|
-
# node reference in the internal hash map.
|
|
76
|
+
# This is an **O(1)** operation.
|
|
53
77
|
#
|
|
54
|
-
# @
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
78
|
+
# @example
|
|
79
|
+
# lru = DLinked::CacheList.new
|
|
80
|
+
# lru.prepend_key(:a, 1)
|
|
81
|
+
# lru.to_a # => [1]
|
|
82
|
+
#
|
|
83
|
+
# @param key [Object] The cache key.
|
|
84
|
+
# @param value [Object] The value to store in the list node.
|
|
85
|
+
# @raise [RuntimeError] If the key already exists.
|
|
86
|
+
# @return [true] `true` on successful insertion.
|
|
58
87
|
def prepend_key(key, value)
|
|
59
|
-
if @lru_nodes.key?(key)
|
|
60
|
-
raise "Key #{key} already exists in the LRU list."
|
|
61
|
-
end
|
|
88
|
+
raise "Key #{key} already exists in the LRU list." if @lru_nodes.key?(key)
|
|
62
89
|
|
|
63
|
-
|
|
64
|
-
node
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
list_prepend_logic(node)
|
|
68
|
-
|
|
69
|
-
# Store the node reference in the map (O(1))
|
|
70
|
-
@lru_nodes[key] = node
|
|
71
|
-
|
|
72
|
-
true
|
|
90
|
+
node = Node.new(value, nil, @head, key)
|
|
91
|
+
list_prepend_logic(node) # Use base class's logic
|
|
92
|
+
@lru_nodes[key] = node
|
|
93
|
+
true
|
|
73
94
|
end
|
|
74
95
|
|
|
75
|
-
# 3. Expose a key-based pop method (for eviction)
|
|
76
|
-
#
|
|
77
96
|
# 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
97
|
#
|
|
80
|
-
#
|
|
98
|
+
# This is an **O(1)** operation.
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# lru = DLinked::CacheList.new
|
|
102
|
+
# lru.prepend_key(:a, 1)
|
|
103
|
+
# lru.prepend_key(:b, 2) # List is now [:b, :a]
|
|
104
|
+
# lru.pop_key # => :a
|
|
105
|
+
#
|
|
106
|
+
# @return [Object, nil] The key of the removed LRU item, or `nil` if the list is empty.
|
|
81
107
|
def pop_key
|
|
82
|
-
|
|
83
|
-
node = list_pop_logic
|
|
108
|
+
node = list_pop_logic
|
|
84
109
|
return nil unless node
|
|
85
|
-
|
|
110
|
+
|
|
86
111
|
@lru_nodes.delete(node.key)
|
|
87
|
-
|
|
112
|
+
node.key
|
|
88
113
|
end
|
|
89
|
-
|
|
90
|
-
#
|
|
114
|
+
|
|
115
|
+
# Moves an existing key from its current position to the head (making it the MRU item).
|
|
116
|
+
# This is a core "touch" operation for an LRU cache.
|
|
91
117
|
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
118
|
+
# This is an **O(1)** operation.
|
|
119
|
+
#
|
|
120
|
+
# @example
|
|
121
|
+
# lru = DLinked::CacheList.new
|
|
122
|
+
# lru.prepend_key(:a, 1)
|
|
123
|
+
# lru.prepend_key(:b, 2) # List order: [:b, :a]
|
|
124
|
+
# lru.move_to_head_by_key(:a) # "touch" :a
|
|
125
|
+
# lru.to_a # => [1, 2] (node values), key order is now [:a, :b]
|
|
94
126
|
#
|
|
95
127
|
# @param key [Object] The key of the item to move.
|
|
96
|
-
# @return [true, false]
|
|
128
|
+
# @return [true, false] `true` if the move was successful, `false` if the key was not found.
|
|
97
129
|
def move_to_head_by_key(key)
|
|
98
130
|
node = @lru_nodes[key]
|
|
99
131
|
return false unless node
|
|
100
132
|
|
|
101
|
-
|
|
102
|
-
_move_to_head(node)
|
|
133
|
+
_move_to_head(node)
|
|
103
134
|
true
|
|
104
135
|
end
|
|
105
136
|
|
|
106
|
-
#
|
|
137
|
+
# Removes an item from the list and map by its key.
|
|
138
|
+
#
|
|
139
|
+
# This is an **O(1)** operation.
|
|
107
140
|
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
141
|
+
# @example
|
|
142
|
+
# lru = DLinked::CacheList.new
|
|
143
|
+
# lru.prepend_key(:a, 1)
|
|
144
|
+
# lru.remove_by_key(:a) # => true
|
|
145
|
+
# lru.empty? # => true
|
|
110
146
|
#
|
|
111
147
|
# @param key [Object] The key of the item to remove.
|
|
112
|
-
# @return [true, false]
|
|
148
|
+
# @return [true, false] `true` if removed, `false` if the key was not found.
|
|
113
149
|
def remove_by_key(key)
|
|
114
150
|
node = @lru_nodes.delete(key)
|
|
115
151
|
return false unless node
|
|
116
|
-
|
|
117
|
-
# Calls the internal O(1) node deletion method
|
|
152
|
+
|
|
118
153
|
_remove_node(node)
|
|
119
154
|
true
|
|
120
155
|
end
|
|
121
156
|
|
|
122
|
-
# Clears
|
|
123
|
-
#
|
|
157
|
+
# Clears the list and the internal key map.
|
|
158
|
+
#
|
|
159
|
+
# This is an **O(1)** operation.
|
|
160
|
+
#
|
|
161
|
+
# @return [self]
|
|
124
162
|
def clear
|
|
125
163
|
@lru_nodes.clear
|
|
126
164
|
super
|
|
127
165
|
end
|
|
128
|
-
|
|
166
|
+
|
|
129
167
|
# --- PROTECTED / INTERNAL O(1) NODE METHODS ---
|
|
130
168
|
|
|
131
169
|
protected
|
|
132
170
|
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
# @
|
|
171
|
+
# Pops the tail node and returns the full node object.
|
|
172
|
+
# @return [DLinked::CacheList::Node, nil] The node from the tail.
|
|
173
|
+
# @api protected
|
|
136
174
|
def list_pop_logic
|
|
137
175
|
return nil if empty?
|
|
138
176
|
|
|
@@ -140,43 +178,34 @@ module DLinked
|
|
|
140
178
|
@tail = @tail.prev
|
|
141
179
|
@tail ? @tail.next = nil : @head = nil
|
|
142
180
|
@size -= 1
|
|
143
|
-
|
|
181
|
+
node
|
|
144
182
|
end
|
|
145
183
|
|
|
146
|
-
#
|
|
147
|
-
# Used internally by {#remove_by_key} and {#_move_to_head}.
|
|
184
|
+
# Deletes a given node reference in O(1) time.
|
|
148
185
|
# @param node [DLinked::CacheList::Node] The node object to remove.
|
|
149
186
|
# @return [Object] The value of the removed node.
|
|
187
|
+
# @api protected
|
|
150
188
|
def _remove_node(node)
|
|
151
|
-
# 1. Update the pointers of the surrounding nodes
|
|
152
189
|
node.prev.next = node.next if node.prev
|
|
153
190
|
node.next.prev = node.prev if node.next
|
|
154
|
-
|
|
155
|
-
# 2. Update list head/tail if necessary
|
|
191
|
+
|
|
156
192
|
@head = node.next if node == @head
|
|
157
193
|
@tail = node.prev if node == @tail
|
|
158
|
-
|
|
194
|
+
|
|
159
195
|
@size -= 1
|
|
160
196
|
node.value
|
|
161
197
|
end
|
|
162
|
-
|
|
163
|
-
#
|
|
164
|
-
# @param node [DLinked::CacheList::Node] The node object to move
|
|
165
|
-
# @
|
|
198
|
+
|
|
199
|
+
# Moves a given node reference to the head in O(1) time.
|
|
200
|
+
# @param node [DLinked::CacheList::Node] The node object to move.
|
|
201
|
+
# @api protected
|
|
166
202
|
def _move_to_head(node)
|
|
167
203
|
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
204
|
|
|
176
|
-
|
|
177
|
-
|
|
205
|
+
_remove_node(node)
|
|
206
|
+
node.next = @head
|
|
207
|
+
node.prev = nil
|
|
178
208
|
list_prepend_logic(node)
|
|
179
209
|
end
|
|
180
|
-
|
|
181
210
|
end
|
|
182
211
|
end
|