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.
- checksums.yaml +7 -0
- data/.github/workflows/release.yml +122 -0
- data/.gitignore +83 -0
- data/.rubocop.yml +45 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +221 -0
- data/Rakefile +10 -0
- data/benchmark.rb +54 -0
- data/dlinked.gemspec +37 -0
- data/example.rb +44 -0
- data/lib/d_linked/list/node.rb +42 -0
- data/lib/d_linked/list.rb +579 -0
- data/lib/d_linked/version.rb +5 -0
- data/lib/dlinked.rb +8 -0
- metadata +156 -0
|
@@ -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
|