dlinked 0.1.1

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.
@@ -0,0 +1,579 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'list/node'
4
+ module DLinked
5
+ # A fast, lightweight doubly linked list implementation
6
+ class List
7
+ include Enumerable
8
+
9
+ # PURE PERFORMANCE: We now use the dedicated Class for Node,
10
+ Node = DLinked::List::Node
11
+ private_constant :Node
12
+
13
+ attr_reader :size
14
+ alias length size
15
+
16
+ def initialize
17
+ @head = nil
18
+ @tail = nil
19
+ @size = 0
20
+ end
21
+
22
+ # --- O(1) CORE OPERATIONS ---
23
+
24
+ # Adds a new value to the beginning of the list (the head).
25
+ #
26
+ # This is an **O(1)** operation, as it only involves updating a few pointers.
27
+ #
28
+ # @param value [Object] The value to store in the new node.
29
+ # @return [self] Returns the list instance, allowing for method chaining (e.g., list.prepend(1).prepend(2)).
30
+ def prepend(value)
31
+ node = Node.new(value, nil, @head)
32
+ @head.prev = node if @head
33
+ @head = node
34
+ @tail ||= node
35
+ @size += 1
36
+ self
37
+ end
38
+ alias unshift prepend
39
+
40
+ # Adds a new value to the end of the list (the tail).
41
+ #
42
+ # This is an O(1) operation.
43
+ #
44
+ # @param value [Object] The value to add to the list.
45
+ # @return [DLinked::List] Returns the list instance for method chaining.
46
+ def append(value)
47
+ node = Node.new(value, @tail, nil)
48
+ @tail.next = node if @tail
49
+ @tail = node
50
+ @head ||= node
51
+ @size += 1
52
+ self
53
+ end
54
+ alias push append
55
+ alias << append
56
+
57
+ # Removes the first element from the list (the head) and returns its value.
58
+ #
59
+ # This is an **O(1)** operation.
60
+ #
61
+ # @return [Object, nil] The value of the removed element, or nil if the list is empty.
62
+ def shift
63
+ return nil if empty?
64
+
65
+ value = @head.value
66
+ @head = @head.next
67
+ @head ? @head.prev = nil : @tail = nil
68
+ @size -= 1
69
+ value
70
+ end
71
+
72
+ # Removes the last element from the list (the tail) and returns its value.
73
+ #
74
+ # This is an **O(1)** operation, as the tail pointer gives immediate access to the node.
75
+ #
76
+ # @return [Object, nil] The value of the removed element, or nil if the list is empty.
77
+ def pop
78
+ return nil if empty?
79
+
80
+ value = @tail.value
81
+ @tail = @tail.prev
82
+ @tail ? @tail.next = nil : @head = nil
83
+ @size -= 1
84
+ value
85
+ end
86
+
87
+ # Returns the value of the element at the head (start) of the list.
88
+ #
89
+ # This is an **O(1)** operation.
90
+ #
91
+ # @return [Object, nil] The value of the first element, or nil if the list is empty.
92
+ def first
93
+ @head&.value
94
+ end
95
+
96
+ # Returns the value of the element at the tail (end) of the list.
97
+ #
98
+ # This is an **O(1)** operation.
99
+ #
100
+ # @return [Object, nil] The value of the last element, or nil if the list is empty.
101
+ def last
102
+ @tail&.value
103
+ end
104
+
105
+ # Checks if the list contains any elements.
106
+ #
107
+ # This is an **O(1)** operation.
108
+ #
109
+ # @return [Boolean] True if the size of the list is zero, false otherwise.
110
+ def empty?
111
+ @size.zero?
112
+ end
113
+
114
+ # Removes all elements from the list, resetting the head, tail, and size.
115
+ #
116
+ # This is an **O(1)** operation, as it only resets instance variables.
117
+ #
118
+ # @return [self] Returns the list instance.
119
+ def clear
120
+ @head = nil
121
+ @tail = nil
122
+ @size = 0
123
+ self
124
+ end
125
+
126
+ # --- O(n) ENUMERATION & LOOKUP ---
127
+
128
+ # Iterates through the list, yielding the value of each element in order.
129
+ #
130
+ # This is an **O(n)** operation, as it traverses every node from head to tail.
131
+ #
132
+ # @yield [Object] The value of the current element.
133
+ # @return [self, Enumerator] Returns the list instance if a block is given,
134
+ # otherwise returns an Enumerator.
135
+ def each
136
+ return enum_for(:each) unless block_given?
137
+
138
+ current = @head
139
+ while current
140
+ yield current.value
141
+ current = current.next
142
+ end
143
+ self
144
+ end
145
+
146
+ # Iterates through the list in reverse order, yielding the value of each element
147
+ # starting from the tail and moving to the head.
148
+ #
149
+ # This is an **O(n)** operation, as it traverses every node.
150
+ #
151
+ # @yield [Object] The value of the current element.
152
+ # @return [self, Enumerator] Returns the list instance if a block is given,
153
+ # otherwise returns an Enumerator.
154
+ def reverse_each
155
+ return enum_for(:reverse_each) unless block_given?
156
+
157
+ current = @tail
158
+ while current
159
+ yield current.value
160
+ current = current.prev
161
+ end
162
+ self
163
+ end
164
+
165
+ # Finds the index of the first occurrence of a given value.
166
+ #
167
+ # This is an **O(n)** operation, as it requires traversing the list from the head.
168
+ #
169
+ # @param value [Object] The value to search for.
170
+ # @return [Integer, nil] The index of the first matching element, or nil if the value is not found.
171
+ def index(value)
172
+ current = @head
173
+ idx = 0
174
+ while current
175
+ return idx if current.value == value
176
+
177
+ current = current.next
178
+ idx += 1
179
+ end
180
+ nil
181
+ end
182
+
183
+ # Converts the linked list into a standard Ruby Array.
184
+ #
185
+ # This is an **O(n)** operation, as it requires iterating over every element
186
+ # and allocating a new Array.
187
+ #
188
+ # @return [Array] A new Array containing all elements in order.
189
+ def to_a
190
+ map { |v| v }
191
+ end
192
+
193
+ # Returns a string representation of the list, resembling a standard Ruby Array.
194
+ #
195
+ # This is an **O(n)** operation due to the call to #to_a.
196
+ #
197
+ # @return [String] The string representation (e.g., "[10, 20, 30]").
198
+ def to_s
199
+ "[#{to_a.join(', ')}]"
200
+ end
201
+ alias inspect to_s
202
+
203
+ # --- O(n) ARRAY/SLICE COMPATIBILITY ---
204
+
205
+ # Retrieves the element(s) at the specified index or within a slice.
206
+ #
207
+ # This method supports two primary forms of access:
208
+ # 1. **Single Index (O(n)):** Returns the element at a specific positive or negative index (e.g., list[2] or list[-1]).
209
+ # 2. **Slice Access (O(n)):** Delegates to the {#slice} method for start/length or range access (e.g., list[1, 2] or list[1..3]).
210
+ #
211
+ # Traversal is optimized: for positive indices less than size/2, traversal starts from the head;
212
+ # otherwise, it starts from the tail.
213
+ #
214
+ # @param args [Array] Arguments representing either a single index or slice parameters:
215
+ # - `(index)` for single element access.
216
+ # - `(start_index, length)` for a slice.
217
+ # - `(range)` for a slice using a Range object.
218
+ # @return [Object, Array<Object>, nil] The value at the index, an array of values for a slice, or nil if the single index is out of bounds.
219
+ def [](*args)
220
+ # Case 1: Single Index Access (list[i])
221
+ if args.size == 1 && args[0].is_a?(Integer)
222
+ index = args[0]
223
+ index += @size if index.negative?
224
+ return nil if index.negative? || index >= @size
225
+
226
+ node = find_node_at_index(index)
227
+ return node.value # Returns raw value
228
+ end
229
+
230
+ # Case 2 & 3: Slicing (list[start, length] or list[range])
231
+ slice(*args) # Delegate to the robust slice method
232
+ end
233
+
234
+ # Sets the value of an element at a single index or replaces a slice (range or start/length)
235
+ # with new element(s).
236
+ #
237
+ # This method handles four main scenarios:
238
+ # 1. Single Element Assignment (O(n)): Overwrites the value at a valid index.
239
+ # 2. Slice Replacement (O(n)): Deletes a section and inserts new elements.
240
+ # 3. Out-of-Bounds Append (O(k)): If the start index is greater than the current size,
241
+ # the new elements are appended to the list (k is the length of the replacement).
242
+ # 4. Out-of-Bounds Non-Append (Returns nil): For a single index assignment that is out of bounds,
243
+ # it returns nil (like a standard Ruby Array setter).
244
+ #
245
+ # The overall complexity is **O(n + k)**, where n is the traversal time to find the start point,
246
+ # and k is the number of elements being inserted or deleted.
247
+ #
248
+ # @param args [Array] The arguments defining the assignment. The last element of this array
249
+ # is always the replacement value.
250
+ # - `(index, replacement)` for single assignment.
251
+ # - `(start_index, length, replacement)` for slice replacement.
252
+ # - `(range, replacement)` for range replacement.
253
+ # @return [Object, Array, nil] The value(s) assigned, or nil if the assignment failed
254
+ # due to an invalid out-of-bounds single index.
255
+ def []=(*args)
256
+ replacement = args.pop
257
+
258
+ # 1. Handle Single Index Assignment (e.g., list[2] = 'a')
259
+ if args.size == 1 && args[0].is_a?(Integer)
260
+ index = args[0]
261
+ index += @size if index.negative?
262
+ # Check bounds for simple assignment (Must be within 0 to size-1)
263
+ return nil unless index >= 0 && index < @size
264
+
265
+ # Simple assignment: O(n) lookup, O(1) set
266
+ node = find_node_at_index(index)
267
+ node.value = replacement
268
+ return replacement
269
+
270
+ # For out-of-bounds, Array compatibility is usually IndexError, but
271
+ # based on your design, we return nil
272
+
273
+ end
274
+
275
+ # 2. Handle Slice Replacement (list[2, 3] = [a, b] or list[2..4] = [a, b])
276
+ start_index, length = *args
277
+
278
+ if args.size == 1 && start_index.is_a?(Range)
279
+ range = start_index
280
+ start_index = range.begin
281
+ length = range.end - range.begin + (range.exclude_end? ? 0 : 1)
282
+ elsif args.size != 2 || !start_index.is_a?(Integer) || !length.is_a?(Integer)
283
+ return nil
284
+ end
285
+
286
+ start_index += @size if start_index.negative?
287
+
288
+ if start_index > @size
289
+ replacement = Array(replacement)
290
+ replacement.each { |val| append(val) }
291
+ return replacement
292
+ end
293
+
294
+ replacement = Array(replacement)
295
+
296
+ # Find Boundaries
297
+ predecessor = start_index.positive? ? find_node_at_index(start_index - 1) : nil
298
+ current = predecessor ? predecessor.next : @head
299
+
300
+ deleted_count = 0
301
+ length.times do
302
+ break unless current
303
+
304
+ current = current.next
305
+ deleted_count += 1
306
+ end
307
+ successor = current
308
+
309
+ # Stage 1: DELETION (Relink the neighbors)
310
+ if predecessor
311
+ predecessor.next = successor
312
+ else
313
+ @head = successor
314
+ end
315
+
316
+ if successor
317
+ successor.prev = predecessor
318
+ else
319
+ @tail = predecessor
320
+ end
321
+ @size -= deleted_count
322
+
323
+ # Stage 2: INSERTION (Insert new nodes at the boundary)
324
+ insertion_point = predecessor
325
+ replacement.each do |value|
326
+ new_node = Node.new(value, insertion_point, successor)
327
+
328
+ if insertion_point
329
+ insertion_point.next = new_node
330
+ else
331
+ @head = new_node
332
+ end
333
+
334
+ insertion_point = new_node
335
+ @size += 1
336
+ end
337
+
338
+ # Stage 3: FINAL RELINKING (The last inserted node links back to the successor)
339
+ if successor
340
+ successor.prev = insertion_point
341
+ else
342
+ @tail = insertion_point
343
+ end
344
+
345
+ replacement # Return the set values
346
+ end
347
+
348
+ # Inserts a new element at the specified index.
349
+ #
350
+ # If the index is 0, this is equivalent to {#prepend} (O(1)).
351
+ # If the index is equal to or greater than the size, this is equivalent to {#append} (O(1)).
352
+ # For all other valid indices, this is an **O(n)** operation as it requires traversal
353
+ # to find the insertion point.
354
+ #
355
+ # Supports negative indices, where list.insert(-1, value) inserts before the last element.
356
+ #
357
+ # @param index [Integer] The index before which the new element should be inserted.
358
+ # @param value [Object] The value to be inserted.
359
+ # @return [DLinked::List] Returns the list instance for method chaining.
360
+ def insert(index, value)
361
+ if index.negative?
362
+ index += @size
363
+ index = 0 if index.negative?
364
+ end
365
+
366
+ return prepend(value) if index <= 0
367
+ return append(value) if index >= @size
368
+
369
+ current = find_node_at_index(index)
370
+
371
+ # Insert before current node (O(1) linking)
372
+ new_node = Node.new(value, current.prev, current)
373
+ current.prev.next = new_node
374
+ current.prev = new_node
375
+
376
+ @size += 1
377
+ self
378
+ end
379
+
380
+ # Deletes the *first* node that matches the given value and returns the value of the deleted element.
381
+ #
382
+ # This is an **O(n)** operation because it requires traversal to find the node.
383
+ # However, once the node is found, the relinking operation is O(1).
384
+ #
385
+ # @param value [Object] The value to search for and delete.
386
+ # @return [Object, nil] The value of the deleted element, or nil if the value was not found in the list.
387
+ def delete(value)
388
+ current = @head
389
+ while current
390
+ if current.value == value
391
+ delete_node(current)
392
+ return value
393
+ end
394
+ current = current.next
395
+ end
396
+ nil
397
+ end
398
+
399
+ # Concatenates the elements of another DLinked::List to the end of this list, modifying the current list.
400
+ #
401
+ # This is an **O(n)** operation, where n is the size of the *other* list, as it must traverse and link
402
+ # every element of the other list into the current list structure.
403
+ #
404
+ # @param other [DLinked::List] The list whose elements will be appended.
405
+ # @return [self] Returns the modified list instance.
406
+ def concat(other)
407
+ raise TypeError, "can't convert #{other.class} into DLinked::List" unless other.respond_to?(:each)
408
+ return self if other.empty?
409
+
410
+ other.each { |value| append(value) }
411
+ self
412
+ end
413
+
414
+ # Returns a new DLinked::List that is the concatenation of this list and another list.
415
+ #
416
+ # This is a non-destructive operation, meaning neither the current list nor the other list is modified.
417
+ # The complexity is **O(n + k)**, where n is the size of the current list and k is the size of the other list,
418
+ # as both must be traversed and copied into the new list.
419
+ #
420
+ # @param other [DLinked::List] The list to append to this one.
421
+ # @return [DLinked::List] A new list containing all elements from both lists.
422
+ def +(other)
423
+ new_list = self.class.new
424
+ each { |value| new_list.append(value) }
425
+ other.each { |value| new_list.append(value) }
426
+ new_list
427
+ end
428
+
429
+ # Extracts a slice of elements from the list, returning a new DLinked::List instance.
430
+ #
431
+ # Supports slicing via:
432
+ # 1. Start index and length (e.g., list.slice(1, 2))
433
+ # 2. Range (e.g., list.slice(1..3))
434
+ #
435
+ # This is an **O(n)** operation, where n is the traversal time to find the start point,
436
+ # plus the time to copy the slice elements into a new list.
437
+ #
438
+ # @param start [Integer, Range] The starting index or a Range object defining the slice.
439
+ # @param length [Integer, nil] The number of elements to include in the slice.
440
+ # @return [DLinked::List, nil] A new list containing the sliced elements, or nil if the slice is out of bounds.
441
+ def slice(start, length = nil)
442
+ # Handle Range Argument
443
+ if start.is_a?(Range) && length.nil?
444
+ range = start
445
+ start = range.begin
446
+ length = range.end - range.begin + (range.exclude_end? ? 0 : 1)
447
+ end
448
+
449
+ # 1. Resolve start index (including negative indices)
450
+ start += @size if start.negative?
451
+
452
+ return nil if start.negative? || start >= @size
453
+
454
+ if length.nil?
455
+ node = find_node_at_index(start)
456
+ new_list = self.class.new
457
+ new_list.append(node.value)
458
+ return new_list # Returns DLinked::List: [value]
459
+ end
460
+
461
+ # Handle negative length returning nil
462
+ return nil if length.negative?
463
+ return List.new if length.zero?
464
+
465
+ new_list = List.new
466
+ current = find_node_at_index(start)
467
+
468
+ count = 0
469
+ while current && count < length
470
+ new_list.append(current.value)
471
+ current = current.next
472
+ count += 1
473
+ end
474
+
475
+ new_list
476
+ end
477
+
478
+ # Extracts and removes a slice of elements from the list, returning a new list
479
+ # containing the removed elements.
480
+ #
481
+ # Supports destructive slicing via:
482
+ # 1. Start index and length (e.g., list.slice!(1, 2))
483
+ # 2. Range (e.g., list.slice!(1..3))
484
+ #
485
+ # The complexity is **O(n + k)**, where n is the traversal time to find the start point,
486
+ # and k is the number of elements removed/copied.
487
+ #
488
+ # @param args [Array] Arguments representing the slice: (start_index, length) or (range).
489
+ # @return [DLinked::List, nil] A new list containing the extracted and removed elements,
490
+ # or nil if the slice is empty or invalid.
491
+ def slice!(*args)
492
+ start_index, length = *args
493
+
494
+ if args.size == 1 && start_index.is_a?(Range)
495
+ range = start_index
496
+ start_index = range.begin
497
+ length = range.end - range.begin + (range.exclude_end? ? 0 : 1)
498
+ elsif args.size == 1
499
+ length = 1
500
+ elsif args.size != 2 || length < 1
501
+ return nil
502
+ end
503
+
504
+ start_index += @size if start_index.negative?
505
+
506
+ return nil if start_index >= @size || length <= 0
507
+
508
+ predecessor = start_index.positive? ? find_node_at_index(start_index - 1) : nil
509
+ current = predecessor ? predecessor.next : @head
510
+
511
+ length.times do
512
+ break unless current
513
+
514
+ current = current.next
515
+ end
516
+ successor = current
517
+
518
+ result = self.class.new
519
+ slice_node = predecessor ? predecessor.next : @head
520
+
521
+ if predecessor
522
+ predecessor.next = successor
523
+ else
524
+ @head = successor
525
+ end
526
+
527
+ if successor
528
+ successor.prev = predecessor
529
+ else
530
+ @tail = predecessor
531
+ end
532
+
533
+ removed_count = 0
534
+ while slice_node != successor
535
+ next_node = slice_node.next
536
+ result.append(slice_node.value)
537
+ slice_node.prev = nil
538
+ slice_node.next = nil
539
+ slice_node = next_node
540
+ removed_count += 1
541
+ end
542
+
543
+ @size -= removed_count
544
+ result.empty? ? nil : result
545
+ end
546
+
547
+ private
548
+
549
+ # O(n/2) - Internal helper method to find the node at a valid index.
550
+ def find_node_at_index(index)
551
+ # Optimization: Start from head or tail, whichever is closer
552
+ if index <= @size / 2
553
+ current = @head
554
+ index.times { current = current.next }
555
+ else
556
+ current = @tail
557
+ (@size - 1 - index).times { current = current.prev }
558
+ end
559
+ current
560
+ end
561
+
562
+ # O(1) - Internal method to delete a specific node
563
+ def delete_node(node)
564
+ if node.prev
565
+ node.prev.next = node.next
566
+ else
567
+ @head = node.next
568
+ end
569
+
570
+ if node.next
571
+ node.next.prev = node.prev
572
+ else
573
+ @tail = node.prev
574
+ end
575
+
576
+ @size -= 1
577
+ end
578
+ end
579
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DLinked
4
+ VERSION = '0.1.1'
5
+ end
data/lib/dlinked.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'd_linked/version'
4
+ require_relative 'd_linked/list'
5
+
6
+ module DLinked
7
+ # The DLinked module serves as the namespace for the gem.
8
+ end