-
Notifications
You must be signed in to change notification settings - Fork 927
/
linked-list.js
330 lines (294 loc) · 9.18 KB
/
linked-list.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
const util = require('util');
const Node = require('./node'); // Doubly
// tag::constructor[]
/**
* Doubly linked list that keeps track of
* the last and first element
*/
class LinkedList {
constructor(
iterable = [],
// end::constructor[]
ListNode = Node, // Node class (e.g. singly, doubly, multilevel)
// tag::constructor[]
) {
this.first = null; // head/root element
this.last = null; // last element of the list
this.size = 0; // total number of elements in the list
// end::constructor[]
this.ListNode = ListNode; // ListNode class
// tag::constructor[]
Array.from(iterable, (i) => this.addLast(i));
}
// end::constructor[]
// tag::addFirst[]
/**
* Adds element to the begining of the list. Similar to Array.unshift
* Runtime: O(1)
* @param {Node} value
*/
addFirst(value) {
const newNode = new this.ListNode(value);
newNode.next = this.first;
if (this.first) { // check if first node exists (list not empty)
this.first.previous = newNode; // <1>
} else { // if list is empty, first & last will point to newNode.
this.last = newNode;
}
this.first = newNode; // update head
this.size += 1;
return newNode;
}
// end::addFirst[]
// tag::addLast[]
/**
* Adds element to the end of the list (tail). Similar to Array.push
* Using the element last reference instead of navigating through the list,
* we can reduced from linear to a constant runtime.
* Runtime: O(1)
* @param {any} value node's value
* @returns {Node} newly created node
*/
addLast(value) {
const newNode = new Node(value);
if (this.first) { // check if first node exists (list not empty)
newNode.previous = this.last;
this.last.next = newNode;
this.last = newNode;
} else { // if list is empty, first & last will point to newNode.
this.first = newNode;
this.last = newNode;
}
this.size += 1;
return newNode;
}
// end::addLast[]
// tag::addMiddle[]
/**
* Insert new element at the given position (index)
*
* Runtime: O(n)
*
* @param {any} value new node's value
* @param {Number} position position to insert element
* @returns {Node|undefined} new node or 'undefined' if the index is out of bound.
*/
addAt(value, position = 0) {
if (position === 0) return this.addFirst(value); // <1>
if (position === this.size) return this.addLast(value); // <2>
// Adding element in the middle
const current = this.findBy({ index: position }).node;
if (!current) return undefined; // out of bound index
const newNode = new Node(value); // <3>
newNode.previous = current.previous; // <4>
newNode.next = current; // <5>
current.previous.next = newNode; // <6>
current.previous = newNode; // <7>
this.size += 1;
return newNode;
}
// end::addMiddle[]
// tag::searchByValue[]
/**
* @deprecated use findBy
* Search by value. It finds first occurrence of
* the position of element matching the value.
* Similar to Array.indexOf.
*
* Runtime: O(n)
*
* @example: assuming a linked list with: a -> b -> c
* linkedList.getIndexByValue('b') // ↪️ 1
* linkedList.getIndexByValue('z') // ↪️ undefined
* @param {any} value
* @returns {number} return index or undefined
*/
getIndexByValue(value) {
return this.findBy({ value }).index;
}
// end::searchByValue[]
// tag::searchByIndex[]
/**
* @deprecated use findBy directly
* Search by index
* Runtime: O(n)
* @example: assuming a linked list with: a -> b -> c
* linkedList.get(1) // ↪️ 'b'
* linkedList.get(40) // ↪️ undefined
* @param {Number} index position of the element
* @returns {Node|undefined} element at the specified position in
* this list or undefined if was not found.
*/
get(index = 0) {
return this.findBy({ index }).node;
}
// end::searchByIndex[]
// tag::find[]
/**
* Find by index or by value, whichever happens first.
* Runtime: O(n)
* @example
* this.findBy({ index: 10 }).node; // node at index 10.
* this.findBy({ value: 10 }).node; // node with value 10.
* this.findBy({ value: 10 }).index; // node's index with value 10.
*
* @param {Object} params - The search params
* @param {number} params.index - The index/position to search for.
* @param {any} params.value - The value to search for.
* @returns {{node: any, index: number}}
*/
findBy({ value, index = Infinity } = {}) {
for (let current = this.first, position = 0; // <1>
current && position <= index; // <2>
position += 1, current = current.next) { // <3>
if (position === index || value === current.value) { // <4>
return { node: current, index: position }; // <5>
}
}
return {}; // not found
}
// end::find[]
// tag::removeFirst[]
/**
* Removes element from the start of the list (head/root).
* Similar to Array.shift().
* Runtime: O(1)
* @returns {any} the first element's value which was removed.
*/
removeFirst() {
if (!this.first) return null; // Check if list is already empty.
const head = this.first;
this.first = head.next; // move first pointer to the next element.
if (this.first) {
this.first.previous = null;
} else { // if list has size zero, then we need to null out last.
this.last = null;
}
this.size -= 1;
return head.value;
}
// end::removeFirst[]
// tag::removeLast[]
/**
* Removes element to the end of the list.
* Similar to Array.pop().
* Runtime: O(1)
* @returns {any} the last element's value which was removed
*/
removeLast() {
if (!this.last) return null; // Check if list is already empty.
const tail = this.last;
this.last = tail.previous;
if (this.last) {
this.last.next = null;
} else { // if list has size zero, then we need to null out first.
this.first = null;
}
this.size -= 1;
return tail.value;
}
// end::removeLast[]
// tag::removeByPosition[]
/**
* Removes the element at the given position (index) in this list.
* Runtime: O(n)
* @param {any} position
* @returns {any} the element's value at the specified position that was removed.
*/
removeByPosition(position = 0) {
if (position === 0) return this.removeFirst();
if (position === this.size - 1) return this.removeLast();
const current = this.findBy({ index: position }).node;
if (!current) return null;
current.previous.next = current.next;
current.next.previous = current.previous;
this.size -= 1;
return current && current.value;
}
// end::removeByPosition[]
/**
* Remove element by Node
* O(1)
*/
removeByNode(node) {
if (!node) { return null; }
if (node === this.first) {
return this.removeFirst();
}
if (node === this.last) {
return this.removeLast();
}
node.previous.next = node.next;
node.next.previous = node.previous;
this.size -= 1;
return node.value;
}
/**
* Iterate through the list yield on each node
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators#User-defined_iterables
*/
* [Symbol.iterator]() {
for (let node = this.first; node; node = node.next) {
yield node;
}
}
toString() {
const parts = [...this]; // see [Symbol.iterator]()
return parts.map((n) => util.inspect(n.value)).join(' -> ');
}
/**
* Alias for size
*/
get length() {
return this.size;
}
/**
* @deprecated use findBy
* Iterate through the list until callback returns a truthy value
* @example see #get and #getIndexByValue
* @param {Function} callback evaluates current node and index.
* If any value other than undefined it's returned it will stop the search.
* @returns {any} callbacks's return value or undefined
*/
find(callback) {
for (let current = this.first, position = 0; // <1>
current; // <2>
position += 1, current = current.next) { // <3>
const result = callback(current, position); // <4>
if (result !== undefined) {
return result; // <5>
}
}
return undefined; // not found
}
/**
* @deprecated use removeByNode or removeByPosition
* Removes the first occurrence of the specified elementt
* from this list, if it is present.
* Runtime: O(n)
* @param {any} callbackOrIndex callback or position index to remove
*/
remove(callbackOrIndex) {
if (typeof callbackOrIndex !== 'function') {
return this.removeByPosition(parseInt(callbackOrIndex, 10) || 0);
}
// find desired position to remove using #find
const position = this.find((node, index) => {
if (callbackOrIndex(node, index)) {
return index;
}
return undefined;
});
if (position !== undefined) { // zero-based position.
return this.removeByPosition(position);
}
return false;
}
}
// Aliases
LinkedList.prototype.push = LinkedList.prototype.addLast;
LinkedList.prototype.pop = LinkedList.prototype.removeLast;
LinkedList.prototype.unshift = LinkedList.prototype.addFirst;
LinkedList.prototype.shift = LinkedList.prototype.removeFirst;
LinkedList.prototype.search = LinkedList.prototype.contains;
module.exports = LinkedList;