dlinked 0.1.7 → 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/LICENSE.txt +1 -1
- data/README.md +143 -3
- data/benchmark.rb +60 -36
- data/dlinked.gemspec +1 -0
- data/lib/d_linked/cache_list.rb +211 -0
- data/lib/d_linked/list.rb +187 -154
- data/lib/d_linked/version.rb +1 -1
- data/lib/dlinked.rb +6 -3
- data/lru_cache_example.rb +152 -0
- metadata +20 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8215705b10ca404e892cc7b1d6571d96ee66aa8521941f42d6e7051582d2372a
|
|
4
|
+
data.tar.gz: bff1d797d11300cdae48d6f7a3623e17bb7e996db7457b270884e5e9312192b6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69e363bbc4d74898aa65673c0f46250633f9448d7c406ecfe089860a2005142905a170f0da5724d3a1766fe4ff70ddbbd2a77653be86b9fab08efd8b23a538a9
|
|
7
|
+
data.tar.gz: 129ab2caf81b5d8fb6d24c67ccad979fff1ce928811341f28104f5b0f851f1d05106783935d2e17bddd1bc9edf953f539332d4cc01131286495bd529a90dc65d
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
|
8
|
+
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
|
9
|
+
level of experience, education, socio-economic status, nationality, personal
|
|
10
|
+
appearance, race, religion, or sexual identity and orientation.
|
|
11
|
+
|
|
12
|
+
## Our Standards
|
|
13
|
+
|
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
|
15
|
+
include:
|
|
16
|
+
|
|
17
|
+
* Using welcoming and inclusive language
|
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
|
19
|
+
* Gracefully accepting constructive criticism
|
|
20
|
+
* Focusing on what is best for the community
|
|
21
|
+
* Showing empathy towards other community members
|
|
22
|
+
|
|
23
|
+
Examples of unacceptable behavior by participants include:
|
|
24
|
+
|
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
26
|
+
advances
|
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
28
|
+
* Public or private harassment
|
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
|
30
|
+
address, without explicit permission
|
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
32
|
+
professional setting
|
|
33
|
+
|
|
34
|
+
## Our Responsibilities
|
|
35
|
+
|
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
|
38
|
+
response to any instances of unacceptable behavior.
|
|
39
|
+
|
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
|
44
|
+
threatening, offensive, or harmful.
|
|
45
|
+
|
|
46
|
+
## Scope
|
|
47
|
+
|
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
|
49
|
+
when an individual is representing the project or its community. Examples of
|
|
50
|
+
representing a project or community include using an official project e-mail
|
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
|
53
|
+
further defined and clarified by project maintainers.
|
|
54
|
+
|
|
55
|
+
## Enforcement
|
|
56
|
+
|
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
58
|
+
reported by contacting the project team. All
|
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
|
63
|
+
|
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
|
66
|
+
members of the project's leadership.
|
|
67
|
+
|
|
68
|
+
## Attribution
|
|
69
|
+
|
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
|
|
71
|
+
available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
|
72
|
+
|
|
73
|
+
[homepage]: https://www.contributor-covenant.org
|
|
74
|
+
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Contributing to DLinked
|
|
2
|
+
|
|
3
|
+
First off, thank you for considering contributing to DLinked! It's people like you that make open source such a great community.
|
|
4
|
+
|
|
5
|
+
We welcome any type of contribution, not just code. You can help with:
|
|
6
|
+
* **Reporting a bug**
|
|
7
|
+
* **Discussing the current state of the code**
|
|
8
|
+
* **Submitting a fix**
|
|
9
|
+
* **Proposing new features**
|
|
10
|
+
* **Becoming a maintainer**
|
|
11
|
+
|
|
12
|
+
## Getting Started
|
|
13
|
+
|
|
14
|
+
### Development Environment
|
|
15
|
+
|
|
16
|
+
To get started with the development environment, you'll need:
|
|
17
|
+
* Ruby (see `.ruby-version` for the required version)
|
|
18
|
+
* Bundler
|
|
19
|
+
|
|
20
|
+
1. **Fork the repository** on GitHub.
|
|
21
|
+
2. **Clone your fork** locally:
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/your-username/dlinked.git
|
|
24
|
+
cd dlinked
|
|
25
|
+
```
|
|
26
|
+
3. **Install dependencies** using Bundler:
|
|
27
|
+
```bash
|
|
28
|
+
bundle install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Running Tests
|
|
32
|
+
|
|
33
|
+
This project uses Minitest for testing. To run the full test suite, use the following command:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bundle exec rake test
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This will also run SimpleCov to generate a code coverage report in the `coverage/` directory. When submitting a pull request, please ensure that your changes are covered by tests and that you maintain or increase the test coverage.
|
|
40
|
+
|
|
41
|
+
### Code Style
|
|
42
|
+
|
|
43
|
+
This project uses RuboCop to enforce a consistent code style. To check your code for style violations, run:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bundle exec rubocop
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Many style violations can be automatically fixed by running:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
bundle exec rubocop -A
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Please ensure your code follows the project's style guidelines before submitting a pull request.
|
|
56
|
+
|
|
57
|
+
## How to Contribute
|
|
58
|
+
|
|
59
|
+
### Reporting Bugs
|
|
60
|
+
|
|
61
|
+
If you find a bug, please open an issue on GitHub. Please include:
|
|
62
|
+
* A clear and descriptive title.
|
|
63
|
+
* A detailed description of the problem, including steps to reproduce it.
|
|
64
|
+
* The expected behavior and what actually happened.
|
|
65
|
+
* The version of DLinked and Ruby you are using.
|
|
66
|
+
|
|
67
|
+
### Submitting Changes (Pull Requests)
|
|
68
|
+
|
|
69
|
+
1. Create a new branch for your changes:
|
|
70
|
+
```bash
|
|
71
|
+
git checkout -b your-feature-branch-name
|
|
72
|
+
```
|
|
73
|
+
2. Make your changes, and add or update tests as needed.
|
|
74
|
+
3. Ensure the test suite passes (`bundle exec rake test`).
|
|
75
|
+
4. Ensure your code passes the RuboCop checks (`bundle exec rubocop`).
|
|
76
|
+
5. Commit your changes with a clear and descriptive commit message.
|
|
77
|
+
6. Push your branch to your fork on GitHub:
|
|
78
|
+
```bash
|
|
79
|
+
git push origin your-feature-branch-name
|
|
80
|
+
```
|
|
81
|
+
7. Open a pull request from your fork to the main DLinked repository.
|
|
82
|
+
8. In the pull request description, please explain the problem you are solving and the changes you have made.
|
|
83
|
+
|
|
84
|
+
## Code of Conduct
|
|
85
|
+
|
|
86
|
+
By participating in this project, you are expected to uphold our [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
87
|
+
|
|
88
|
+
Thank you for your contribution!
|
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,10 +27,19 @@ gem install dlinked
|
|
|
27
27
|
## Test
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
bundle exec
|
|
30
|
+
bundle exec rake test
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Documentation
|
|
31
34
|
|
|
35
|
+
To generate the YARD documentation for this project, run:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bundle exec yard doc
|
|
32
39
|
```
|
|
33
40
|
|
|
41
|
+
This will create a `doc/` directory containing the full HTML documentation. You can view it by opening `doc/index.html` in your browser.
|
|
42
|
+
|
|
34
43
|
## Usage
|
|
35
44
|
### 1. Basic Initialization and O(1) Operations
|
|
36
45
|
Demonstrate creating the list, adding elements to both ends, and removing them quickly.
|
|
@@ -189,6 +198,109 @@ list1.concat(list2)
|
|
|
189
198
|
list1.to_a # => [1, 2, 3, 4]
|
|
190
199
|
```
|
|
191
200
|
|
|
201
|
+
|
|
202
|
+
### 7. DLinked::CacheList (LRU Cache Utility)
|
|
203
|
+
`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:
|
|
204
|
+
- #prepend_key(key, value) (Add as MRU)
|
|
205
|
+
- #move_to_head_by_key(key) (Touch/Access)
|
|
206
|
+
- #pop_key (Evict LRU)
|
|
207
|
+
- #remove_by_key(key) (Remove)
|
|
208
|
+
|
|
209
|
+
- **Most Recently Used (MRU)** items are at the **head** of the list.
|
|
210
|
+
- **Least Recently Used (LRU)** items are at the **tail** of the list.
|
|
211
|
+
|
|
212
|
+
This makes it highly efficient for tracking key access order in a memory-limited cache.
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
require 'dlinked'
|
|
216
|
+
|
|
217
|
+
# 1. Initialization
|
|
218
|
+
lru_list = DLinked::CacheList.new
|
|
219
|
+
lru_list.size # => 0
|
|
220
|
+
|
|
221
|
+
# 2. Add keys to the cache (as MRU)
|
|
222
|
+
# In a real cache, the value might be the cached data itself.
|
|
223
|
+
# For key tracking, value can be the same as the key.
|
|
224
|
+
lru_list.prepend_key(:key1, :key1)
|
|
225
|
+
lru_list.prepend_key(:key2, :key2)
|
|
226
|
+
lru_list.prepend_key(:key3, :key3)
|
|
227
|
+
|
|
228
|
+
# List order (MRU to LRU): [:key3, :key2, :key1]
|
|
229
|
+
lru_list.to_a # => [:key3, :key2, :key1]
|
|
230
|
+
|
|
231
|
+
# 3. "Touch" an existing key, moving it to the head (MRU)
|
|
232
|
+
lru_list.move_to_head_by_key(:key1)
|
|
233
|
+
|
|
234
|
+
# List order is now: [:key1, :key3, :key2]
|
|
235
|
+
lru_list.to_a # => [:key1, :key3, :key2]
|
|
236
|
+
|
|
237
|
+
# 4. Evict the least recently used key (from the tail)
|
|
238
|
+
evicted_key = lru_list.pop_key
|
|
239
|
+
evicted_key # => :key2
|
|
240
|
+
|
|
241
|
+
# List order is now: [:key1, :key3]
|
|
242
|
+
lru_list.to_a # => [:key1, :key3]
|
|
243
|
+
|
|
244
|
+
# 5. Remove a specific key (O(1) operation)
|
|
245
|
+
lru_list.remove_by_key(:key3)
|
|
246
|
+
lru_list.to_a # => [:key1]
|
|
247
|
+
|
|
248
|
+
# 6. Clear the list and the key map (O(1) operation)
|
|
249
|
+
lru_list.clear
|
|
250
|
+
lru_list.size # => 0
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Real-World Example: A Complete LRU Cache
|
|
255
|
+
While `DLinked::CacheList` provides the low-level, high-performance key tracking, you can easily build a complete, practical `LRUCache` class around it.
|
|
256
|
+
|
|
257
|
+
The following example demonstrates how to combine `DLinked::CacheList` with a `Hash` for data storage to create a fully functional LRU cache.
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
# A complete, working implementation of a Least Recently Used (LRU) Cache
|
|
261
|
+
# built on top of DLinked::CacheList.
|
|
262
|
+
class LRUCache
|
|
263
|
+
attr_reader :capacity, :size
|
|
264
|
+
|
|
265
|
+
def initialize(capacity)
|
|
266
|
+
raise ArgumentError, 'Capacity must be a positive integer' unless capacity.is_a?(Integer) && capacity > 0
|
|
267
|
+
|
|
268
|
+
@capacity = capacity
|
|
269
|
+
@size = 0
|
|
270
|
+
@list = DLinked::CacheList.new
|
|
271
|
+
@data = {}
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def get(key)
|
|
275
|
+
return nil unless @data.key?(key)
|
|
276
|
+
@list.move_to_head_by_key(key)
|
|
277
|
+
@data[key]
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def set(key, value)
|
|
281
|
+
if @data.key?(key)
|
|
282
|
+
@list.move_to_head_by_key(key)
|
|
283
|
+
else
|
|
284
|
+
@list.prepend_key(key, value)
|
|
285
|
+
@size += 1
|
|
286
|
+
evict if @size > @capacity
|
|
287
|
+
end
|
|
288
|
+
@data[key] = value
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
private
|
|
292
|
+
|
|
293
|
+
def evict
|
|
294
|
+
lru_key = @list.pop_key
|
|
295
|
+
return unless lru_key
|
|
296
|
+
@data.delete(lru_key)
|
|
297
|
+
@size -= 1
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
For a complete, runnable demonstration of this `LRUCache` class in action, see the [lru_cache_example.rb](./lru_cache_example.rb) file.
|
|
303
|
+
|
|
192
304
|
## ⚡ Performance Characteristics
|
|
193
305
|
This library is designed to offer the guaranteed performance benefits of a Doubly Linked List over a standard Ruby `Array` for certain operations.
|
|
194
306
|
|
|
@@ -202,6 +314,34 @@ This library is designed to offer the guaranteed performance benefits of a Doubl
|
|
|
202
314
|
|
|
203
315
|
|
|
204
316
|
|
|
317
|
+
## ⚡ Performance Benchmarks
|
|
318
|
+
|
|
319
|
+
To quantify the performance benefits of `DLinked::List` over Ruby's `Array`, a benchmark suite is available in `benchmark.rb`. It uses the `benchmark-ips` gem to compare the performance of single operations on a pre-filled data structure of 10,000 items.
|
|
320
|
+
|
|
321
|
+
The benchmark measures a single operation (e.g., `shift`) and immediately performs the inverse operation (e.g., `push`) to ensure the list size remains constant for every measurement. This provides a more accurate comparison of how each data structure handles these calls on a large collection.
|
|
322
|
+
|
|
323
|
+
You can run the benchmark yourself:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
bundle install
|
|
327
|
+
bundle exec ruby benchmark.rb
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Results:**
|
|
331
|
+
|
|
332
|
+
The results below were generated on Ruby 3.1.4. They demonstrate that for a list of 10,000 items, the performance of Ruby's native, C-implemented `Array` is significantly faster than `DLinked::List`'s pure Ruby implementation, even for operations where the linked list has a better theoretical time complexity.
|
|
333
|
+
|
|
334
|
+
| Operation | Comparison | Analysis |
|
|
335
|
+
| :--- | :--- | :--- |
|
|
336
|
+
| `append` / `push` | `Array` is ~5.2x faster | `Array` is faster. This is expected as it's a highly optimized C implementation, while `DLinked::List` has the overhead of Ruby method calls and `Node` object allocation. |
|
|
337
|
+
| `prepend` / `unshift`| `Array` is ~5.1x faster | Surprisingly, `Array#unshift` is still faster at this scale. The cost of memory shifting in C for 10,000 items is lower than the overhead of `DLinked::List`'s Ruby implementation. |
|
|
338
|
+
| `pop` | `Array` is ~5.3x faster | Similar to `push`, the native `Array` implementation is faster for this O(1) operation. |
|
|
339
|
+
| `shift` | `Array` is ~5.4x faster | Like `unshift`, `Array#shift`'s O(n) operation in C is faster than `DLinked::List`'s O(1) operation in Ruby at this list size, due to the overhead of the Ruby implementation. |
|
|
340
|
+
|
|
341
|
+
**Conclusion:**
|
|
342
|
+
|
|
343
|
+
These benchmarks show that the raw speed of the underlying C implementation of `Array` outweighs the Big-O algorithmic advantages of a pure Ruby linked list for collections of this size. `DLinked::List` is better suited for educational purposes or for algorithms where the explicit node structure and pointer manipulation are more important than raw wall-clock performance against `Array`.
|
|
344
|
+
|
|
205
345
|
## 💾 Memory Usage
|
|
206
346
|
|
|
207
347
|
While memory usage is highly dependent on the objects stored, the overhead of the list structure itself is minimal and highly efficient:
|
data/benchmark.rb
CHANGED
|
@@ -1,54 +1,78 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
require 'benchmark'
|
|
3
|
+
require 'benchmark/ips'
|
|
4
|
+
require 'dlinked'
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
puts "Ruby version: #{RUBY_VERSION}"
|
|
7
|
+
puts "DLinked version: #{DLinked::VERSION}"
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
LIST_SIZE = 10_000
|
|
10
|
+
puts "\n--- Benchmarking single operations on a list of #{LIST_SIZE} items ---"
|
|
11
|
+
puts "A single operation is performed, and then undone to maintain list size."
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
# --- Setup ---
|
|
14
|
+
array = (0...LIST_SIZE).to_a
|
|
15
|
+
list = DLinked::List.new
|
|
16
|
+
array.each { |i| list.append(i) }
|
|
17
|
+
|
|
18
|
+
# --- Benchmark Suite ---
|
|
19
|
+
|
|
20
|
+
puts "\nAppend/Push at the end:"
|
|
21
|
+
Benchmark.ips do |x|
|
|
22
|
+
x.report("Array#push") do
|
|
23
|
+
array.push(0)
|
|
24
|
+
array.pop
|
|
18
25
|
end
|
|
19
|
-
end
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
x.report("DLinked::List#append") do
|
|
28
|
+
list.append(0)
|
|
29
|
+
list.pop
|
|
30
|
+
end
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
x.compare!
|
|
33
|
+
end
|
|
24
34
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
puts "\nPrepend/Unshift at the beginning:"
|
|
36
|
+
Benchmark.ips do |x|
|
|
37
|
+
x.report("Array#unshift") do
|
|
38
|
+
array.unshift(0)
|
|
39
|
+
array.shift
|
|
28
40
|
end
|
|
29
41
|
|
|
30
|
-
x.report(
|
|
31
|
-
|
|
42
|
+
x.report("DLinked::List#prepend") do
|
|
43
|
+
list.prepend(0)
|
|
44
|
+
list.shift
|
|
32
45
|
end
|
|
33
46
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class_nodes = Array.new(1000) { |i| NodeClass.new(i, nil, nil) }
|
|
47
|
+
x.compare!
|
|
48
|
+
end
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
end
|
|
50
|
+
puts "\nPop from the end:"
|
|
51
|
+
Benchmark.ips do |x|
|
|
52
|
+
x.report("Array#pop") do
|
|
53
|
+
el = array.pop
|
|
54
|
+
array.push(el)
|
|
44
55
|
end
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
node.value = v + 1
|
|
50
|
-
end
|
|
56
|
+
|
|
57
|
+
x.report("DLinked::List#pop") do
|
|
58
|
+
el = list.pop
|
|
59
|
+
list.append(el)
|
|
51
60
|
end
|
|
61
|
+
|
|
62
|
+
x.compare!
|
|
52
63
|
end
|
|
53
64
|
|
|
54
|
-
puts "\
|
|
65
|
+
puts "\nShift from the beginning:"
|
|
66
|
+
Benchmark.ips do |x|
|
|
67
|
+
x.report("Array#shift") do
|
|
68
|
+
el = array.shift
|
|
69
|
+
array.push(el)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
x.report("DLinked::List#shift") do
|
|
73
|
+
el = list.shift
|
|
74
|
+
list.append(el)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
x.compare!
|
|
78
|
+
end
|
data/dlinked.gemspec
CHANGED
|
@@ -38,4 +38,5 @@ Gem::Specification.new do |spec|
|
|
|
38
38
|
spec.add_development_dependency "rubocop-minitest", "~> 0.16.0"
|
|
39
39
|
spec.add_development_dependency "simplecov", "~> 0.22"
|
|
40
40
|
spec.add_development_dependency "yard", "~> 0.9"
|
|
41
|
+
spec.add_development_dependency "benchmark-ips", "~> 2.8"
|
|
41
42
|
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DLinked
|
|
4
|
+
# A specialized doubly linked list for managing keys in a Least Recently Used (LRU) cache.
|
|
5
|
+
#
|
|
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.
|
|
9
|
+
#
|
|
10
|
+
# The list maintains items in order from Most Recently Used (MRU) at the
|
|
11
|
+
# head to Least Recently Used (LRU) at the tail.
|
|
12
|
+
#
|
|
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
|
|
44
|
+
class CacheList < DLinked::List
|
|
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.
|
|
49
|
+
attr_accessor :key
|
|
50
|
+
|
|
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.
|
|
56
|
+
def initialize(value, prev = nil, next_node = nil, key = nil)
|
|
57
|
+
super(value, prev, next_node)
|
|
58
|
+
@key = key
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @!attribute [r] lru_nodes
|
|
63
|
+
# @return [Hash{Object => DLinked::CacheList::Node}] The internal hash map.
|
|
64
|
+
attr_reader :lru_nodes
|
|
65
|
+
|
|
66
|
+
# Initializes a new, empty `CacheList`.
|
|
67
|
+
def initialize
|
|
68
|
+
super
|
|
69
|
+
@lru_nodes = {}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# --- LRU Management Methods (O(1)) ---
|
|
73
|
+
|
|
74
|
+
# Adds a new key-value pair to the head of the list (making it the MRU item).
|
|
75
|
+
#
|
|
76
|
+
# This is an **O(1)** operation.
|
|
77
|
+
#
|
|
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.
|
|
87
|
+
def prepend_key(key, value)
|
|
88
|
+
raise "Key #{key} already exists in the LRU list." if @lru_nodes.key?(key)
|
|
89
|
+
|
|
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
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Removes the Least Recently Used (LRU) item from the tail of the list.
|
|
97
|
+
#
|
|
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.
|
|
107
|
+
def pop_key
|
|
108
|
+
node = list_pop_logic
|
|
109
|
+
return nil unless node
|
|
110
|
+
|
|
111
|
+
@lru_nodes.delete(node.key)
|
|
112
|
+
node.key
|
|
113
|
+
end
|
|
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.
|
|
117
|
+
#
|
|
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]
|
|
126
|
+
#
|
|
127
|
+
# @param key [Object] The key of the item to move.
|
|
128
|
+
# @return [true, false] `true` if the move was successful, `false` if the key was not found.
|
|
129
|
+
def move_to_head_by_key(key)
|
|
130
|
+
node = @lru_nodes[key]
|
|
131
|
+
return false unless node
|
|
132
|
+
|
|
133
|
+
_move_to_head(node)
|
|
134
|
+
true
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Removes an item from the list and map by its key.
|
|
138
|
+
#
|
|
139
|
+
# This is an **O(1)** operation.
|
|
140
|
+
#
|
|
141
|
+
# @example
|
|
142
|
+
# lru = DLinked::CacheList.new
|
|
143
|
+
# lru.prepend_key(:a, 1)
|
|
144
|
+
# lru.remove_by_key(:a) # => true
|
|
145
|
+
# lru.empty? # => true
|
|
146
|
+
#
|
|
147
|
+
# @param key [Object] The key of the item to remove.
|
|
148
|
+
# @return [true, false] `true` if removed, `false` if the key was not found.
|
|
149
|
+
def remove_by_key(key)
|
|
150
|
+
node = @lru_nodes.delete(key)
|
|
151
|
+
return false unless node
|
|
152
|
+
|
|
153
|
+
_remove_node(node)
|
|
154
|
+
true
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Clears the list and the internal key map.
|
|
158
|
+
#
|
|
159
|
+
# This is an **O(1)** operation.
|
|
160
|
+
#
|
|
161
|
+
# @return [self]
|
|
162
|
+
def clear
|
|
163
|
+
@lru_nodes.clear
|
|
164
|
+
super
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# --- PROTECTED / INTERNAL O(1) NODE METHODS ---
|
|
168
|
+
|
|
169
|
+
protected
|
|
170
|
+
|
|
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
|
|
174
|
+
def list_pop_logic
|
|
175
|
+
return nil if empty?
|
|
176
|
+
|
|
177
|
+
node = @tail
|
|
178
|
+
@tail = @tail.prev
|
|
179
|
+
@tail ? @tail.next = nil : @head = nil
|
|
180
|
+
@size -= 1
|
|
181
|
+
node
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Deletes a given node reference in O(1) time.
|
|
185
|
+
# @param node [DLinked::CacheList::Node] The node object to remove.
|
|
186
|
+
# @return [Object] The value of the removed node.
|
|
187
|
+
# @api protected
|
|
188
|
+
def _remove_node(node)
|
|
189
|
+
node.prev.next = node.next if node.prev
|
|
190
|
+
node.next.prev = node.prev if node.next
|
|
191
|
+
|
|
192
|
+
@head = node.next if node == @head
|
|
193
|
+
@tail = node.prev if node == @tail
|
|
194
|
+
|
|
195
|
+
@size -= 1
|
|
196
|
+
node.value
|
|
197
|
+
end
|
|
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
|
|
202
|
+
def _move_to_head(node)
|
|
203
|
+
return if node == @head
|
|
204
|
+
|
|
205
|
+
_remove_node(node)
|
|
206
|
+
node.next = @head
|
|
207
|
+
node.prev = nil
|
|
208
|
+
list_prepend_logic(node)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|