diff --git a/src/data-structures/heap/Heap.js b/src/data-structures/heap/Heap.js index 45dfcfa267..7ecce9ab58 100644 --- a/src/data-structures/heap/Heap.js +++ b/src/data-structures/heap/Heap.js @@ -155,31 +155,40 @@ export default class Heap { // We need to find item index to remove each time after removal since // indices are being changed after each heapify process. const indexToRemove = this.find(item, comparator).pop(); + this.removeIndex(indexToRemove); + } + + return this; + } - // If we need to remove last child in the heap then just remove it. - // There is no need to heapify the heap afterwards. - if (indexToRemove === (this.heapContainer.length - 1)) { - this.heapContainer.pop(); + /** + * @param {number} indexToRemove + * @return {Heap} + */ + removeIndex(indexToRemove) { + // If we need to remove last child in the heap then just remove it. + // There is no need to heapify the heap afterwards. + if (indexToRemove === (this.heapContainer.length - 1)) { + this.heapContainer.pop(); + } else { + // Move last element in heap to the vacant (removed) position. + this.heapContainer[indexToRemove] = this.heapContainer.pop(); + + // Get parent. + const parentItem = this.parent(indexToRemove); + + // If there is no parent or parent is in correct order with the node + // we're going to delete then heapify down. Otherwise heapify up. + if ( + this.hasLeftChild(indexToRemove) + && ( + !parentItem + || this.pairIsInCorrectOrder(parentItem, this.heapContainer[indexToRemove]) + ) + ) { + this.heapifyDown(indexToRemove); } else { - // Move last element in heap to the vacant (removed) position. - this.heapContainer[indexToRemove] = this.heapContainer.pop(); - - // Get parent. - const parentItem = this.parent(indexToRemove); - - // If there is no parent or parent is in correct order with the node - // we're going to delete then heapify down. Otherwise heapify up. - if ( - this.hasLeftChild(indexToRemove) - && ( - !parentItem - || this.pairIsInCorrectOrder(parentItem, this.heapContainer[indexToRemove]) - ) - ) { - this.heapifyDown(indexToRemove); - } else { - this.heapifyUp(indexToRemove); - } + this.heapifyUp(indexToRemove); } } @@ -203,6 +212,15 @@ export default class Heap { return foundItemIndices; } + /** + * + * @param {number} index + * @return {*} + */ + getElementAtIndex(index) { + return this.heapContainer[index]; + } + /** * @return {boolean} */ diff --git a/src/data-structures/priority-queue/PriorityQueue.js b/src/data-structures/priority-queue/PriorityQueue.js index 2bf27bb33a..032273ba62 100644 --- a/src/data-structures/priority-queue/PriorityQueue.js +++ b/src/data-structures/priority-queue/PriorityQueue.js @@ -4,19 +4,26 @@ import Comparator from '../../utils/comparator/Comparator'; // It is the same as min heap except that when comparing to elements // we take into account not element's value but rather its priority. export default class PriorityQueue extends MinHeap { - constructor() { + /** + * @constructor + * @param {function} [compareValueFunction] + */ + constructor(compareValueFunction) { super(); - this.priorities = {}; + // Map data structure supports using any value for key type + // e.g. functions, objects, or primitives + this.priorities = new Map(); this.compare = new Comparator(this.comparePriority.bind(this)); + this.compareValue = new Comparator(compareValueFunction); } /** * @param {*} item - * @param {number} [priority] + * @param {number} [priority = 0] * @return {PriorityQueue} */ add(item, priority = 0) { - this.priorities[item] = priority; + this.priorities.set(item, priority); super.add(item); return this; @@ -24,12 +31,13 @@ export default class PriorityQueue extends MinHeap { /** * @param {*} item - * @param {Comparator} [customFindingComparator] + * @param {Comparator|function} [maybeComparator] * @return {PriorityQueue} */ - remove(item, customFindingComparator) { - super.remove(item, customFindingComparator); - delete this.priorities[item]; + remove(item, maybeComparator) { + const comparator = this.getValueComparator(maybeComparator); + super.remove(item, comparator); + this.priorities.delete(item); return this; } @@ -37,42 +45,73 @@ export default class PriorityQueue extends MinHeap { /** * @param {*} item * @param {number} priority + * @param {Comparator|function} [maybeComparator] * @return {PriorityQueue} */ - changePriority(item, priority) { - this.remove(item, new Comparator(this.compareValue)); - this.add(item, priority); + changePriority(item, priority, maybeComparator) { + const comparator = this.getValueComparator(maybeComparator); + const numberOfItemsToRemove = this.find(item, comparator).length; + const itemsToUpdate = []; + + for (let iteration = 0; iteration < numberOfItemsToRemove; iteration += 1) { + // We need to find item index to remove each time after removal since + // indices are being changed after each heapify process. + const indexToRemove = this.find(item, comparator).pop(); + const itemToUpdate = this.getElementAtIndex(indexToRemove); + itemsToUpdate.push(itemToUpdate); + this.priorities.delete(itemToUpdate); + this.removeIndex(indexToRemove); + } + + itemsToUpdate.forEach((itemToUpdate) => { + this.add(itemToUpdate, priority); + }); return this; } /** * @param {*} item - * @return {Number[]} + * @param {Comparator|function} [maybeComparator] + * @return {*[]} */ - findByValue(item) { - return this.find(item, new Comparator(this.compareValue)); + findByValue(item, maybeComparator) { + const comparator = this.getValueComparator(maybeComparator); + return this.find(item, comparator); } /** * @param {*} item + * @param {Comparator|function} [maybeComparator] * @return {boolean} */ - hasValue(item) { - return this.findByValue(item).length > 0; + hasValue(item, maybeComparator) { + const comparator = this.getValueComparator(maybeComparator); + return this.findByValue(item, comparator).length > 0; } /** - * @param {*} a - * @param {*} b - * @return {number} + * @param {Comparator|function} [maybeComparator] + * @return {Comparator} */ - comparePriority(a, b) { - if (this.priorities[a] === this.priorities[b]) { - return 0; + getValueComparator(maybeComparator) { + if (maybeComparator == null) { + return this.compareValue; + } + + if (maybeComparator instanceof Comparator) { + return maybeComparator; } - return this.priorities[a] < this.priorities[b] ? -1 : 1; + if (maybeComparator instanceof Function) { + return new Comparator(maybeComparator); + } + + throw new TypeError( + 'Invalid comparator type\n' + + 'Must be one of: Comparator | Function | undefined\n' + + `Given: ${typeof maybeComparator}`, + ); } /** @@ -80,11 +119,11 @@ export default class PriorityQueue extends MinHeap { * @param {*} b * @return {number} */ - compareValue(a, b) { - if (a === b) { + comparePriority(a, b) { + if (this.priorities.get(a) === this.priorities.get(b)) { return 0; } - return a < b ? -1 : 1; + return this.priorities.get(a) < this.priorities.get(b) ? -1 : 1; } } diff --git a/src/data-structures/priority-queue/__test__/PriorityQueue.test.js b/src/data-structures/priority-queue/__test__/PriorityQueue.test.js index 264893d393..4772f25521 100644 --- a/src/data-structures/priority-queue/__test__/PriorityQueue.test.js +++ b/src/data-structures/priority-queue/__test__/PriorityQueue.test.js @@ -1,5 +1,11 @@ import PriorityQueue from '../PriorityQueue'; +const JOB1 = { type: 'job1' }; +const JOB2 = { type: 'job2' }; +const JOB3 = { type: 'job3' }; +const JOB4 = { type: 'job4' }; +const JOB5 = { type: 'job5' }; + describe('PriorityQueue', () => { it('should create default priority queue', () => { const priorityQueue = new PriorityQueue(); @@ -10,94 +16,147 @@ describe('PriorityQueue', () => { it('should insert items to the queue and respect priorities', () => { const priorityQueue = new PriorityQueue(); - priorityQueue.add(10, 1); - expect(priorityQueue.peek()).toBe(10); + priorityQueue.add(JOB1, 1); + expect(priorityQueue.peek()).toBe(JOB1); - priorityQueue.add(5, 2); - expect(priorityQueue.peek()).toBe(10); + priorityQueue.add(JOB2, 2); + expect(priorityQueue.peek()).toBe(JOB1); - priorityQueue.add(100, 0); - expect(priorityQueue.peek()).toBe(100); + priorityQueue.add(JOB3, 0); + expect(priorityQueue.peek()).toBe(JOB3); }); it('should poll from queue with respect to priorities', () => { const priorityQueue = new PriorityQueue(); - priorityQueue.add(10, 1); - priorityQueue.add(5, 2); - priorityQueue.add(100, 0); - priorityQueue.add(200, 0); + priorityQueue.add(JOB1, 1); + priorityQueue.add(JOB2, 2); + priorityQueue.add(JOB3, 0); + priorityQueue.add(JOB4, 0); - expect(priorityQueue.poll()).toBe(100); - expect(priorityQueue.poll()).toBe(200); - expect(priorityQueue.poll()).toBe(10); - expect(priorityQueue.poll()).toBe(5); + expect(priorityQueue.poll()).toBe(JOB3); + expect(priorityQueue.poll()).toBe(JOB4); + expect(priorityQueue.poll()).toBe(JOB1); + expect(priorityQueue.poll()).toBe(JOB2); }); it('should be possible to change priority of internal nodes', () => { const priorityQueue = new PriorityQueue(); - priorityQueue.add(10, 1); - priorityQueue.add(5, 2); - priorityQueue.add(100, 0); - priorityQueue.add(200, 0); + priorityQueue.add(JOB1, 1); + priorityQueue.add(JOB2, 2); + priorityQueue.add(JOB3, 0); + priorityQueue.add(JOB4, 0); - priorityQueue.changePriority(100, 10); - priorityQueue.changePriority(10, 20); + priorityQueue.changePriority(JOB4, 10); + priorityQueue.changePriority(JOB1, 20); - expect(priorityQueue.poll()).toBe(200); - expect(priorityQueue.poll()).toBe(5); - expect(priorityQueue.poll()).toBe(100); - expect(priorityQueue.poll()).toBe(10); + expect(priorityQueue.poll()).toBe(JOB3); + expect(priorityQueue.poll()).toBe(JOB2); + expect(priorityQueue.poll()).toBe(JOB4); + expect(priorityQueue.poll()).toBe(JOB1); }); it('should be possible to change priority of head node', () => { const priorityQueue = new PriorityQueue(); - priorityQueue.add(10, 1); - priorityQueue.add(5, 2); - priorityQueue.add(100, 0); - priorityQueue.add(200, 0); + priorityQueue.add(JOB1, 1); + priorityQueue.add(JOB2, 2); + priorityQueue.add(JOB3, 0); + priorityQueue.add(JOB4, 0); - priorityQueue.changePriority(200, 10); - priorityQueue.changePriority(10, 20); + priorityQueue.changePriority(JOB3, 10); + priorityQueue.changePriority(JOB1, 20); - expect(priorityQueue.poll()).toBe(100); - expect(priorityQueue.poll()).toBe(5); - expect(priorityQueue.poll()).toBe(200); - expect(priorityQueue.poll()).toBe(10); + expect(priorityQueue.poll()).toBe(JOB4); + expect(priorityQueue.poll()).toBe(JOB2); + expect(priorityQueue.poll()).toBe(JOB3); + expect(priorityQueue.poll()).toBe(JOB1); }); it('should be possible to change priority along with node addition', () => { const priorityQueue = new PriorityQueue(); - priorityQueue.add(10, 1); - priorityQueue.add(5, 2); - priorityQueue.add(100, 0); - priorityQueue.add(200, 0); + priorityQueue.add(JOB1, 1); + priorityQueue.add(JOB2, 2); + priorityQueue.add(JOB3, 0); + priorityQueue.add(JOB4, 0); - priorityQueue.changePriority(200, 10); - priorityQueue.changePriority(10, 20); + priorityQueue.changePriority(JOB4, 10); + priorityQueue.changePriority(JOB1, 20); - priorityQueue.add(15, 15); + priorityQueue.add(JOB5, 15); - expect(priorityQueue.poll()).toBe(100); - expect(priorityQueue.poll()).toBe(5); - expect(priorityQueue.poll()).toBe(200); - expect(priorityQueue.poll()).toBe(15); - expect(priorityQueue.poll()).toBe(10); + expect(priorityQueue.poll()).toBe(JOB3); + expect(priorityQueue.poll()).toBe(JOB2); + expect(priorityQueue.poll()).toBe(JOB4); + expect(priorityQueue.poll()).toBe(JOB5); + expect(priorityQueue.poll()).toBe(JOB1); + }); + + it('should be possible to change the priority of a group of elements', () => { + const A = 'a'; + const B = 'b'; + const jobA1 = { type: A, id: 1 }; + const jobB1 = { type: B, id: 2 }; + const jobB2 = { type: B, id: 3 }; + + const priorityQueue = new PriorityQueue(); + + priorityQueue.add(jobA1, 2); + priorityQueue.add(jobB1, 8); + priorityQueue.add(jobB2, 9); + + expect(priorityQueue.peek()).toBe(jobA1); + + const compareByType = (a, b) => { + if (a.type === b.type) { + return 0; + } + + return a.type < b.type ? -1 : 1; + }; + + priorityQueue.changePriority({ type: B }, 1, compareByType); + + expect(priorityQueue.poll().type).toBe(B); + expect(priorityQueue.poll().type).toBe(B); }); it('should be possible to search in priority queue by value', () => { const priorityQueue = new PriorityQueue(); - priorityQueue.add(10, 1); - priorityQueue.add(5, 2); - priorityQueue.add(100, 0); - priorityQueue.add(200, 0); - priorityQueue.add(15, 15); + priorityQueue.add(JOB1, 1); + priorityQueue.add(JOB2, 2); + priorityQueue.add(JOB3, 0); + priorityQueue.add(JOB4, 0); + priorityQueue.add(JOB5, 15); + + const job6 = { type: 'job6' }; + + expect(priorityQueue.hasValue(job6)).toBe(false); + expect(priorityQueue.hasValue(JOB5)).toBe(true); + }); + + it('should accept a custom compareValue function', () => { + const compareByType = (a, b) => { + if (a.type === b.type) { + return 0; + } + + return a.type < b.type ? -1 : 1; + }; + + const priorityQueue = new PriorityQueue(compareByType); + + priorityQueue.add(JOB1, 1); + priorityQueue.add(JOB2, 2); + priorityQueue.add(JOB3, 0); + + const existingJobType = { type: 'job1' }; + const newJobType = { type: 'job4' }; - expect(priorityQueue.hasValue(70)).toBe(false); - expect(priorityQueue.hasValue(15)).toBe(true); + expect(priorityQueue.hasValue(existingJobType)).toBe(true); + expect(priorityQueue.hasValue(newJobType)).toBe(false); }); });