@d1g1tal/collections
Version:
JavaScript Collections ES Modules
384 lines (338 loc) • 10.6 kB
text/typescript
import { Node } from './node';
type LinkedListType = (typeof LinkedList.Type)[keyof typeof LinkedList.Type];
/**
* JavaScript implementation of a LinkedList.
*
* @template E
* @type {LinkedList<E>}
*/
export class LinkedList<E> {
private $head: Node<E> | null = null;
private $tail: Node<E> | null = null;
private $size: number = 0;
private $doublyLinked: boolean;
static Type = { Singly: 'singly', Doubly: 'doubly' } as const;
/**
* Creates a new LinkedList.
*
* @param {LinkedListType} type The type of the list ('singly' or 'doubly' linked).
*/
constructor(type: LinkedListType = LinkedList.Type.Singly) {
this.$doublyLinked = type == LinkedList.Type.Doubly;
}
/**
* Adds an element to the start of the list.
*
* @param {E} value The element to add.
* @returns {void}
*/
addFirst(value: E): void {
const node = new Node({ next: this.$head, value });
if (this.$doublyLinked && this.$head) { this.$head.previous = node }
this.$head = node;
if (!this.$tail) { this.$tail = node }
this.$size++;
}
/**
* Adds an element to the end of the list.
*
* @param {E} value The element to add.
* @returns {void}
*/
addLast(value: E): void {
const node = new Node({ value, previous: this.$doublyLinked ? this.$tail : null });
if (this.$tail) { this.$tail.next = node }
this.$tail = node;
if (!this.$head) { this.$head = node }
this.$size++;
}
/**
* Gets the first node in the list.
*
* @returns {E|null} The first node in the list, or null if the list is empty.
*/
getFirst(): E | null {
return this.$head?.value ?? null;
}
/**
* Gets the last node in the list.
*
* @returns {E|null} The last node in the list, or null if the list is empty.
*/
getLast(): E | null {
return this.$tail?.value ?? null;
}
/**
* Removes the first element from the list.
*
* @returns {E | null} The removed element, or null if the list was empty.
*/
removeFirst(): E | null {
return this.removeNode(this.$head);
}
/**
* Removes the last element from the list.
*
* @returns {E | null} The removed element, or null if the list was empty.
*/
removeLast(): E | null {
return this.removeNode(this.$tail);
}
/**
* Removes the first occurrence of an element from the list.
*
* @param {E} value The element to remove.
* @returns {E|null} The removed element, or null if the element was not found.
*/
remove(value: E): E | null {
for (let node = this.$head; node; node = node.next) {
if (node.value === value) { return this.removeNode(node) }
}
return null;
}
/**
* Gets the value of the node at the specified index.
*
* @param {number} index The index of the element to remove.
* @returns {E|null} The removed element, or null if the index was out of bounds.
*/
get(index: number): E | null {
return this.getNodeAt(index)?.value ?? null;
}
/**
* Sets the value of the node at the specified index.
* Replaces the value of the node at the specified index with the specified value.
*
* @param {number} index The index of the element to set.
* @param {E} value The new value of the element.
* @returns {void}
* @throws {RangeError} If the index is out of bounds.
*/
set(index: number, value: E): void {
const node = this.getNodeAt(index);
if (!node) { throw new RangeError('Index out of bounds') }
node.value = value;
}
/**
* Inserts an element at the specified index.
* Shifts the element currently at that position (if any) and any subsequent elements to the right (adds one to their indices).
* If the index is equal to the size of the list, the element is added to the end of the list.
* If the index is 0, the element is added to the start of the list.
* Otherwise, the element is inserted at the specified index.
*
* @param {number} index The index at which to insert the element.
* @param {E} value The element to insert.
* @returns {void}
* @throws {RangeError} If the index is out of bounds.
*/
insert(index: number, value: E): void {
if (index < 0 || index > this.$size) { throw new RangeError('Index out of bounds') }
if (index === 0) {
this.addFirst(value);
} else if (index === this.$size) {
this.addLast(value);
} else {
// TODO: Fix ! assertion
const prevNode = this.getNodeAt(index - 1)!;
prevNode.next = new Node({ value, previous: this.$doublyLinked ? prevNode : null, next: prevNode.next });
this.$size++;
}
}
/**
* Checks if the list contains the specified element.
*
* @param {E} value The element to check for.
* @returns {boolean} True if the list contains the element, false otherwise.
*/
contains(value: E): boolean {
for (let node = this.$head; node; node = node.next) {
if (node.value === value) return true;
}
return false;
}
/**
* Reverses the list.
* The first element becomes the last, and the last element becomes the first.
* This method runs in linear time.
*
* @returns {void}
*/
reverse(): void {
let node = this.$head;
let prev = null;
while (node) {
const next = node.next;
node.next = prev;
if (prev) prev.previous = node;
prev = node;
node = next;
}
[this.$head, this.$tail] = [this.$tail, this.$head];
}
/**
* Removes all elements from the list.
* The list will be empty after this call returns.
* This method runs in linear time.
*
* @returns {void}
*/
clear(): void {
for (let node = this.$head; node; node = node.next) { node.unlink() }
this.$head = this.$tail = null;
this.$size = 0;
}
/**
* Checks if the list is empty.
*
* @returns {boolean} True if the list is empty, false otherwise.
*/
isEmpty(): boolean {
return this.$size === 0;
}
/**
* Gets the index of the value in the list.
* If the value is not found, -1 is returned.
* If the value is found multiple times, the index of the first occurrence is returned.
* This method runs in linear time.
*
* @param {E} value The value to search for.
* @returns {number} The index of the value in the list, or -1 if the value is not found.
*/
indexOf(value: E): number {
for (let index = 0, node = this.$head; node; node = node.next, index++) {
if (node.value === value) return index;
}
return -1;
}
/**
* Iterates over the list and calls the specified consumer function for each element.
* The consumer function is called with three arguments: the value of the element, the index of the element, and the list itself.
*
* @param {function(E, number, LinkedList<E>): void} consumer The consumer function to call for each element.
* @param {Object} [context] The context to call the consumer function in.
* @returns {void}
*/
forEach(consumer: (arg0: E, arg1: number, arg2: LinkedList<E>) => void, context: object = this): void {
for (let index = 0, node = this.$head; node; node = node.next, index++) {
consumer.call(context, node.value, index, this);
}
}
/**
* Returns an iterator over the values in the list.
* The values are returned in order from the first to the last element.
* This method runs in constant time.
* The returned iterator is fail-fast.
* Modifying the list after getting the iterator, except through the iterator's own methods, will throw an error.
*
* @yields {Iterable<E>} An iterator over the values in the list.
*/
*values(): Generator<E, void, unknown> {
yield* this;
}
/**
* Gets the size of the list.
*
* @readonly
* @returns {number} The size of the list.
*/
get size(): number {
return this.$size;
}
/**
* Gets an array containing all the values in the list.
* The values are returned in order from the first to the last element.
* This method runs in linear time.
* The returned array is a shallow copy of the list.
* Modifying the array will not modify the list.
*
* @returns {E[]} An array containing all the values in the list.
*/
toArray(): E[] {
return Array.from(this.values());
}
/**
* Returns an iterator over the values in the list.
* The values are returned in order from the first to the last element.
* This method runs in constant time.
* The returned iterator is fail-fast.
* Modifying the list after getting the iterator, except through the iterator's own methods, will throw an error.
* The iterator does not support modifying the list during iteration.
* This method is called when the list is used in a for-of loop.
*
* @yields {Iterable<E>} An iterator over the values in the list.
* @example
* ````js
* for (const value of list) {
* console.log(value);
* }
* ````
*/
*[Symbol.iterator](): Generator<E, void, unknown> {
for (let node = this.$head; node; node = node.next) {
yield node.value;
}
}
/**
* Returns a string description of the list.
*
* @returns {string} A string description of the list.
*/
get [Symbol.toStringTag](): string {
return 'LinkedList';
}
/**
* Gets the node at the specified index.
* If the index is out of bounds, null is returned.
*
* @private
* @param {number} index The index of the node to get.
* @returns {Node<E>} The node at the specified index, or null if the index is out of bounds.
*/
private getNodeAt(index: number): Node<E> | null {
if (index < 0 || index >= this.$size) { return null }
let node: Node<E> | null = null;
if (this.$doublyLinked && index >= this.$size / 2) {
node = this.$tail;
for (let i = this.$size - 1; i > index; i--) {
// TODO: Remove ugly TypeScript ! assertion
node = node!.previous;
}
} else {
node = this.$head;
for (let i = 0; i < index; i++) {
// TODO: Remove ugly TypeScript ! assertion
node = node!.next;
}
}
return node;
}
/**
* Removes a node from the list.
*
* @private
* @param {Node<E>} node The node to remove.
* @returns {E} The value of the removed node.
*/
private removeNode(node: Node<E> | null): E | null {
if (node === null) { return null }
const value = node.value;
// If the node to be removed is the only node in the list
if (node === this.$head && node === this.$tail) {
this.$head = this.$tail = null;
} else {
// Update head or tail reference and unlink the node
if (node === this.$head) {
this.$head = node.next;
} else if (node === this.$tail) {
this.$tail = this.$doublyLinked ? node.previous : null;
if (this.$tail) this.$tail.next = null;
} else {
node.unlink();
}
}
this.$size--;
// If there's only one node left, make sure head and tail point to it
if (this.$size === 1) { this.$tail = this.$head }
return value;
}
}