diff --git a/package-lock.json b/package-lock.json index 750f400..562f4dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,11 +19,11 @@ "@typescript-eslint/eslint-plugin": "^8.12.1", "@typescript-eslint/parser": "^8.12.1", "auto-changelog": "^2.5.0", - "avl-tree-typed": "^1.54.2", + "avl-tree-typed": "^1.54.3", "benchmark": "^2.1.4", - "binary-tree-typed": "^1.54.2", - "bst-typed": "^1.54.2", - "data-structure-typed": "^1.54.2", + "binary-tree-typed": "^1.54.3", + "bst-typed": "^1.54.3", + "data-structure-typed": "^1.54.3", "dependency-cruiser": "^16.5.0", "doctoc": "^2.2.1", "eslint": "^9.13.0", @@ -32,7 +32,7 @@ "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-import": "^2.31.0", "fast-glob": "^3.3.2", - "heap-typed": "^1.54.2", + "heap-typed": "^1.54.3", "istanbul-badges-readme": "^1.9.0", "jest": "^29.7.0", "js-sdsl": "^4.4.2", @@ -3450,13 +3450,13 @@ } }, "node_modules/avl-tree-typed": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/avl-tree-typed/-/avl-tree-typed-1.54.2.tgz", - "integrity": "sha512-bp+MMq2CpASeAB7up5kggHZhcqxx3sKdd5AJ8aWMqabZin13BxRnzwRY/II9gp4hb4Tvtitw6AYfJliGcdWYyQ==", + "version": "1.54.3", + "resolved": "https://registry.npmjs.org/avl-tree-typed/-/avl-tree-typed-1.54.3.tgz", + "integrity": "sha512-lJYYqt0rdS6iPDhh7hbaMGkxur9/GJLljas1LqojMZbLmUabVx4T4MszdsCghJy94wThlXN+jjMeCxVB4k4XRw==", "dev": true, "license": "MIT", "dependencies": { - "data-structure-typed": "^1.54.2" + "data-structure-typed": "^1.54.3" } }, "node_modules/babel-jest": { @@ -3615,13 +3615,13 @@ } }, "node_modules/binary-tree-typed": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/binary-tree-typed/-/binary-tree-typed-1.54.2.tgz", - "integrity": "sha512-U6qW0bIh6o2qGz8OzWzUHnNwXa6HjYp0I5Ds4VURqtl4OEOeJh57cNOlKgbiEHZ5viai7S5aRqXIZHW6vStezA==", + "version": "1.54.3", + "resolved": "https://registry.npmjs.org/binary-tree-typed/-/binary-tree-typed-1.54.3.tgz", + "integrity": "sha512-azYUQ8qCx5JHKEqZvaxKpSWN7BrYccW729CIc61G7/0ECCzw8AJxYL1jHIzfYTUJgRxGxbzsCkRMjaKaVtuD5w==", "dev": true, "license": "MIT", "dependencies": { - "data-structure-typed": "^1.54.2" + "data-structure-typed": "^1.54.3" } }, "node_modules/brace-expansion": { @@ -3705,13 +3705,13 @@ } }, "node_modules/bst-typed": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/bst-typed/-/bst-typed-1.54.2.tgz", - "integrity": "sha512-hl49IvHT/uJMphP1AL6sifiCKVc0pCdsGG8PW5ealU2K0LO9kO3nMBU+FcfwsmMcvhRrhikW/1fC63c9X7Lq+Q==", + "version": "1.54.3", + "resolved": "https://registry.npmjs.org/bst-typed/-/bst-typed-1.54.3.tgz", + "integrity": "sha512-4E4HWana/ON/KZSrOPghMhF81H9G7U9nt9nXvJx2CKCc30qAH8rC/RC9gtxyLoB1QziUFHL73ZNyWORP1wR3cQ==", "dev": true, "license": "MIT", "dependencies": { - "data-structure-typed": "^1.54.2" + "data-structure-typed": "^1.54.3" } }, "node_modules/buffer-from": { @@ -4083,9 +4083,9 @@ } }, "node_modules/data-structure-typed": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/data-structure-typed/-/data-structure-typed-1.54.2.tgz", - "integrity": "sha512-H4Ct6XqKsGk7O6/6mb9MHsZdrp5fAYHTgv2Tb+LrbTHnbenKu2ZfM0wP7fbrkhrRPzCFFvZKKDTfSatgykolmw==", + "version": "1.54.3", + "resolved": "https://registry.npmjs.org/data-structure-typed/-/data-structure-typed-1.54.3.tgz", + "integrity": "sha512-hAJ+2cbsfAWMHNP3lupdM9Fji3nqu2jQECWlJ9Hcy7g/Iwu90Xj0YwN3qfTlae86fnMZniv3nVWoeuEhTpMWng==", "dev": true, "license": "MIT" }, @@ -5904,13 +5904,13 @@ } }, "node_modules/heap-typed": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/heap-typed/-/heap-typed-1.54.2.tgz", - "integrity": "sha512-BEgfIZndyoxN4XRY9kWSS8217u6nt4GuWrBWBnNxZwp8EVyhExS/CA3NsOH8xzj3mgsxqU88LdWwGFpQqBxMbA==", + "version": "1.54.3", + "resolved": "https://registry.npmjs.org/heap-typed/-/heap-typed-1.54.3.tgz", + "integrity": "sha512-XdfkyJuiKnXrfV8vSGNonu+7nON35BuP7/EBt7m8hIipsgxQT4d21pgCwNhsU1TH5xawV9sr6P1GSZkqrQOMZA==", "dev": true, "license": "MIT", "dependencies": { - "data-structure-typed": "^1.54.2" + "data-structure-typed": "^1.54.3" } }, "node_modules/html-escaper": { diff --git a/package.json b/package.json index f2092af..81afec8 100644 --- a/package.json +++ b/package.json @@ -65,11 +65,11 @@ "@typescript-eslint/eslint-plugin": "^8.12.1", "@typescript-eslint/parser": "^8.12.1", "auto-changelog": "^2.5.0", - "avl-tree-typed": "^1.54.2", + "avl-tree-typed": "^1.54.3", "benchmark": "^2.1.4", - "binary-tree-typed": "^1.54.2", - "bst-typed": "^1.54.2", - "data-structure-typed": "^1.54.2", + "binary-tree-typed": "^1.54.3", + "bst-typed": "^1.54.3", + "data-structure-typed": "^1.54.3", "dependency-cruiser": "^16.5.0", "doctoc": "^2.2.1", "eslint": "^9.13.0", @@ -78,7 +78,7 @@ "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-import": "^2.31.0", "fast-glob": "^3.3.2", - "heap-typed": "^1.54.2", + "heap-typed": "^1.54.3", "istanbul-badges-readme": "^1.9.0", "jest": "^29.7.0", "js-sdsl": "^4.4.2", diff --git a/src/data-structures/base/linear-base.ts b/src/data-structures/base/linear-base.ts index 3ec146e..1e8ba8f 100644 --- a/src/data-structures/base/linear-base.ts +++ b/src/data-structures/base/linear-base.ts @@ -1,7 +1,45 @@ import { ElementCallback, LinearBaseOptions, ReduceLinearCallback } from '../../types'; import { IterableElementBase } from './iterable-element-base'; -export abstract class LinearBase extends IterableElementBase { +export class LinkedListNode { + constructor(value: E) { + this._value = value; + this._next = undefined; + } + + protected _value: E; + + get value(): E { + return this._value; + } + + set value(value: E) { + this._value = value; + } + + protected _next: LinkedListNode | undefined; + + get next(): LinkedListNode | undefined { + return this._next; + } + + set next(value: LinkedListNode | undefined) { + this._next = value; + } +} + +export abstract class LinearBase< + E, + R = any, + NODE extends LinkedListNode = LinkedListNode +> extends IterableElementBase { + /** + * The constructor initializes the LinearBase class with optional options, setting the maximum length + * if provided. + * @param [options] - The `options` parameter is an optional object that can be passed to the + * constructor. It is of type `LinearBaseOptions`. The constructor checks if the `options` + * object is provided and then extracts the `maxLen` property from it. If `maxLen` is a + */ protected constructor(options?: LinearBaseOptions) { super(options); if (options) { @@ -103,7 +141,7 @@ export abstract class LinearBase extends IterableElement return -1; } - concat(...items: LinearBase[]): this; + concat(...items: this[]): this; /** * Time Complexity: O(n + m) @@ -111,12 +149,12 @@ export abstract class LinearBase extends IterableElement * * The `concat` function in TypeScript concatenates multiple items into a new list, handling both * individual elements and instances of `LinearBase`. - * @param {(E | LinearBase)[]} items - The `concat` method takes in an array of items, where + * @param {(E | this)[]} items - The `concat` method takes in an array of items, where * each item can be either of type `E` or an instance of `LinearBase`. * @returns The `concat` method is returning a new instance of the class that it belongs to, with the * items passed as arguments concatenated to it. */ - concat(...items: (E | LinearBase)[]): this { + concat(...items: (E | this)[]): this { const newList = this.clone(); for (const item of items) { @@ -171,30 +209,27 @@ export abstract class LinearBase extends IterableElement * during the operation. */ splice(start: number, deleteCount: number = 0, ...items: E[]): this { - start = start < 0 ? this.length + start : start; - if (start < 0) start = 0; - if (start > this.length) start = this.length; - - const removedList = this._createInstance({ toElementFn: this._toElementFn }); + const removedList = this._createInstance(); - // Move to starting position - let currentNode = this.at(start); - let currentIndex = start; + // Handling negative indexes and bounds + start = start < 0 ? this.length + start : start; + start = Math.max(0, Math.min(start, this.length)); + deleteCount = Math.max(0, Math.min(deleteCount, this.length - start)); // Delete elements - for (let i = 0; i < deleteCount && currentNode !== undefined; i++) { - removedList.push(currentNode); - this.delete(currentNode); - currentNode = this.at(currentIndex); // Update node reference + for (let i = 0; i < deleteCount; i++) { + const removed = this.deleteAt(start); // Always delete the start position + if (removed !== undefined) { + removedList.push(removed); // Add removed elements to the returned list + } } // Insert new element - for (const item of items) { - this.addAt(currentIndex, item); - currentIndex++; + for (let i = 0; i < items.length; i++) { + this.addAt(start + i, items[i]); // Insert new elements one by one at the current position } - return removedList; + return removedList; // Returns a list of removed elements } /** @@ -337,7 +372,7 @@ export abstract class LinearBase extends IterableElement abstract at(index: number): E | undefined; - abstract deleteAt(pos: number): boolean; + abstract deleteAt(pos: number): E | undefined; abstract addAt(index: number, newElementOrNode: E | NODE): boolean; @@ -346,7 +381,18 @@ export abstract class LinearBase extends IterableElement protected abstract _getReverseIterator(...args: any[]): IterableIterator; } -export abstract class LinearLinkedBase extends LinearBase { +export abstract class LinearLinkedBase< + E, + R = any, + NODE extends LinkedListNode = LinkedListNode +> extends LinearBase { + /** + * The constructor initializes the LinearBase class with optional options, setting the maximum length + * if provided and valid. + * @param [options] - The `options` parameter is an optional object that can be passed to the + * constructor. It is of type `LinearBaseOptions`. This object may contain properties such as + * `maxLen`, which is a number representing the maximum length. If `maxLen` is a positive integer, + */ protected constructor(options?: LinearBaseOptions) { super(options); if (options) { @@ -458,6 +504,41 @@ export abstract class LinearLinkedBase extends LinearBas return newList; } + /** + * Time Complexity: O(m) + * Space Complexity: O(m) + * + * The `slice` method is overridden to improve performance by creating a new instance and iterating + * through the array to extract a subset based on the specified start and end indices. + * @param {number} [start=0] - The `start` parameter in the `slice` method specifies the index at + * which to begin extracting elements from the array. If no `start` parameter is provided, the + * default value is 0, indicating that extraction should start from the beginning of the array. + * @param {number} end - The `end` parameter in the `slice` method represents the index at which to + * end the slicing of the array. If not provided, it defaults to the length of the array. + * @returns The `slice` method is returning a new instance of the array implementation with elements + * sliced from the original array based on the `start` and `end` parameters. + */ + override slice(start: number = 0, end: number = this.length): this { + // In order to improve performance, it is best to override this method in the subclass of the array implementation + start = start < 0 ? this.length + start : start; + end = end < 0 ? this.length + end : end; + + const newList = this._createInstance(); + const iterator = this._getIterator(); + let current = iterator.next(); + let c = 0; + while (c < start) { + current = iterator.next(); + c++; + } + for (let i = start; i < end; i++) { + newList.push(current.value); + current = iterator.next(); + } + + return newList; + } + /** * Time Complexity: O(n + m) * Space Complexity: O(m) @@ -478,27 +559,45 @@ export abstract class LinearLinkedBase extends LinearBas * elements provided in the `items` array. */ override splice(start: number, deleteCount: number = 0, ...items: E[]): this { - start = start < 0 ? this.length + start : start; - if (start < 0) start = 0; - if (start > this.length) start = this.length; + const removedList = this._createInstance(); // Used to store deleted elements - const removedList = this._createInstance({ toElementFn: this._toElementFn }); - - // Move to starting position - let currentNode = this.at(start); - let currentIndex = start; + // Handling negative indexes + start = start < 0 ? this.length + start : start; + start = Math.max(0, Math.min(start, this.length)); // Correct start range + deleteCount = Math.max(0, deleteCount); // Make sure deleteCount is non-negative + + let currentIndex = 0; + let currentNode: NODE | undefined = undefined; + let previousNode: NODE | undefined = undefined; + + // Find the starting point using an iterator + const iterator = this._getNodeIterator(); + for (const node of iterator) { + if (currentIndex === start) { + currentNode = node; // Find the starting node + break; + } + previousNode = node; // Update the previous node + currentIndex++; + } - // Delete elements - for (let i = 0; i < deleteCount && currentNode !== undefined; i++) { - removedList.push(currentNode); - this.delete(currentNode); - currentNode = this.at(currentIndex); // Update node reference + // Delete nodes + for (let i = 0; i < deleteCount && currentNode; i++) { + removedList.push(currentNode.value); // Store the deleted value in removedList + const nextNode = currentNode.next; // Save next node + this.delete(currentNode); // Delete current node + currentNode = nextNode as NODE; } - // Insert new element - for (const item of items) { - this.addAt(currentIndex, item); - currentIndex++; + // Insert new value + for (let i = 0; i < items.length; i++) { + if (previousNode) { + this.addAfter(previousNode, items[i]); // Insert after previousNode + previousNode = previousNode.next as NODE; // Move to newly inserted node + } else { + this.addAt(0, items[i]); // Insert at the head of the linked list + previousNode = this._getNodeIterator().next().value; // Update the head node to be the first inserted node + } } return removedList; @@ -534,38 +633,17 @@ export abstract class LinearLinkedBase extends LinearBas return accumulator; } - /** - * Time Complexity: O(m) - * Space Complexity: O(m) - * - * The `slice` method is overridden to improve performance by creating a new instance and iterating - * through the array to extract a subset based on the specified start and end indices. - * @param {number} [start=0] - The `start` parameter in the `slice` method specifies the index at - * which to begin extracting elements from the array. If no `start` parameter is provided, the - * default value is 0, indicating that extraction should start from the beginning of the array. - * @param {number} end - The `end` parameter in the `slice` method represents the index at which to - * end the slicing of the array. If not provided, it defaults to the length of the array. - * @returns The `slice` method is returning a new instance of the array implementation with elements - * sliced from the original array based on the `start` and `end` parameters. - */ - override slice(start: number = 0, end: number = this.length): this { - // In order to improve performance, it is best to override this method in the subclass of the array implementation - start = start < 0 ? this.length + start : start; - end = end < 0 ? this.length + end : end; + abstract override delete(elementOrNode: E | NODE | undefined): boolean; - const newList = this._createInstance(); - const iterator = this._getIterator(); - let current = iterator.next(); - let c = 0; - while (c < start) { - current = iterator.next(); - c++; - } - for (let i = start; i < end; i++) { - newList.push(current.value); - current = iterator.next(); - } + abstract addBefore(existingElementOrNode: E | NODE, newElementOrNode: E | NODE): boolean; - return newList; - } + abstract addAfter(existingElementOrNode: E | NODE, newElementOrNode: E | NODE): boolean; + + abstract getNodeAt(index: number): NODE | undefined; + + protected abstract _getNodeIterator(...args: any[]): IterableIterator; + + // protected abstract _getReverseNodeIterator(...args: any[]): IterableIterator; + + protected abstract _getPrevNode(node: NODE): NODE | undefined; } diff --git a/src/data-structures/linked-list/doubly-linked-list.ts b/src/data-structures/linked-list/doubly-linked-list.ts index c76b003..f905675 100644 --- a/src/data-structures/linked-list/doubly-linked-list.ts +++ b/src/data-structures/linked-list/doubly-linked-list.ts @@ -6,37 +6,28 @@ * @license MIT License */ import type { DoublyLinkedListOptions, ElementCallback } from '../../types'; -import { LinearLinkedBase } from '../base/linear-base'; +import { LinearLinkedBase, LinkedListNode } from '../base/linear-base'; -export class DoublyLinkedListNode { +export class DoublyLinkedListNode extends LinkedListNode { /** * The constructor function initializes the value, next, and previous properties of an object. * @param {E} value - The "value" parameter is the value that will be stored in the node. It can be of any data type, as it * is defined as a generic type "E". */ constructor(value: E) { + super(value); this._value = value; this._next = undefined; this._prev = undefined; } - protected _value: E; + protected override _next: DoublyLinkedListNode | undefined; - get value(): E { - return this._value; - } - - set value(value: E) { - this._value = value; - } - - protected _next: DoublyLinkedListNode | undefined; - - get next(): DoublyLinkedListNode | undefined { + override get next(): DoublyLinkedListNode | undefined { return this._next; } - set next(value: DoublyLinkedListNode | undefined) { + override set next(value: DoublyLinkedListNode | undefined) { this._next = value; } @@ -52,10 +43,11 @@ export class DoublyLinkedListNode { } /** - *1. Node Structure: Each node contains three parts: a data field, a pointer (or reference) to the previous node, and a pointer to the next node. This structure allows traversal of the linked list in both directions. + * 1. Node Structure: Each node contains three parts: a data field, a pointer (or reference) to the previous node, and a pointer to the next node. This structure allows traversal of the linked list in both directions. * 2. Bidirectional Traversal: Unlike singly linked lists, doubly linked lists can be easily traversed forwards or backwards. This makes insertions and deletions in the list more flexible and efficient. * 3. No Centralized Index: Unlike arrays, elements in a linked list are not stored contiguously, so there is no centralized index. Accessing elements in a linked list typically requires traversing from the head or tail node. * 4. High Efficiency in Insertion and Deletion: Adding or removing elements in a linked list does not require moving other elements, making these operations more efficient than in arrays. + * Caution: Although our linked list classes provide methods such as at, setAt, addAt, and indexOf that are based on array indices, their time complexity, like that of the native Array.lastIndexOf, is 𝑂(𝑛). If you need to use these methods frequently, you might want to consider other data structures, such as Deque or Queue (designed for random access). Similarly, since the native Array.shift method has a time complexity of 𝑂(𝑛), using an array to simulate a queue can be inefficient. In such cases, you should use Queue or Deque, as these data structures leverage deferred array rearrangement, effectively reducing the average time complexity to 𝑂(1). * @example * // text editor operation history * const actions = [ @@ -787,6 +779,7 @@ export class DoublyLinkedList extends LinearLinkedBase | ((node: DoublyLinkedListNode) => boolean) | undefined ): DoublyLinkedListNode | undefined { if (elementNodeOrPredicate === undefined) return; + if (this.isNode(elementNodeOrPredicate)) return elementNodeOrPredicate; const predicate = this._ensurePredicate(elementNodeOrPredicate); let current = this.head; @@ -948,15 +941,18 @@ export class DoublyLinkedList extends LinearLinkedBase= this._length) return false; + deleteAt(index: number): E | undefined { + if (index < 0 || index >= this._length) return; + let deleted: E | undefined; if (index === 0) { + deleted = this.first; this.shift(); - return true; + return deleted; } if (index === this._length - 1) { + deleted = this.last; this.pop(); - return true; + return deleted; } const removedNode = this.getNodeAt(index); @@ -965,7 +961,7 @@ export class DoublyLinkedList extends LinearLinkedBase extends LinearLinkedBase, thisArg?: any): DoublyLinkedList { - const filteredList = new DoublyLinkedList([], { toElementFn: this.toElementFn }); + const filteredList = this._createInstance({ toElementFn: this.toElementFn, maxLen: this._maxLen }); let index = 0; for (const current of this) { if (callback.call(thisArg, current, index, this)) { @@ -1157,7 +1153,7 @@ export class DoublyLinkedList extends LinearLinkedBase EM, thisArg?: any ): DoublyLinkedList { - const mappedList = new DoublyLinkedList([], { toElementFn }); + const mappedList = new DoublyLinkedList([], { toElementFn, maxLen: this._maxLen }); let index = 0; for (const current of this) { mappedList.push(callback.call(thisArg, current, index, this)); @@ -1205,6 +1201,40 @@ export class DoublyLinkedList extends LinearLinkedBase { + let current = this.tail; + + while (current) { + yield current.value; + current = current.prev; + } + } + + /** + * The function returns an iterator that iterates over the nodes of a doubly linked list starting + * from the head. + */ + protected *_getNodeIterator(): IterableIterator> { + let current = this.head; + + while (current) { + yield current; + current = current.next; + } + } + + // protected *_getReverseNodeIterator(): IterableIterator> { + // const reversedArr = [...this._getNodeIterator()].reverse(); + // + // for (const item of reversedArr) { + // yield item; + // } + // } + /** * The function `_isPredicate` checks if the input is a function that takes a `DoublyLinkedListNode` * as an argument and returns a boolean. @@ -1268,15 +1298,15 @@ export class DoublyLinkedList extends LinearLinkedBase`, which represents a node in a doubly linked list containing an element + * of type `E`. + * @returns The `_getPrevNode` method is returning the previous node of the input `node` in a doubly + * linked list. If the input node has a previous node, it will return that node. Otherwise, it will + * return `undefined`. */ - protected *_getReverseIterator(): IterableIterator { - let current = this.tail; - - while (current) { - yield current.value; - current = current.prev; - } + protected _getPrevNode(node: DoublyLinkedListNode): DoublyLinkedListNode | undefined { + return node.prev; } } diff --git a/src/data-structures/linked-list/singly-linked-list.ts b/src/data-structures/linked-list/singly-linked-list.ts index 9c24c50..b5f0b5d 100644 --- a/src/data-structures/linked-list/singly-linked-list.ts +++ b/src/data-structures/linked-list/singly-linked-list.ts @@ -6,60 +6,37 @@ * @license MIT License */ import type { ElementCallback, SinglyLinkedListOptions } from '../../types'; -import { LinearLinkedBase } from '../base/linear-base'; +import { LinearLinkedBase, LinkedListNode } from '../base/linear-base'; -export class SinglyLinkedListNode { +export class SinglyLinkedListNode extends LinkedListNode { /** * The constructor function initializes an instance of a class with a given value and sets the next property to undefined. * @param {E} value - The "value" parameter is of type E, which means it can be any data type. It represents the value that * will be stored in the node of a linked list. */ constructor(value: E) { + super(value); this._value = value; this._next = undefined; } - protected _value: E; + protected override _next: SinglyLinkedListNode | undefined; - /** - * The function returns the value of a protected variable. - * @returns The value of the variable `_value` is being returned. - */ - get value(): E { - return this._value; - } - - /** - * The above function sets the value of a variable. - * @param {E} value - The parameter "value" is of type E, which means it can be any type. - */ - set value(value: E) { - this._value = value; - } - - protected _next: SinglyLinkedListNode | undefined; - - /** - * The `next` function returns the next node in a singly linked list. - * @returns The `next` property is being returned. It can be either a `SinglyLinkedListNode` - * object or `undefined`. - */ - get next(): SinglyLinkedListNode | undefined { + override get next(): SinglyLinkedListNode | undefined { return this._next; } - /** - * The "next" property of a SinglyLinkedListNode is set to the provided value. - * @param {SinglyLinkedListNode | undefined} value - The `value` parameter is of type - * `SinglyLinkedListNode | undefined`. This means that it can accept either a - * `SinglyLinkedListNode` object or `undefined` as its value. - */ - set next(value: SinglyLinkedListNode | undefined) { + override set next(value: SinglyLinkedListNode | undefined) { this._next = value; } } /** + * 1. Node Structure: Each node contains three parts: a data field, a pointer (or reference) to the previous node, and a pointer to the next node. This structure allows traversal of the linked list in both directions. + * 2. Bidirectional Traversal: Unlike doubly linked lists, singly linked lists can be easily traversed forwards but not backwards. + * 3. No Centralized Index: Unlike arrays, elements in a linked list are not stored contiguously, so there is no centralized index. Accessing elements in a linked list typically requires traversing from the head or tail node. + * 4. High Efficiency in Insertion and Deletion: Adding or removing elements in a linked list does not require moving other elements, making these operations more efficient than in arrays. + * Caution: Although our linked list classes provide methods such as at, setAt, addAt, and indexOf that are based on array indices, their time complexity, like that of the native Array.lastIndexOf, is 𝑂(𝑛). If you need to use these methods frequently, you might want to consider other data structures, such as Deque or Queue (designed for random access). Similarly, since the native Array.shift method has a time complexity of 𝑂(𝑛), using an array to simulate a queue can be inefficient. In such cases, you should use Queue or Deque, as these data structures leverage deferred array rearrangement, effectively reducing the average time complexity to 𝑂(1). * */ export class SinglyLinkedList extends LinearLinkedBase> { @@ -348,22 +325,27 @@ export class SinglyLinkedList extends LinearLinkedBase= this._length) return false; + deleteAt(index: number): E | undefined { + if (index < 0 || index >= this._length) return; + let deleted: E | undefined; if (index === 0) { + deleted = this.first; this.shift(); - return true; + return deleted; } - if (index === this._length - 1) { - this.pop(); - return true; + + const targetNode = this.getNodeAt(index); + const prevNode = this._getPrevNode(targetNode!); + + if (prevNode && targetNode) { + deleted = targetNode.value; + prevNode.next = targetNode.next; + if (targetNode === this.tail) this._tail = prevNode; + this._length--; + return deleted; } - const prevNode = this.getNodeAt(index - 1); - const removedNode = prevNode!.next; - prevNode!.next = removedNode!.next; - this._length--; - return true; + return; } /** @@ -377,37 +359,25 @@ export class SinglyLinkedList extends LinearLinkedBase | undefined): boolean { - if (elementOrNode === undefined) return false; - let value: E; - if (elementOrNode instanceof SinglyLinkedListNode) { - value = elementOrNode.value; - } else { - value = elementOrNode; - } - let current = this.head, - prev = undefined; + if (elementOrNode === undefined || !this.head) return false; - while (current) { - if (current.value === value) { - if (prev === undefined) { - this._head = current.next; - if (current === this.tail) { - this._tail = undefined; - } - } else { - prev.next = current.next; - if (current === this.tail) { - this._tail = prev; - } - } - this._length--; - return true; - } - prev = current; - current = current.next; + const node = this.isNode(elementOrNode) ? elementOrNode : this.getNode(elementOrNode); + + if (!node) return false; + + const prevNode = this._getPrevNode(node); + + if (!prevNode) { + // The node is the head + this._head = node.next; + if (node === this.tail) this._tail = undefined; + } else { + prevNode.next = node.next; + if (node === this.tail) this._tail = prevNode; } - return false; + this._length--; + return true; } /** @@ -534,6 +504,7 @@ export class SinglyLinkedList extends LinearLinkedBase | ((node: SinglyLinkedListNode) => boolean) | undefined ): SinglyLinkedListNode | undefined { if (elementNodeOrPredicate === undefined) return; + if (this.isNode(elementNodeOrPredicate)) return elementNodeOrPredicate; const predicate = this._ensurePredicate(elementNodeOrPredicate); let current = this.head; @@ -567,32 +538,22 @@ export class SinglyLinkedList extends LinearLinkedBase, newElementOrNode: E | SinglyLinkedListNode ): boolean { - if (!this.head) return false; + const existingNode = this.getNode(existingElementOrNode); + if (!existingNode) return false; - let existingValue: E; - if (this.isNode(existingElementOrNode)) { - existingValue = existingElementOrNode.value; - } else { - existingValue = existingElementOrNode; - } - if (this.head.value === existingValue) { - this.unshift(newElementOrNode); - return true; - } + const prevNode = this._getPrevNode(existingNode); + const newNode = this._ensureNode(newElementOrNode); - let current = this.head; - while (current.next) { - if (current.next.value === existingValue) { - const newNode = this._ensureNode(newElementOrNode); - newNode.next = current.next; - current.next = newNode; - this._length++; - return true; - } - current = current.next; + if (!prevNode) { + // Add at the head + this.unshift(newNode); + } else { + prevNode.next = newNode; + newNode.next = existingNode; + this._length++; } - return false; + return true; } /** @@ -628,6 +589,75 @@ export class SinglyLinkedList extends LinearLinkedBase | undefined = undefined; + + for (const item of items) { + const newNode = this._ensureNode(item); + if (!lastInsertedNode) { + if (prevNode) { + prevNode.next = newNode; + } else { + this._head = newNode; + } + } else { + lastInsertedNode.next = newNode; + } + lastInsertedNode = newNode; + } + + // Connect new node to `nextNode` + if (lastInsertedNode) { + lastInsertedNode.next = nextNode; + } else if (prevNode) { + prevNode.next = nextNode; + } + + // Update tail node and length + if (!nextNode) { + this._tail = lastInsertedNode || prevNode; + } + this._length += items.length - removedList.length; + + return removedList as this; + } + /** * Time Complexity: O(n) * Space Complexity: O(1) @@ -685,7 +715,7 @@ export class SinglyLinkedList extends LinearLinkedBase, thisArg?: any): SinglyLinkedList { - const filteredList = new SinglyLinkedList([], { toElementFn: this.toElementFn }); + const filteredList = this._createInstance({ toElementFn: this.toElementFn, maxLen: this._maxLen }); let index = 0; for (const current of this) { if (callback.call(thisArg, current, index, this)) { @@ -721,7 +751,7 @@ export class SinglyLinkedList extends LinearLinkedBase EM, thisArg?: any ): SinglyLinkedList { - const mappedList = new SinglyLinkedList([], { toElementFn }); + const mappedList = new SinglyLinkedList([], { toElementFn, maxLen: this._maxLen }); let index = 0; for (const current of this) { mappedList.push(callback.call(thisArg, current, index, this)); @@ -731,6 +761,20 @@ export class SinglyLinkedList extends LinearLinkedBase`, which is used to configure the behavior of the `SinglyLinkedList` + * instance being created. It is an optional parameter, meaning it can be omitted when calling the + * method. + * @returns An instance of the `SinglyLinkedList` class with an empty array and the provided options + * is being returned. + */ + protected override _createInstance(options?: SinglyLinkedListOptions): this { + return new SinglyLinkedList([], options) as this; + } + /** * The function `_getIterator` returns an iterable iterator that yields the values of a linked list. */ @@ -743,6 +787,38 @@ export class SinglyLinkedList extends LinearLinkedBase { + const reversedArr = [...this].reverse(); + + for (const item of reversedArr) { + yield item; + } + } + + /** + * The function `_getNodeIterator` returns an iterator that iterates over the nodes of a singly + * linked list. + */ + protected *_getNodeIterator(): IterableIterator> { + let current = this.head; + + while (current) { + yield current; + current = current.next; + } + } + + // protected *_getReverseNodeIterator(): IterableIterator> { + // const reversedArr = [...this._getNodeIterator()].reverse(); + // + // for (const item of reversedArr) { + // yield item; + // } + // } + /** * The _isPredicate function in TypeScript checks if the input is a function that takes a * SinglyLinkedListNode as an argument and returns a boolean. @@ -793,27 +869,22 @@ export class SinglyLinkedList extends LinearLinkedBase`, which is used to configure the behavior of the `SinglyLinkedList` - * instance being created. It is an optional parameter, meaning it can be omitted when calling the - * method. - * @returns An instance of the `SinglyLinkedList` class with an empty array and the provided options - * is being returned. + * The function `_getPrevNode` returns the node before a given node in a singly linked list. + * @param node - The `node` parameter in the `_getPrevNode` method is a reference to a node in a + * singly linked list. The method is used to find the node that comes before the given node in the + * linked list. + * @returns The `_getPrevNode` method returns either the previous node of the input node in a singly + * linked list or `undefined` if the input node is the head of the list or if the input node is not + * found in the list. */ - protected override _createInstance(options?: SinglyLinkedListOptions): this { - return new SinglyLinkedList([], options) as this; - } + protected _getPrevNode(node: SinglyLinkedListNode): SinglyLinkedListNode | undefined { + if (!this.head || this.head === node) return undefined; - /** - * The function returns an iterator that iterates over the elements of a collection in reverse order. - */ - protected *_getReverseIterator(): IterableIterator { - const reversedArr = [...this].reverse(); - - for (const item of reversedArr) { - yield item; + let current = this.head; + while (current.next && current.next !== node) { + current = current.next; } + + return current.next === node ? current : undefined; } } diff --git a/src/data-structures/queue/deque.ts b/src/data-structures/queue/deque.ts index 215b6db..6e74089 100644 --- a/src/data-structures/queue/deque.ts +++ b/src/data-structures/queue/deque.ts @@ -486,7 +486,11 @@ export class Deque extends LinearBase { this._length = pos + 1; return this; } else { - const newDeque = new Deque([], { bucketSize: this._bucketSize }); + const newDeque = this._createInstance({ + bucketSize: this._bucketSize, + toElementFn: this._toElementFn, + maxLen: this._maxLen + }); for (let i = 0; i <= pos; i++) { newDeque.push(this.at(i)); @@ -496,6 +500,61 @@ export class Deque extends LinearBase { } } + /** + * Time Complexity: O(n) + * Space Complexity: O(1) + * + * The `splice` function in TypeScript overrides the default behavior to remove and insert elements + * in a Deque data structure while ensuring the starting position and delete count are within bounds. + * @param {number} start - The `start` parameter in the `splice` method represents the index at which + * to start changing the array. Items will be removed or added starting from this index. + * @param {number} deleteCount - The `deleteCount` parameter in the `splice` method represents the + * number of elements to remove from the array starting at the specified `start` index. If + * `deleteCount` is not provided, it defaults to the number of elements from the `start` index to the + * end of the array (` + * @param {E[]} items - The `items` parameter in the `splice` method represents the elements that + * will be inserted into the deque at the specified `start` index. These elements will be inserted in + * place of the elements that are removed based on the `start` and `deleteCount` parameters. + * @returns The `splice` method is returning the array `deletedElements` which contains the elements + * that were removed from the Deque during the splice operation. + */ + override splice(start: number, deleteCount: number = this._length - start, ...items: E[]): this { + // Check whether the starting position is legal + rangeCheck(start, 0, this._length); + + // Adjust the value of deleteCount + if (deleteCount < 0) deleteCount = 0; + if (start + deleteCount > this._length) deleteCount = this._length - start; + + // Save deleted elements + const deletedElements = this._createInstance(); + + // Add removed elements to the result + for (let i = 0; i < deleteCount; i++) { + deletedElements.push(this.at(start + i)); + } + + // Calculate the range that needs to be deleted + const elementsAfter = []; + for (let i = start + deleteCount; i < this._length; i++) { + elementsAfter.push(this.at(i)); + } + + // Adjust the length of the current Deque + this.cut(start - 1, true); + + for (const item of items) { + this.push(item); + } + + // Insert subsequent elements back + for (const element of elementsAfter) { + this.push(element); + } + + return deletedElements; + } + /** * Time Complexity: O(1) * Space Complexity: O(1) or O(n) @@ -522,7 +581,11 @@ export class Deque extends LinearBase { this._length = this._length - pos; return this; } else { - const newDeque = new Deque([], { bucketSize: this._bucketSize }); + const newDeque = this._createInstance({ + bucketSize: this._bucketSize, + toElementFn: this._toElementFn, + maxLen: this._maxLen + }); if (pos < 0) pos = 0; for (let i = pos; i < this._length; i++) { newDeque.push(this.at(i)); @@ -543,22 +606,34 @@ export class Deque extends LinearBase { * the index of the element to be deleted. * @returns The size of the data structure after the deletion operation is performed. */ - deleteAt(pos: number): boolean { + deleteAt(pos: number): E | undefined { rangeCheck(pos, 0, this._length - 1); - if (pos === 0) this.shift(); - else if (pos === this._length - 1) this.pop(); - else { + + let deleted: E | undefined; + if (pos === 0) { + //If it is the first element, use shift() directly + return this.shift(); + } else if (pos === this._length - 1) { + // If it is the last element, just use pop() + deleted = this.last; + this.pop(); + return deleted; + } else { + // Delete the middle element const length = this._length - 1; - let { bucketIndex: curBucket, indexInBucket: curPointer } = this._getBucketAndPosition(pos); - for (let i = pos; i < length; ++i) { - const { bucketIndex: nextBucket, indexInBucket: nextPointer } = this._getBucketAndPosition(pos + 1); + const { bucketIndex: targetBucket, indexInBucket: targetPointer } = this._getBucketAndPosition(pos); + deleted = this._buckets[targetBucket][targetPointer]; + + for (let i = pos; i < length; i++) { + const { bucketIndex: curBucket, indexInBucket: curPointer } = this._getBucketAndPosition(i); + const { bucketIndex: nextBucket, indexInBucket: nextPointer } = this._getBucketAndPosition(i + 1); this._buckets[curBucket][curPointer] = this._buckets[nextBucket][nextPointer]; - curBucket = nextBucket; - curPointer = nextPointer; } + + // Remove last duplicate element this.pop(); + return deleted; } - return true; } /** @@ -588,6 +663,21 @@ export class Deque extends LinearBase { return true; } + // /** + // * Time Complexity: O(n) + // * Space Complexity: O(1) + // * + // * This function overrides the indexOf method to search for an element within a custom data + // * structure. + // * @param {E} searchElement - The `searchElement` parameter is the element that you are searching for + // * within the data structure. The `indexOf` method will return the index of the first occurrence of + // * this element within the data structure. + // * @param {number} [fromIndex=0] - The `fromIndex` parameter in the `indexOf` method specifies the + // * index at which to start searching for the `searchElement` within the data structure. If provided, + // * the search will begin at this index instead of the beginning of the data structure. + // * @returns The indexOf method is returning the index of the searchElement if it is found in the data + // * structure, or -1 if the searchElement is not found. + // */ // override indexOf(searchElement: E, fromIndex: number = 0): number { // let index = fromIndex; // let bucketIndex = this._bucketFirst; @@ -720,7 +810,11 @@ export class Deque extends LinearBase { * satisfy the given predicate function. */ filter(predicate: ElementCallback, thisArg?: any): Deque { - const newDeque = new Deque([], { bucketSize: this._bucketSize, toElementFn: this.toElementFn }); + const newDeque = this._createInstance({ + bucketSize: this._bucketSize, + toElementFn: this.toElementFn, + maxLen: this._maxLen + }); let index = 0; for (const el of this) { if (predicate.call(thisArg, el, index, this)) { @@ -750,7 +844,7 @@ export class Deque extends LinearBase { * @returns a new Deque object with elements of type EM and raw elements of type RM. */ map(callback: ElementCallback, toElementFn?: (rawElement: RM) => EM, thisArg?: any): Deque { - const newDeque = new Deque([], { bucketSize: this._bucketSize, toElementFn }); + const newDeque = new Deque([], { bucketSize: this._bucketSize, toElementFn, maxLen: this._maxLen }); let index = 0; for (const el of this) { newDeque.push(callback.call(thisArg, el, index, this)); diff --git a/src/data-structures/queue/queue.ts b/src/data-structures/queue/queue.ts index 0503789..4b1da3c 100644 --- a/src/data-structures/queue/queue.ts +++ b/src/data-structures/queue/queue.ts @@ -154,7 +154,7 @@ export class Queue extends LinearBase { */ delete(element: E): boolean { const index = this.elements.indexOf(element); - return this.deleteAt(index); + return !!this.deleteAt(index); } /** @@ -165,9 +165,10 @@ export class Queue extends LinearBase { * @param {number} index - Determine the index of the element to be deleted * @return A boolean value */ - deleteAt(index: number): boolean { - const spliced = this.elements.splice(index, 1); - return spliced.length === 1; + deleteAt(index: number): E | undefined { + const deleted = this.elements[index]; + this.elements.splice(index, 1); + return deleted; } /** @@ -275,6 +276,40 @@ export class Queue extends LinearBase { return true; } + /** + * Time Complexity: O(n) + * Space Complexity: O(n) + * + * The function overrides the splice method to remove and insert elements in a queue-like data + * structure. + * @param {number} start - The `start` parameter in the `splice` method specifies the index at which + * to start changing the array. Items will be added or removed starting from this index. + * @param {number} [deleteCount=0] - The `deleteCount` parameter in the `splice` method specifies the + * number of elements to remove from the array starting at the specified `start` index. If + * `deleteCount` is not provided, it defaults to 0, meaning no elements will be removed but new + * elements can still be inserted at + * @param {E[]} items - The `items` parameter in the `splice` method represents the elements that + * will be added to the array at the specified `start` index. These elements will replace the + * existing elements starting from the `start` index for the `deleteCount` number of elements. + * @returns The `splice` method is returning the `removedQueue`, which is an instance of the same + * class as the original object. + */ + override splice(start: number, deleteCount: number = 0, ...items: E[]): this { + const removedQueue = this._createInstance(); + + start = Math.max(0, Math.min(start, this.length)); + deleteCount = Math.max(0, Math.min(deleteCount, this.length - start)); + + const globalStartIndex = this.offset + start; + + const removedElements = this._elements.splice(globalStartIndex, deleteCount, ...items); + removedQueue.pushMany(removedElements); + + this.compact(); + + return removedQueue; + } + /** * Time Complexity: O(n) * Space Complexity: O(n) @@ -303,7 +338,11 @@ export class Queue extends LinearBase { * satisfy the given predicate function. */ filter(predicate: ElementCallback, thisArg?: any): Queue { - const newDeque = new Queue([], { toElementFn: this.toElementFn }); + const newDeque = this._createInstance({ + toElementFn: this._toElementFn, + autoCompactRatio: this._autoCompactRatio, + maxLen: this._maxLen + }); let index = 0; for (const el of this) { if (predicate.call(thisArg, el, index, this)) { @@ -333,7 +372,11 @@ export class Queue extends LinearBase { * callback function to each element in the original Queue object. */ map(callback: ElementCallback, toElementFn?: (rawElement: RM) => EM, thisArg?: any): Queue { - const newDeque = new Queue([], { toElementFn }); + const newDeque = new Queue([], { + toElementFn, + autoCompactRatio: this._autoCompactRatio, + maxLen: this._maxLen + }); let index = 0; for (const el of this) { newDeque.push(callback.call(thisArg, el, index, this)); diff --git a/test/integration/all-in-one.test.ts b/test/integration/all-in-one.test.ts index 5958e3f..06f25f0 100644 --- a/test/integration/all-in-one.test.ts +++ b/test/integration/all-in-one.test.ts @@ -23,7 +23,7 @@ describe('AVL Tree Test from data-structure-typed', () => { expect(getMinNodeBySpecificNode?.key).toBe(12); let subTreeSum = 0; - if (node15) tree.dfs(node => (subTreeSum += node.key), 'PRE', 15); + if (node15) tree.dfs(node => (subTreeSum += node.key), 'PRE', false, 15); expect(subTreeSum).toBe(70); let lesserSum = 0; diff --git a/test/integration/avl-tree.test.ts b/test/integration/avl-tree.test.ts index d5dc9e2..41b5ef6 100644 --- a/test/integration/avl-tree.test.ts +++ b/test/integration/avl-tree.test.ts @@ -40,7 +40,7 @@ describe('AVL Tree Test', () => { expect(getMinNodeBySpecificNode?.key).toBe(12); let subTreeSum = 0; - if (node15) tree.dfs(node => (subTreeSum += node.key), 'IN', 15); + if (node15) tree.dfs(node => (subTreeSum += node.key), 'IN', false, 15); expect(subTreeSum).toBe(70); let lesserSum = 0; diff --git a/test/integration/bst.test.ts b/test/integration/bst.test.ts index e6719dd..ed884f6 100644 --- a/test/integration/bst.test.ts +++ b/test/integration/bst.test.ts @@ -37,7 +37,7 @@ describe('Individual package BST operations test', () => { expect(minNodeBySpecificNode?.key).toBe(14); let subTreeSum = 0; - if (node15) bst.dfs(node => (subTreeSum += node.key), 'IN', 15); + if (node15) bst.dfs(node => (subTreeSum += node.key), 'IN', false, 15); expect(subTreeSum).toBe(45); let lesserSum = 0; @@ -236,7 +236,7 @@ describe('Individual package BST operations test', () => { expect(minNodeBySpecificNode?.key).toBe(14); let subTreeSum = 0; - if (node15) objBST.dfs(node => (subTreeSum += node.key), 'IN', node15); + if (node15) objBST.dfs(node => (subTreeSum += node.key), 'IN', false, node15); expect(subTreeSum).toBe(45); let lesserSum = 0; diff --git a/test/unit/data-structures/linked-list/doubly-linked-list.test.ts b/test/unit/data-structures/linked-list/doubly-linked-list.test.ts index aa2f03b..293447f 100644 --- a/test/unit/data-structures/linked-list/doubly-linked-list.test.ts +++ b/test/unit/data-structures/linked-list/doubly-linked-list.test.ts @@ -32,14 +32,14 @@ describe('DoublyLinkedList Operation Test', () => { }); it('should deleteAt', () => { - expect(list.deleteAt(1)).toBe(true); - expect(list.deleteAt(-1)).toBe(false); - expect(list.deleteAt(list.length)).toBe(false); + expect(list.deleteAt(1)).toBe(2); + expect(list.deleteAt(-1)).toBe(undefined); + expect(list.deleteAt(list.length)).toBe(undefined); expect(list.length).toBe(4); - expect(list.deleteAt(4)).toBe(false); + expect(list.deleteAt(4)).toBe(undefined); expect([...list]).toEqual([1, 3, 4, 5]); expect(list.isEmpty()).toBe(false); - expect(list.deleteAt(3)).toBe(true); + expect(list.deleteAt(3)).toBe(5); expect([...list]).toEqual([1, 3, 4]); }); @@ -190,7 +190,7 @@ describe('DoublyLinkedList Operation Test', () => { // Deleting from the beginning const deletedValue = list.deleteAt(0); - expect(deletedValue).toBe(true); + expect(deletedValue).toBe(1); expect(list.length).toBe(2); expect(list.head!.value).toBe(2); diff --git a/test/unit/data-structures/linked-list/singly-linked-list.test.ts b/test/unit/data-structures/linked-list/singly-linked-list.test.ts index 041650e..af1bebf 100644 --- a/test/unit/data-structures/linked-list/singly-linked-list.test.ts +++ b/test/unit/data-structures/linked-list/singly-linked-list.test.ts @@ -300,14 +300,14 @@ describe('SinglyLinkedList Operation Test', () => { list.push(2); list.push(3); const removed = list.deleteAt(1); - expect(removed).toBe(true); + expect(removed).toBe(2); expect(list.toArray()).toEqual([1, 3]); }); it('should return undefined for an out-of-bounds index', () => { list.push(1); const removed = list.deleteAt(1); - expect(removed).toBe(false); + expect(removed).toBe(undefined); }); it('should delete and return the first element', () => { @@ -318,7 +318,7 @@ describe('SinglyLinkedList Operation Test', () => { const removed = list.deleteAt(0); expect(list.first).toBe(2); expect(list.last).toBe(2); - expect(removed).toBe(true); + expect(removed).toBe(1); expect(list.toArray()).toEqual([2]); }); @@ -326,7 +326,7 @@ describe('SinglyLinkedList Operation Test', () => { list.push(1); list.push(2); const removed = list.deleteAt(1); - expect(removed).toBe(true); + expect(removed).toBe(2); expect(list.toArray()).toEqual([1]); }); });