diff --git a/src/data-structures/doubly-linked-list/DoublyLinkedList.js b/src/data-structures/doubly-linked-list/DoublyLinkedList.js new file mode 100644 index 0000000000..5eaacc24ee --- /dev/null +++ b/src/data-structures/doubly-linked-list/DoublyLinkedList.js @@ -0,0 +1,211 @@ +import DoublyLinkedListNode from './DoublyLinkedListNode'; +import Comparator from './../../utils/comparator/Comparator'; + +export default class DoublyLinkedList { + /** + * @param {Function} [comparatorFunction] + */ + constructor(comparatorFunction) { + /** @var DoublyLinkedListNode */ + this.head = null; + + /** @var DoublyLinkedListNode */ + this.tail = null; + + this.compare = new Comparator(comparatorFunction); + } + + /** + * @param {*} value + * @return {DoublyLinkedList} + */ + prepend(value) { + // Make new node to be a head. + const newNode = new DoublyLinkedListNode(value, this.head); + + // If there is head, then it won't be head anymore + // Therefore, make its previous reference to be new node (new head) + // Then mark the new node as head + if (this.head) { + this.head.previous = newNode; + } + this.head = newNode; + + // If there is no tail yet let's make new node a tail. + if (!this.tail) { + this.tail = newNode; + } + + return this; + } + + /** + * @param {*} value + * @return {DoublyLinkedList} + */ + append(value) { + const newNode = new DoublyLinkedListNode(value); + + // If there is no head yet let's make new node a head. + if (!this.head) { + this.head = newNode; + this.tail = newNode; + + return this; + } + + // Attach new node to the end of linked list. + this.tail.next = newNode; + + // Attach current tail to the new node's previous reference + newNode.previous = this.tail; + + // Set new node to be the tail of linked list. + this.tail = newNode; + + return this; + } + + /** + * @param {*} value + * @return {DoublyLinkedListNode} + */ + delete(value) { + if (!this.head) { + return null; + } + + let deletedNode = null; + let currentNode = this.head; + + do { + if (this.compare.equal(currentNode.value, value)) { + deletedNode = currentNode; + + if (deletedNode === this.head) { + // set head to second node, which will become new head + this.head = deletedNode.next; + + // set new head's previous to null + if (this.head) { + this.head.previous = null; + } + + // If all the nodes in list has same value that is passed as argument + // then all nodes will get deleted, therefore tail needs to be updated + if (deletedNode === this.tail) { + this.tail = null; + } + } else if (deletedNode === this.tail) { + // set tail to second last node, which will become new tail + this.tail = deletedNode.previous; + this.tail.next = null; + } else { + const previousNode = deletedNode.previous; + const nextNode = deletedNode.next; + previousNode.next = nextNode; + nextNode.previous = previousNode; + } + } + + currentNode = currentNode.next; + } while (currentNode); + + return deletedNode; + } + + /** + * @param {Object} findParams + * @param {*} findParams.value + * @param {function} [findParams.callback] + * @return {DoublyLinkedListNode} + */ + find({ value = undefined, callback = undefined }) { + if (!this.head) { + return null; + } + + let currentNode = this.head; + + while (currentNode) { + // If callback is specified then try to find node by callback. + if (callback && callback(currentNode.value)) { + return currentNode; + } + + // If value is specified then try to compare by value.. + if (value !== undefined && this.compare.equal(currentNode.value, value)) { + return currentNode; + } + + currentNode = currentNode.next; + } + + return null; + } + + /** + * @return {DoublyLinkedListNode} + */ + deleteTail() { + if (!this.tail) { + return null; + } else if (this.head === this.tail) { + const deletedTail = this.tail; + this.head = null; + this.tail = null; + + return deletedTail; + } + + const deletedTail = this.tail; + this.tail = this.tail.previous; + this.tail.next = null; + + return deletedTail; + } + + /** + * @return {DoublyLinkedListNode} + */ + deleteHead() { + if (!this.head) { + return null; + } + + const deletedHead = this.head; + + if (this.head.next) { + this.head = this.head.next; + this.head.previous = null; + } else { + this.head = null; + this.tail = null; + } + + return deletedHead; + } + + /** + * @return {DoublyLinkedListNode[]} + */ + toArray() { + const nodes = []; + + let currentNode = this.head; + while (currentNode) { + nodes.push(currentNode); + currentNode = currentNode.next; + } + + return nodes; + } + + /** + * @param {function} [callback] + * @return {string} + */ + toString(callback) { + return this.toArray().map(node => node.toString(callback)).toString(); + } +} diff --git a/src/data-structures/doubly-linked-list/DoublyLinkedListNode.js b/src/data-structures/doubly-linked-list/DoublyLinkedListNode.js new file mode 100644 index 0000000000..1c97bd2c70 --- /dev/null +++ b/src/data-structures/doubly-linked-list/DoublyLinkedListNode.js @@ -0,0 +1,11 @@ +export default class DoublyLinkedListNode { + constructor(value, next = null, previous = null) { + this.value = value; + this.next = next; + this.previous = previous; + } + + toString(callback) { + return callback ? callback(this.value) : `${this.value}`; + } +} diff --git a/src/data-structures/doubly-linked-list/README.md b/src/data-structures/doubly-linked-list/README.md new file mode 100644 index 0000000000..f1e767a004 --- /dev/null +++ b/src/data-structures/doubly-linked-list/README.md @@ -0,0 +1,10 @@ +# Doubly Linked List + +In computer science, a doubly linked list is a linked data structure that consists of a set of sequentially linked records called nodes. Each node contains two fields, called links, that are references to the previous and to the next node in the sequence of nodes. The beginning and ending nodes' previous and next links, respectively, point to some kind of terminator, typically a sentinel node or null, to facilitate traversal of the list. If there is only one sentinel node, then the list is circularly linked via the sentinel node. It can be conceptualized as two singly linked lists formed from the same data items, but in opposite sequential orders. + +![Doubly Linked List](https://upload.wikimedia.org/wikipedia/commons/5/5e/Doubly-linked-list.svg) + +## References + +- [Wikipedia](https://en.wikipedia.org/wiki/Doubly_linked_list) +- [YouTube](https://www.youtube.com/watch?v=JdQeNxWCguQ) \ No newline at end of file diff --git a/src/data-structures/doubly-linked-list/__test__/DoublyLinkedList.test.js b/src/data-structures/doubly-linked-list/__test__/DoublyLinkedList.test.js new file mode 100644 index 0000000000..4490574e59 --- /dev/null +++ b/src/data-structures/doubly-linked-list/__test__/DoublyLinkedList.test.js @@ -0,0 +1,224 @@ +import DoublyLinkedList from '../DoublyLinkedList'; + +describe('DoublyLinkedList', () => { + it('should create empty linked list', () => { + const linkedList = new DoublyLinkedList(); + expect(linkedList.toString()).toBe(''); + }); + + it('should append node to linked list', () => { + const linkedList = new DoublyLinkedList(); + + expect(linkedList.head).toBeNull(); + expect(linkedList.tail).toBeNull(); + + linkedList.append(1); + linkedList.append(2); + + expect(linkedList.head.next.value).toBe(2); + expect(linkedList.tail.previous.value).toBe(1); + expect(linkedList.toString()).toBe('1,2'); + }); + + it('should prepend node to linked list', () => { + const linkedList = new DoublyLinkedList(); + + linkedList.prepend(2); + expect(linkedList.head.toString()).toBe('2'); + expect(linkedList.tail.toString()).toBe('2'); + + linkedList.append(1); + linkedList.prepend(3); + + expect(linkedList.head.next.next.previous).toBe(linkedList.head.next); + expect(linkedList.tail.previous.next).toBe(linkedList.tail); + expect(linkedList.tail.previous.value).toBe(2); + expect(linkedList.toString()).toBe('3,2,1'); + }); + + it('should delete node by value from linked list', () => { + const linkedList = new DoublyLinkedList(); + + expect(linkedList.delete(5)).toBeNull(); + + linkedList.append(1); + linkedList.append(1); + linkedList.append(2); + linkedList.append(3); + linkedList.append(3); + linkedList.append(3); + linkedList.append(4); + linkedList.append(5); + + expect(linkedList.head.toString()).toBe('1'); + expect(linkedList.tail.toString()).toBe('5'); + + const deletedNode = linkedList.delete(3); + expect(deletedNode.value).toBe(3); + expect(linkedList.tail.previous.previous.value).toBe(2); + expect(linkedList.toString()).toBe('1,1,2,4,5'); + + linkedList.delete(3); + expect(linkedList.toString()).toBe('1,1,2,4,5'); + + linkedList.delete(1); + expect(linkedList.toString()).toBe('2,4,5'); + + expect(linkedList.head.toString()).toBe('2'); + expect(linkedList.head.next.next).toBe(linkedList.tail); + expect(linkedList.tail.previous.previous).toBe(linkedList.head); + expect(linkedList.tail.toString()).toBe('5'); + + linkedList.delete(5); + expect(linkedList.toString()).toBe('2,4'); + + expect(linkedList.head.toString()).toBe('2'); + expect(linkedList.tail.toString()).toBe('4'); + + linkedList.delete(4); + expect(linkedList.toString()).toBe('2'); + + expect(linkedList.head.toString()).toBe('2'); + expect(linkedList.tail.toString()).toBe('2'); + expect(linkedList.head).toBe(linkedList.tail); + + linkedList.delete(2); + expect(linkedList.toString()).toBe(''); + }); + + it('should delete linked list tail', () => { + const linkedList = new DoublyLinkedList(); + + expect(linkedList.deleteTail()).toBeNull(); + + linkedList.append(1); + linkedList.append(2); + linkedList.append(3); + + expect(linkedList.head.toString()).toBe('1'); + expect(linkedList.tail.toString()).toBe('3'); + + const deletedNode1 = linkedList.deleteTail(); + + expect(deletedNode1.value).toBe(3); + expect(linkedList.toString()).toBe('1,2'); + expect(linkedList.head.toString()).toBe('1'); + expect(linkedList.tail.toString()).toBe('2'); + + const deletedNode2 = linkedList.deleteTail(); + + expect(deletedNode2.value).toBe(2); + expect(linkedList.toString()).toBe('1'); + expect(linkedList.head.toString()).toBe('1'); + expect(linkedList.tail.toString()).toBe('1'); + + const deletedNode3 = linkedList.deleteTail(); + + expect(deletedNode3.value).toBe(1); + expect(linkedList.toString()).toBe(''); + expect(linkedList.head).toBeNull(); + expect(linkedList.tail).toBeNull(); + }); + + it('should delete linked list head', () => { + const linkedList = new DoublyLinkedList(); + + expect(linkedList.deleteHead()).toBeNull(); + + linkedList.append(1); + linkedList.append(2); + + expect(linkedList.head.toString()).toBe('1'); + expect(linkedList.tail.toString()).toBe('2'); + + const deletedNode1 = linkedList.deleteHead(); + + expect(deletedNode1.value).toBe(1); + expect(linkedList.head.previous).toBeNull(); + expect(linkedList.toString()).toBe('2'); + expect(linkedList.head.toString()).toBe('2'); + expect(linkedList.tail.toString()).toBe('2'); + + const deletedNode2 = linkedList.deleteHead(); + + expect(deletedNode2.value).toBe(2); + expect(linkedList.toString()).toBe(''); + expect(linkedList.head).toBeNull(); + expect(linkedList.tail).toBeNull(); + }); + + it('should be possible to store objects in the list and to print them out', () => { + const linkedList = new DoublyLinkedList(); + + const nodeValue1 = { value: 1, key: 'key1' }; + const nodeValue2 = { value: 2, key: 'key2' }; + + linkedList + .append(nodeValue1) + .prepend(nodeValue2); + + const nodeStringifier = value => `${value.key}:${value.value}`; + + expect(linkedList.toString(nodeStringifier)).toBe('key2:2,key1:1'); + }); + + it('should find node by value', () => { + const linkedList = new DoublyLinkedList(); + + expect(linkedList.find({ value: 5 })).toBeNull(); + + linkedList.append(1); + expect(linkedList.find({ value: 1 })).toBeDefined(); + + linkedList + .append(2) + .append(3); + + const node = linkedList.find({ value: 2 }); + + expect(node.value).toBe(2); + expect(linkedList.find({ value: 5 })).toBeNull(); + }); + + it('should find node by callback', () => { + const linkedList = new DoublyLinkedList(); + + linkedList + .append({ value: 1, key: 'test1' }) + .append({ value: 2, key: 'test2' }) + .append({ value: 3, key: 'test3' }); + + const node = linkedList.find({ callback: value => value.key === 'test2' }); + + expect(node).toBeDefined(); + expect(node.value.value).toBe(2); + expect(node.value.key).toBe('test2'); + expect(linkedList.find({ callback: value => value.key === 'test5' })).toBeNull(); + }); + + it('should find node by means of custom compare function', () => { + const comparatorFunction = (a, b) => { + if (a.customValue === b.customValue) { + return 0; + } + + return a.customValue < b.customValue ? -1 : 1; + }; + + const linkedList = new DoublyLinkedList(comparatorFunction); + + linkedList + .append({ value: 1, customValue: 'test1' }) + .append({ value: 2, customValue: 'test2' }) + .append({ value: 3, customValue: 'test3' }); + + const node = linkedList.find({ + value: { value: 2, customValue: 'test2' }, + }); + + expect(node).toBeDefined(); + expect(node.value.value).toBe(2); + expect(node.value.customValue).toBe('test2'); + expect(linkedList.find({ value: 2, customValue: 'test5' })).toBeNull(); + }); +}); diff --git a/src/data-structures/doubly-linked-list/__test__/DoublyLinkedListNode.test.js b/src/data-structures/doubly-linked-list/__test__/DoublyLinkedListNode.test.js new file mode 100644 index 0000000000..0da8c8ce84 --- /dev/null +++ b/src/data-structures/doubly-linked-list/__test__/DoublyLinkedListNode.test.js @@ -0,0 +1,54 @@ +import DoublyLinkedListNode from './../DoublyLinkedListNode'; + +describe('DoublyLinkedListNode', () => { + it('should create list node with value', () => { + const node = new DoublyLinkedListNode(1); + + expect(node.value).toBe(1); + expect(node.next).toBeNull(); + }); + + it('should create list node with object as a value', () => { + const nodeValue = { value: 1, key: 'test' }; + const node = new DoublyLinkedListNode(nodeValue); + + expect(node.value.value).toBe(1); + expect(node.value.key).toBe('test'); + expect(node.next).toBeNull(); + expect(node.previous).toBeNull(); + }); + + it('should link nodes together', () => { + const node2 = new DoublyLinkedListNode(2); + const node1 = new DoublyLinkedListNode(1, node2); + const node3 = new DoublyLinkedListNode(10, node1, node2); + + expect(node1.next).toBeDefined(); + expect(node1.previous).toBeNull(); + expect(node2.next).toBeNull(); + expect(node2.previous).toBeNull(); + expect(node3.next).toBeDefined(); + expect(node3.previous).toBeDefined(); + expect(node1.value).toBe(1); + expect(node1.next.value).toBe(2); + expect(node3.next.value).toBe(1); + expect(node3.previous.value).toBe(2); + }); + + it('should convert node to string', () => { + const node = new DoublyLinkedListNode(1); + + expect(node.toString()).toBe('1'); + + node.value = 'string value'; + expect(node.toString()).toBe('string value'); + }); + + it('should convert node to string with custom stringifier', () => { + const nodeValue = { value: 1, key: 'test' }; + const node = new DoublyLinkedListNode(nodeValue); + const toStringCallback = value => `value: ${value.value}, key: ${value.key}`; + + expect(node.toString(toStringCallback)).toBe('value: 1, key: test'); + }); +});