Skip to content

[pull] master from amejiarosario:master #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 16, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(heap): add min/max/median-heaps
  • Loading branch information
amejiarosario committed May 16, 2020
commit 202ca9f989ddba433b4f591e27bc094640cbbadf
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
*/
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
*/
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