diff --git a/data_structures/doubly_linked_list.ts b/data_structures/doubly_linked_list.ts index 67e3dda8..7ed7447f 100644 --- a/data_structures/doubly_linked_list.ts +++ b/data_structures/doubly_linked_list.ts @@ -1,3 +1,5 @@ +import { LinkedList } from "./linked_list"; + /** * This is an implementation of a Doubly Linked List. * A Doubly Linked List is a data structure that contains a head, tail and length property. @@ -10,7 +12,7 @@ * @property tail The tail of the list. * @property length The length of the list. */ -export class DoublyLinkedList { +export class DoublyLinkedList implements LinkedList { private head?: DoublyLinkedListNode = undefined; private tail?: DoublyLinkedListNode = undefined; private length: number = 0; diff --git a/data_structures/linked_list.ts b/data_structures/linked_list.ts new file mode 100644 index 00000000..596af23b --- /dev/null +++ b/data_structures/linked_list.ts @@ -0,0 +1,16 @@ +/** + * An interface for linked lists, which shares the common methods. + */ +export interface LinkedList { + isEmpty(): boolean; + get(index: number): T | null | undefined; + push(data: T): void; + pop(): T | undefined; + append(data: T): void; + removeTail(): T | undefined; + insertAt(index: number, data: T): void; + removeAt(index: number): T | undefined; + clear(): void; + toArray(): (T | undefined)[]; + getLength(): number; +} diff --git a/data_structures/linked_list_stack.ts b/data_structures/linked_list_stack.ts new file mode 100644 index 00000000..800a4150 --- /dev/null +++ b/data_structures/linked_list_stack.ts @@ -0,0 +1,82 @@ +import { SinglyLinkedList } from "./singly_linked_list"; + +/** + * This is an implementation of a stack, based on a linked list. + * A stack is a linear data structure that works with the LIFO (Last-In-First-Out) principle. + * A linked list is a linear data structure that works with the FIFO (First-In-First-Out) principle and uses references + * to determine which element is next in the list. + */ +export class LinkedListStack { + private list: SinglyLinkedList; + private limit: number; + + /** + * Creates a new stack object. + */ + constructor(limit: number = Number.MAX_VALUE) { + this.list = new SinglyLinkedList(); + this.limit = limit; + } + + /** + * Gets the top element of the stack. + * Time complexity: constant (O(1)) + * + * @returns The top element of the stack. + */ + top(): T | null { + if (this.list.isEmpty()) { + return null; + } + + return this.list.get(0)!; + } + + /** + * Inserts a new element on the top of the stack. + * Time complexity: constant (O(1)) + * + * @param data The data of the element to insert. + * @throws Stack overflow, if the new element does not fit in the limit. + */ + push(data: T): void { + if (this.list.getLength() + 1 > this.limit) { + throw new Error('Stack overflow') + } + + this.list.push(data); + } + + /** + * Removes the top element from the stack. + * Time complexity: constant (O(1)) + * + * @returns The previous top element. + * @throws Stack underflow, if the stack has no elements to pop. + */ + pop(): T { + if (this.list.isEmpty()) { + throw new Error('Stack underflow') + } + + return this.list.pop(); + } + + /** + * Gets the amount of elements in the stack. + * + * @returns The amount of elements in the stack. + */ + length(): number { + return this.list.getLength(); + } + + /** + * Gets whether the stack is empty or not. + * + * @returns Whether the stack is empty or not. + */ + isEmpty(): boolean { + return this.list.isEmpty(); + } +} \ No newline at end of file diff --git a/data_structures/singly_linked_list.ts b/data_structures/singly_linked_list.ts new file mode 100644 index 00000000..56172a8c --- /dev/null +++ b/data_structures/singly_linked_list.ts @@ -0,0 +1,275 @@ +import { LinkedList } from "./linked_list"; + +/** + * Represents a node in a linked list. + * + * @template T The type of the data stored in the node. + * @property data The data stored in the node. + * @property next A reference to the next node in the list. Can reference to null, if there is no next element. + */ +class ListNode { + constructor(public data: T, public next?: ListNode) {} +} + +/** + * This is an implementation of a (singly) linked list. + * A linked list is a data structure that stores each element with a pointer (or reference) to the next element + * in the list. Therefore, it is a linear data structure, which can be resized dynamically during runtime, as there is + * no fixed memory block allocated. + * + * @template T The type of the value of the nodes. + * @property head The head of the list. + * @property tail The tail of the list. + * @property length The length of the list. + */ +export class SinglyLinkedList implements LinkedList { + private head?: ListNode; + private tail?: ListNode; + private length: number; + + /** + * Creates a new, empty linked list. + */ + constructor() { + this.head = undefined; + this.tail = undefined; + this.length = 0; + } + + /** + * Checks, if the list is empty. + * + * @returns Whether the list is empty or not. + */ + isEmpty(): boolean { + return !this.head; + } + + /** + * Gets the data of the node at the given index. + * Time complexity: linear (O(n)) + * + * @param index The index of the node. + * @returns The data of the node at the given index or null, if no data is present. + */ + get(index: number): T | null { + if (index < 0 || index >= this.length) { + return null; + } + + if (this.isEmpty()) { + return null; + } + + let currentNode: ListNode = this.head!; + for (let i: number = 0; i < index; i++) { + if (!currentNode.next) { + return null; + } + + currentNode = currentNode.next; + } + + return currentNode.data; + } + + /** + * Inserts the given data as the first node of the list. + * Time complexity: constant (O(1)) + * + * @param data The data to be inserted. + */ + push(data: T): void { + const node: ListNode = new ListNode(data); + + if (this.isEmpty()) { + this.head = node; + this.tail = node; + } else { + node.next = this.head; + this.head = node; + } + + this.length++; + } + + /** + * Removes the first node of the list. + * Time complexity: constant (O(1)) + * + * @returns The data of the node that was removed. + * @throws Index out of bounds if the list is empty. + */ + pop(): T { + if (this.isEmpty()) { + throw new Error('Index out of bounds'); + } + + const node: ListNode = this.head!; + this.head = this.head!.next; + this.length--; + + return node.data; + } + + /** + * Inserts the given data as a new node after the current TAIL. + * Time complexity: constant (O(1)) + * + * @param data The data of the node being inserted. + */ + append(data: T): void { + const node: ListNode = new ListNode(data); + + if (this.isEmpty()) { + this.head = node; + } else { + this.tail!.next = node; + } + + this.tail = node; + this.length++; + } + + /** + * Removes the current TAIL of the list. + * Time complexity: linear (O(n)) + * + * @returns The data of the former TAIL. + * @throws Index out of bounds if the list is empty. + */ + removeTail(): T { + if (!this.head) { + throw new Error('Index out of bounds'); + } + + const currentTail = this.tail; + if (this.head === this.tail) { + this.head = undefined; + this.tail = undefined; + this.length--; + + return currentTail!.data; + } + + let currentNode: ListNode = this.head; + while (currentNode.next !== currentTail) { + currentNode = currentNode.next!; + } + + this.tail = currentNode; + this.length--; + + return currentTail!.data; + } + + /** + * Inserts the data as a new node at the given index. + * Time complexity: O(n) + * + * @param index The index where the node is to be inserted. + * @param data The data to insert. + * @throws Index out of bounds, when given an invalid index. + */ + insertAt(index: number, data: T): void { + if (index < 0 || index > this.length) { + throw new Error('Index out of bounds'); + } + + if (index === 0) { + this.push(data); + + return; + } + + if (index === this.length) { + this.append(data); + + return; + } + + const newNode = new ListNode(data); + let currentNode: ListNode | undefined = this.head; + for (let i: number = 0; i < index - 1; i++) { + currentNode = currentNode?.next; + } + + const nextNode = currentNode?.next; + currentNode!.next = newNode; + newNode.next = nextNode; + + this.length++; + } + + /** + * Removes the node at the given index. + * Time complexity: O(n) + * + * @param index The index of the node to be removed. + * @returns The data of the removed node. + * @throws Index out of bounds, when given an invalid index. + */ + removeAt(index: number): T { + if (index < 0 || index >= this.length) { + throw new Error('Index out of bounds'); + } + + if (index === 0) { + return this.pop(); + } + + if (index === this.length - 1) { + return this.removeTail(); + } + + let previousNode: ListNode | undefined; + let currentNode: ListNode | undefined = this.head; + for (let i: number = 0; i < index; i++) { + if (i === index - 1) { + previousNode = currentNode; + } + + currentNode = currentNode?.next; + } + + previousNode!.next = currentNode?.next; + this.length--; + + return currentNode!.data; + } + + /** + * Clears the list. + */ + clear(): void { + this.head = undefined; + this.tail = undefined; + this.length = 0; + } + + /** + * Converts the list to an array. + * + * @returns The array representation of the list. + */ + toArray(): T[] { + const array: T[] = []; + let currentNode: ListNode | undefined = this.head; + + while (currentNode) { + array.push(currentNode.data); + currentNode = currentNode.next; + } + + return array; + } + + /** + * Gets the length of the list. + * + * @returns The length of the list. + */ + getLength(): number { + return this.length; + } +} diff --git a/data_structures/test/doubly_linked_list.test.ts b/data_structures/test/doubly_linked_list.test.ts index 085b06d4..492af254 100644 --- a/data_structures/test/doubly_linked_list.test.ts +++ b/data_structures/test/doubly_linked_list.test.ts @@ -1,118 +1,24 @@ -import { DoublyLinkedList } from "../doubly_linked_list"; +import { DoublyLinkedList } from '../doubly_linked_list'; +import { testLinkedList } from './linked_list'; describe("DoublyLinkedList", () => { - describe("with filled list (push)", () => { - let list: DoublyLinkedList; + testLinkedList(DoublyLinkedList); - beforeEach(() => { - list = new DoublyLinkedList(); - list.push(1); - list.push(2); - list.push(3); - }); + it("should reverse the list", () => { + const list: DoublyLinkedList = new DoublyLinkedList(); - it("should return false for isEmpty when list is not empty", () => { - expect(list.isEmpty()).toBeFalsy(); - }); + list.append(1); + list.append(2); + list.append(3); + list.reverse(); - it("should return correct node for get", () => { - expect(list.get(1)).toBe(2); - }); + expect(list.get(0)).toBe(3); + expect(list.get(1)).toBe(2); + }); - it("should push nodes to the list and return correct head and tail", () => { - expect(list.get(0)).toBe(3); - expect(list.get(2)).toBe(1); - }); + it('should return null for reverse when list is empty', () => { + const list: DoublyLinkedList = new DoublyLinkedList(); - it("should pop nodes from the list and return correct head and tail", () => { - expect(list.pop()).toBe(3); - expect(list.get(0)).toBe(2); - expect(list.get(1)).toBe(1); - }); - }); - - describe("with filled list (append)", () => { - let list: DoublyLinkedList; - - beforeEach(() => { - list = new DoublyLinkedList(); - list.append(1); - list.append(2); - list.append(3); - }); - - it("should append nodes to the list and return correct head and tail", () => { - expect(list.get(0)).toBe(1); - expect(list.get(2)).toBe(3); - }); - - it("should remove tail from the list and return correct head and tail", () => { - expect(list.removeTail()).toBe(3); - expect(list.get(0)).toBe(1); - expect(list.get(1)).toBe(2); - }); - - it("should insert nodes at the correct index", () => { - list.insertAt(1, 4); - - expect(list.get(1)).toBe(4); - }); - - it("should remove nodes at the correct index", () => { - expect(list.removeAt(1)).toBe(2); - }); - - it("should return null for removeAt when index is out of bounds", () => { - expect(() => list.removeAt(3)).toThrowError("Index out of bounds"); - }); - - it("should reverse the list", () => { - list.reverse(); - - expect(list.get(0)).toBe(3); - expect(list.get(1)).toBe(2); - }); - - it("should clear the list", () => { - list.clear(); - - expect(list.isEmpty()).toBeTruthy(); - }); - - it("should convert the list to an array", () => { - expect(list.toArray()).toEqual([1, 2, 3]); - }); - - it("should return correct length", () => { - expect(list.getLength()).toBe(3); - }); - }); - - describe("with empty list", () => { - let list: DoublyLinkedList; - - beforeEach(() => { - list = new DoublyLinkedList(); - }); - - it("should return true for isEmpty when list is empty", () => { - expect(list.isEmpty()).toBeTruthy(); - }); - - it("should return null for get when index is out of bounds", () => { - expect(list.get(1)).toBeNull(); - }); - - it("should throw error for pop when list is empty", () => { - expect(() => list.pop()).toThrowError("Index out of bounds"); - }); - - it("should return null for removeTail when list is empty", () => { - expect(() => list.removeTail()).toThrowError("Index out of bounds"); - }); - - it("should return null for reverse when list is empty", () => { - expect(list.reverse()).toBeNull(); - }); - }); + expect(list.reverse()).toBeNull(); + }); }); diff --git a/data_structures/test/linked_list.ts b/data_structures/test/linked_list.ts new file mode 100644 index 00000000..5717ab15 --- /dev/null +++ b/data_structures/test/linked_list.ts @@ -0,0 +1,109 @@ +import { LinkedList } from "../linked_list"; + +type LinkedListConstructor = new () => LinkedList; + +export function testLinkedList(LinkedList: LinkedListConstructor) { + describe('with filled list (push)', () => { + let list: LinkedList = new LinkedList; + + beforeEach(() => { + list = new LinkedList(); + list.push(1); + list.push(2); + list.push(3); + }); + + it('should return false for isEmpty when list is not empty', () => { + expect(list.isEmpty()).toBeFalsy(); + }); + + it('should return correct node for get', () => { + expect(list.get(1)).toBe(2); + }); + + it('should push nodes to the list and return correct head and tail', () => { + expect(list.get(0)).toBe(3); + expect(list.get(2)).toBe(1); + }); + + it('should pop nodes from the list and return correct head and tail', () => { + expect(list.pop()).toBe(3); + expect(list.get(0)).toBe(2); + expect(list.get(1)).toBe(1); + }); + }); + + describe('with filled list (append)', () => { + let list: LinkedList = new LinkedList(); + + beforeEach(() => { + list = new LinkedList(); + list.append(1); + list.append(2); + list.append(3); + }); + + it('should append nodes to the list and return correct head and tail', () => { + expect(list.get(0)).toBe(1); + expect(list.get(2)).toBe(3); + }); + + it('should remove tail from the list and return correct head and tail', () => { + expect(list.removeTail()).toBe(3); + expect(list.get(0)).toBe(1); + expect(list.get(1)).toBe(2); + }); + + it('should insert nodes at the correct index', () => { + list.insertAt(1, 4); + + expect(list.get(1)).toBe(4); + }); + + it('should remove nodes at the correct index', () => { + expect(list.removeAt(1)).toBe(2); + }); + + it('should return null for removeAt when index is out of bounds', () => { + expect(() => list.removeAt(3)).toThrowError('Index out of bounds'); + }); + + it('should clear the list', () => { + list.clear(); + + expect(list.isEmpty()).toBeTruthy(); + }); + + it('should convert the list to an array', () => { + expect(list.toArray()).toEqual([1, 2, 3]); + }); + + it('should return correct length', () => { + expect(list.getLength()).toBe(3); + }); + }); + + describe('with empty list', () => { + let list: LinkedList; + + beforeEach(() => { + list = new LinkedList(); + }); + + it('should return true for isEmpty when list is empty', () => { + expect(list.isEmpty()).toBeTruthy(); + }); + + it('should return null for get when index is out of bounds', () => { + expect(list.get(1)).toBeNull(); + }); + + it('should throw error for pop when list is empty', () => { + expect(() => list.pop()).toThrowError('Index out of bounds'); + }); + + it('should return null for removeTail when list is empty', () => { + expect(() => list.removeTail()).toThrowError('Index out of bounds'); + }); + }); +} \ No newline at end of file diff --git a/data_structures/test/linked_list_stack.test.ts b/data_structures/test/linked_list_stack.test.ts new file mode 100644 index 00000000..2efc03d3 --- /dev/null +++ b/data_structures/test/linked_list_stack.test.ts @@ -0,0 +1,32 @@ +import { LinkedListStack } from "../linked_list_stack"; + +describe("Linked List Stack", () => { + let stack: LinkedListStack = new LinkedListStack(4); + + stack.push(1); + stack.push(2); + stack.push(3); + + it("should get the top element from the stack", () => { + expect(stack.top()).toBe(3); + }); + + it("should remove the top element from the stack and give the new top element", () => { + expect(stack.pop()).toBe(3); + expect(stack.top()).toBe(2); + }); + + it("should add a new element on top", () => { + expect(stack.push(4)); + }); + + it("should fail to add the second element on top, because of a stack overflow", () => { + stack.push(4); + expect(() => stack.push(5)).toThrowError('Stack overflow'); + }); + + it('should fail to pop the top element on an empty stack', () => { + const s: LinkedListStack = new LinkedListStack(); + expect(() => s.pop()).toThrowError('Stack underflow'); + }); +}); \ No newline at end of file diff --git a/data_structures/test/singly_linked_list.test.ts b/data_structures/test/singly_linked_list.test.ts new file mode 100644 index 00000000..0754c5e6 --- /dev/null +++ b/data_structures/test/singly_linked_list.test.ts @@ -0,0 +1,4 @@ +import { SinglyLinkedList } from "../singly_linked_list"; +import { testLinkedList } from "./linked_list"; + +describe("Singly linked list", () => testLinkedList(SinglyLinkedList)); \ No newline at end of file