diff --git a/src/dataStructures/doublyLinkedList.js b/src/dataStructures/doublyLinkedList.js new file mode 100644 index 0000000..61c6159 --- /dev/null +++ b/src/dataStructures/doublyLinkedList.js @@ -0,0 +1,136 @@ +class Node { + constructor(value, prev = null, next = null) { + this.value = value; + this.next = next; + this.prev = prev; + } +} + +const INSERT_POSITION = ['before', 'after']; +const TRAVERSE_DIRECTION = ['forward', 'backward']; + +class DoublyLinkedList { + constructor() { + this.head = null; + this.tail = null; + } + + includes(value) { + if (this.head === null) { + return false; + } + + let current = this.head; + + while (current) { + if (current.value === value) { + return true; + } + current = current.next; + } + + return false; + } + + get(value) { + if (this.head === null) { + return null; + } + + let current = this.head; + + while (current) { + if (current.value === value) { + return current; + } + current = current.next; + } + + return null; + } + + append(value) { + const node = new Node(value, this.tail); + + if (this.head === null) { + this.head = node; + this.tail = node; + return this; + } + + this.tail.next = node; + this.tail = node; + return this; + } + + // value1 = new value to insert + // value2 = anchor value in the list to which the new value is to be added + // position (before/after) = position relative to value2 to which value1 will be added + insert(value1, value2, position = 'after') { + if(!this.includes(value2) || !INSERT_POSITION.includes(position) || value2 == null) { + return this; + } + + const newNode = new Node(value1); + const anchorNode = this.get(value2); + if(position === 'after') { + if(anchorNode === this.tail) { + this.tail = newNode; + } + + newNode.next = anchorNode.next; + newNode.prev = anchorNode; + anchorNode.next = newNode; + } else { + if(anchorNode === this.head) { + this.head = newNode; + } + + newNode.prev = anchorNode.prev; + (anchorNode.prev || {}).next = newNode; + anchorNode.prev = newNode; + newNode.next = anchorNode; + } + + return this; + } + + remove(value) { + if(!this.includes(value)) { + return false; + } + + const node = this.get(value); + + if(this.head === node) { + this.head = node.next; + } + + if(this.tail === node) { + this.tail = node.prev; + } + + (node.prev || {}).next = node.next; + (node.next || {}).prev = node.prev; + return true; + } + + toString(direction = 'forward') { + if (this.head === null || !TRAVERSE_DIRECTION.includes(direction)) { + return ''; + } + + const key = direction === 'forward' ? 'next' : 'prev'; + + const arr = []; + let current = direction === 'forward' ? this.head : this.tail; + while (current) { + arr.push(current.value); + current = current[key]; + } + + return arr.join(', '); + } +} + +module.exports = DoublyLinkedList; diff --git a/test/dataStructures/doublyLinkedList.test.js b/test/dataStructures/doublyLinkedList.test.js new file mode 100644 index 0000000..f8e8a90 --- /dev/null +++ b/test/dataStructures/doublyLinkedList.test.js @@ -0,0 +1,86 @@ +const DoublyLinkedList = require('../../src/dataStructures/doublyLinkedList'); + +describe('Doubly linked list data structure', () => { + it ('Should have empty head and tail when instantiated', () => { + const list = new DoublyLinkedList(); + expect(list.head).toBe(null); + expect(list.tail).toBe(null); + }); + + it('Should add the first node as head and tail of the list', () => { + const list = new DoublyLinkedList(); + list.append(1); + expect(list.head).toBe(list.tail); + }); + + it('Should check whether an element is in the list', () => { + const list = new DoublyLinkedList(); + list.append(1).append(2).append(3); + expect(list.includes(2)).toBe(true); + expect(list.includes(4)).toBe(false); + }); + + it('Should be able to get an element from the list', () => { + const list = new DoublyLinkedList(); + expect(list.get(1)).toBe(null); + + list.append(1).append(2).append(3); + expect(list.get(2).value).toBe(2); + expect(list.get(4)).toBe(null); + }); + + it('Should be able to remove an item from the list', () => { + const list = new DoublyLinkedList(); + expect(list.remove(1)).toBe(false); + + list.append(1).append(2).append(3).append(4).append(5); + expect(list.remove(2)).toBe(true); + expect(list.remove(6)).toBe(false); + + list.remove(1); + expect(list.head.value).toBe(3); + + list.remove(5); + expect(list.tail.value).toBe(4); + }); + + it('Should update references of neighboring elements of removed element', () => { + const list = new DoublyLinkedList(); + list.append(1).append(2).append(3).append(4); + expect(list.get(2).next.value).toBe(3); + + list.remove(3); + expect(list.get(2).next.value).toBe(4); + }); + + it('Should be able to insert an element into the list', () => { + const list = new DoublyLinkedList(); + list.append(1).append(2).append(3); + expect(list.get(2).next.value).toBe(3); + + list.insert(2.5, 2, 'after'); + expect(list.get(2).next.value).toBe(2.5); + + list.insert(2.25, 2.5, 'before'); + expect(list.get(2.5).prev.value).toBe(2.25); + + list.insert(4, 3, 'position'); + expect(list.includes(4)).toBe(false); + + list.insert(4, 3, 'after'); + expect(list.tail.value).toBe(4); + + list.insert(0, 1, 'before'); + expect(list.head.value).toBe(0); + }); + + it('Should print a correct representation of the list as string', () => { + const list = new DoublyLinkedList(); + expect(list.toString()).toBe(''); + + list.append(1).append(2).append(3).append(4).append(5); + expect(list.toString()).toBe('1, 2, 3, 4, 5'); + expect(list.toString('backward')).toBe('5, 4, 3, 2, 1'); + expect(list.toString('direction')).toBe(''); + }); +});