Skip to content

Commit

Permalink
feat(heap): add min/max/median-heaps
Browse files Browse the repository at this point in the history
  • Loading branch information
amejiarosario committed May 16, 2020
1 parent 7279e5c commit 202ca9f
Show file tree
Hide file tree
Showing 9 changed files with 372 additions and 153 deletions.
Original file line number Diff line number Diff line change
@@ -1,29 +1,58 @@
/**
* Heap data structure a.k.a Priority Queue
*
* Used to get min or max values from a collection in constant time.
*
* @author Adrian Mejia <adrian@adrianmejia.com>
*/
class Heap {
constructor(comparator = (a, b) => a - b) {
this.array = [];
this.comparator = (i1, i2) => comparator(this.array[i1], this.array[i2]);
}

/**
* Insert element
* @runtime O(log n)
* @param {any} value
*/
add(value) {
this.array.push(value);
this.bubbleUp();
}

/**
* Retrieves, but does not remove, the head of this heap
* @runtime O(1)
*/
peek() {
return this.array[0];
}

/**
* Retrieves and removes the head of this heap, or returns null if this heap is empty.
* @runtime O(log n)
*/
remove() {
if (!this.size()) return null;
this.swap(0, this.size() - 1);
const value = this.array.pop();
this.bubbleDown();
return value;
}

/**
* Returns the number of elements in this collection.
* @runtime O(1)
*/
size() {
return this.array.length;
}

/**
* Move new element upwards on the heap, if it's out of order
* @runtime O(log n)
*/
bubbleUp() {
let index = this.size() - 1;
const parent = (i) => Math.ceil(i / 2 - 1);
Expand All @@ -33,6 +62,10 @@ class Heap {
}
}

/**
* After removal, moves element downwards on the heap, if it's out of order
* @runtime O(log n)
*/
bubbleDown() {
let index = 0;
const left = (i) => 2 * i + 1;
Expand All @@ -47,9 +80,20 @@ class Heap {
}
}

/**
* "Private": Swap elements on the heap
* @runtime O(1)
* @param {number} i1 index 1
* @param {number} i2 index 2
*/
swap(i1, i2) {
[this.array[i1], this.array[i2]] = [this.array[i2], this.array[i1]];
}
}

// aliases
Heap.prototype.poll = Heap.prototype.remove;
Heap.prototype.offer = Heap.prototype.add;
Heap.prototype.element = Heap.prototype.peek;

module.exports = Heap;
164 changes: 164 additions & 0 deletions src/data-structures/heaps/heap.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
const Heap = require('./heap');
const PriorityQueue = require('./priority-queue');
const MaxHeap = require('./max-heap');
const MinHeap = require('./min-heap');

[[Heap], [PriorityQueue], [MinHeap]].forEach(([DS, arg]) => {
describe('Min-Heap (Priority Queue)', () => {
let heap;

beforeEach(() => {
heap = new DS(arg);
});

describe('#contructor', () => {
it('should initialize', () => {
expect(heap).not.toBe(undefined);
});
});

describe('#add', () => {
it('should add an element', () => {
expect(heap.add(1)).toBe(undefined);
expect(heap.array).toEqual([1]);
expect(heap.size()).toBe(1);
});

it('should keep things in order', () => {
heap.add(3);
expect(heap.array[0]).toEqual(3);
heap.add(2);
expect(heap.array[0]).toEqual(2);
heap.add(1);
expect(heap.array[0]).toEqual(1);
expect(heap.size()).toEqual(3);
});
});

describe('#remove', () => {
it('should work', () => {
heap.add(1);
heap.add(0);
expect(heap.remove()).toBe(0);
expect(heap.size()).toBe(1);
expect(heap.array).toEqual([1]);
});

it('should return null if empty', () => {
heap = new Heap();
expect(heap.remove()).toBe(null);
});
});

describe('when has elements', () => {
beforeEach(() => {
heap.add(1);
heap.add(2);
heap.add(3);
heap.add(0);
});

describe('#peek', () => {
it('should get min', () => {
expect(heap.peek()).toEqual(0);
});
});

describe('#remove', () => {
it('should get min', () => {
expect(heap.remove()).toEqual(0);
expect(heap.remove()).toEqual(1);
expect(heap.remove()).toEqual(2);
expect(heap.remove()).toEqual(3);
expect(heap.size()).toBe(0);
});
});
});
});
});

[[Heap, (a, b) => b - a], [PriorityQueue, (a, b) => b - a], [MaxHeap]].forEach(([DS, arg]) => {
describe('Max-Heap (Priority Queue)', () => {
let heap;

beforeEach(() => {
heap = new DS(arg);
});

describe('#contructor', () => {
it('should initialize', () => {
expect(heap).not.toBe(undefined);
});
});

describe('#add', () => {
it('should add an element', () => {
expect(heap.add(1)).toBe(undefined);
expect(heap.array).toEqual([1]);
expect(heap.size()).toBe(1);
});

it('should keep things in order', () => {
heap.add(1);
expect(heap.array[0]).toEqual(1);
heap.add(2);
expect(heap.array[0]).toEqual(2);
heap.add(3);
expect(heap.array[0]).toEqual(3);
expect(heap.size()).toEqual(3);
});
});

describe('#remove', () => {
it('should work', () => {
heap.add(1);
heap.add(0);
expect(heap.remove()).toBe(1);
expect(heap.size()).toBe(1);
expect(heap.array).toEqual([0]);
});

it('should work with duplicates', () => {
heap.add(3);
heap.add(2);
heap.add(3);
heap.add(1);
heap.add(2);
heap.add(4);
heap.add(5);
heap.add(5);
heap.add(6);

expect(heap.remove()).toEqual(6);
expect(heap.remove()).toEqual(5);
expect(heap.remove()).toEqual(5);
expect(heap.remove()).toEqual(4);
});
});

describe('when has elements', () => {
beforeEach(() => {
heap.add(1);
heap.add(2);
heap.add(3);
heap.add(0);
});

describe('#peek', () => {
it('should get min', () => {
expect(heap.peek()).toEqual(3);
});
});

describe('#remove', () => {
it('should get min when duplicates', () => {
expect(heap.remove()).toEqual(3);
expect(heap.remove()).toEqual(2);
expect(heap.remove()).toEqual(1);
expect(heap.remove()).toEqual(0);
expect(heap.size()).toBe(0);
});
});
});
});
});
8 changes: 8 additions & 0 deletions src/data-structures/heaps/max-heap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const Heap = require('./heap');

class MaxHeap extends Heap {
constructor() {
super((a, b) => b - a);
}
}
module.exports = MaxHeap;
75 changes: 75 additions & 0 deletions src/data-structures/heaps/median-heap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const Heap = require('./heap');

/**
* Median Heap using one MaxHeap and one MinHeap.
*
* Each heap contains about one half of the data.
* Every element in the min-heap is greater or equal to the median,
* and every element in the max-heap is less or equal to the median.
*
* @author Adrian Mejia <adrian@adrianmejia.com>
*/
class MedianHeap {
constructor() {
this.min = new Heap((a, b) => a - b);
this.max = new Heap((a, b) => b - a);
}

/**
* Add a value to the heap
* @runtime O(log n)
* @param {any} value
*/
add(value) {
if (value > this.findMedian()) {
// If the new element is greater than the current median, it goes to the min-heap.
this.min.add(value);
} else {
// If it is less than the current median, it goes to the max heap.
this.max.add(value);
}

// rebalance if the sizes of the heaps differ by more than one element
if (Math.abs(this.min.size() - this.max.size()) > 1) {
// extract the min/max from the heap with more elements and insert it into the other heap.
if (this.min.size() > this.max.size()) {
this.max.add(this.min.remove());
} else {
this.min.add(this.max.remove());
}
}
}

/**
* Find median
* @runtime O(1)
*/
findMedian() {
let median;

if (this.max.size() === this.min.size()) {
// When both heaps contain the same number of elements,
// the total number of elements is even.
// The median is the mean of the two middle elements.
median = (this.max.peek() + this.min.peek()) / 2;
} else if (this.max.size() > this.min.size()) {
// when the max-heap contains one more element than the min-heap,
// the median is in the top of the max-heap.
median = this.max.peek();
} else {
// When the min-heap contains one more element than the max-heap,
// the median is in the top of the min-heap.
median = this.min.peek();
}
return median;
}

/**
* Return size of the heap.
*/
size() {
return this.min.size() + this.max.size();
}
}

module.exports = MedianHeap;
Loading

0 comments on commit 202ca9f

Please sign in to comment.