Skip to content
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

feat: add dijkstra with adjacency list and heap increasePriority #134

Merged
merged 4 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
84 changes: 64 additions & 20 deletions data_structures/heap/heap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,13 @@
*/

export abstract class Heap<T> {
private heap: T[];
protected heap: T[];
// A comparison function. Returns true if a should be the parent of b.
private compare: (a: any, b: any) => boolean;
private compare: (a: T, b: T) => boolean;

constructor(elements: T[] = [], compare: (a: T, b: T) => boolean) {
constructor(compare: (a: T, b: T) => boolean) {
this.heap = [];
this.compare = compare;
for (let element of elements) {
this.insert(element);
}
}

/**
Expand Down Expand Up @@ -68,17 +65,20 @@ export abstract class Heap<T> {
return this.size() === 0;
}

private bubbleUp(): void {
let index = this.size() - 1;
protected swap(a: number, b: number) {
[this.heap[a], this.heap[b]] = [
this.heap[b],
this.heap[a],
];
}

protected bubbleUp(index = this.size() - 1): void {
let parentIndex;

while (index > 0) {
parentIndex = Math.floor((index - 1) / 2);
if (this.isRightlyPlaced(index, parentIndex)) break;
[this.heap[parentIndex], this.heap[index]] = [
this.heap[index],
this.heap[parentIndex],
];
this.swap(parentIndex, index);
index = parentIndex;
}
}
Expand All @@ -95,10 +95,7 @@ export abstract class Heap<T> {
rightChildIndex
);
if (this.isRightlyPlaced(childIndexToSwap, index)) break;
[this.heap[childIndexToSwap], this.heap[index]] = [
this.heap[index],
this.heap[childIndexToSwap],
];
this.swap(childIndexToSwap, index);
index = childIndexToSwap;
leftChildIndex = this.getLeftChildIndex(index);
rightChildIndex = this.getRightChildIndex(index);
Expand Down Expand Up @@ -140,13 +137,60 @@ export abstract class Heap<T> {
}

export class MinHeap<T> extends Heap<T> {
constructor(elements: T[] = [], compare = (a: T, b: T) => { return a < b }) {
super(elements, compare);
constructor(compare = (a: T, b: T) => { return a < b }) {
super(compare);
}
}

export class MaxHeap<T> extends Heap<T> {
constructor(elements: T[] = [], compare = (a: T, b: T) => { return a > b }) {
super(elements, compare);
constructor(compare = (a: T, b: T) => { return a > b }) {
super(compare);
}
}

// Priority queue that supports increasePriority() in O(log(n)). The limitation is that there can only be a single element for each key, and the max number or keys must be specified at heap construction. Most of the functions are wrappers around MinHeap functions and update the keys array.
export class PriorityQueue<T> extends MinHeap<T> {
// Maps from the n'th node to its index within the heap.
private keys: number[];
// Maps from element to its index with keys.
private keys_index: (a: T) => number;

constructor(keys_index: (a: T) => number, num_keys: number, compare = (a: T, b: T) => { return a < b }) {
super(compare);
this.keys = Array(num_keys).fill(-1);
this.keys_index = keys_index;
}

protected swap(a: number, b: number) {
let akey = this.keys_index(this.heap[a]);
let bkey = this.keys_index(this.heap[b]);
[this.keys[akey], this.keys[bkey]] = [this.keys[bkey], this.keys[akey]];
super.swap(a, b);
}

public insert(value: T) {
this.keys[this.keys_index(value)] = this.size();
super.insert(value);
}

public extract(): T {
// Unmark the the highest priority element and set key to zero for the last element in the heap.
this.keys[this.keys_index(this.heap[0])] = -1;
if (this.size() > 1) {
this.keys[this.keys_index(this.heap[this.size() - 1])] = 0;
}
return super.extract();
}

public increasePriority(idx: number, value: T) {
if (this.keys[idx] == -1) {
// If the key does not exist, insert the value.
this.insert(value);
return;
}
let key = this.keys[idx];
// Increase the priority and bubble it up the heap.
this.heap[key] = value;
this.bubbleUp(key);
}
}
5 changes: 4 additions & 1 deletion data_structures/heap/test/max_heap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ describe("MaxHeap", () => {
];

beforeEach(() => {
heap = new MaxHeap(elements);
heap = new MaxHeap();
for (let element of elements) {
heap.insert(element);
}
});

it("should initialize a heap from input array", () => {
Expand Down
34 changes: 29 additions & 5 deletions data_structures/heap/test/min_heap.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MinHeap } from "../heap";
import { MinHeap, PriorityQueue } from "../heap";

describe("MinHeap", () => {
let heap: MinHeap<number>;
Expand All @@ -7,7 +7,10 @@ describe("MinHeap", () => {
];

beforeEach(() => {
heap = new MinHeap(elements);
heap = new MinHeap();
for (let element of elements) {
heap.insert(element);
}
});

it("should initialize a heap from input array", () => {
Expand All @@ -27,7 +30,7 @@ describe("MinHeap", () => {
heap.check();
});

const extract_all = (heap: MinHeap<number>) => {
const extract_all = (heap: MinHeap<number>, elements: number[]) => {
[...elements].sort((a, b) => a - b).forEach((element: number) => {
expect(heap.extract()).toEqual(element);
});
Expand All @@ -36,7 +39,7 @@ describe("MinHeap", () => {
}

it("should remove and return the min elements in order", () => {
extract_all(heap);
extract_all(heap, elements);
});

it("should insert all, then remove and return the min elements in order", () => {
Expand All @@ -46,6 +49,27 @@ describe("MinHeap", () => {
});
heap.check();
expect(heap.size()).toEqual(elements.length);
extract_all(heap);
extract_all(heap, elements);
});

it("should increase priority", () => {
let heap = new PriorityQueue((a: number) => { return a; }, elements.length);
elements.forEach((element: number) => {
heap.insert(element);
});
heap.check();
expect(heap.size()).toEqual(elements.length);

heap.increasePriority(55, 14);
heap.increasePriority(18, 16);
heap.increasePriority(81, 72);
heap.increasePriority(9, 0);
heap.increasePriority(43, 33);
heap.check();
// Elements after increasing priority
const newElements: number[] = [
12, 4, 33, 42, 0, 7, 39, 16, 14, 1, 51, 34, 72, 16,
];
extract_all(heap, newElements);
});
});
37 changes: 37 additions & 0 deletions graph/dijkstra.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { MinHeap, PriorityQueue } from '../data_structures/heap/heap';
/**
* @function dijkstra
* @description Compute the shortest path from a source node to all other nodes. The input graph is in adjacency list form. It is a multidimensional array of edges. graph[i] holds the edges for the i'th node. Each edge is a 2-tuple where the 0'th item is the destination node, and the 1'th item is the edge weight.
* @Complexity_Analysis
* Time complexity: O((V+E)*log(V)). For fully connected graphs, it is O(E*log(V)).
* Space Complexity: O(V)
* @param {[number, number][][]} graph - The graph in adjacency list form
* @param {number} start - The source node
* @return {number[]} - The shortest path to each node
* @see https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
*/
export const dijkstra = (graph: [number, number][][], start: number): number[] => {
// We use a priority queue to make sure we always visit the closest node. The
// queue makes comparisons based on path weights.
let priorityQueue = new PriorityQueue((a: [number, number]) => { return a[0] }, graph.length, (a: [number, number], b: [number, number]) => { return a[1] < b[1] });
priorityQueue.insert([start, 0]);
// We save the shortest distance to each node in `distances`. If a node is
// unreachable from the start node, its distance is Infinity.
let distances = Array(graph.length).fill(Infinity);
distances[start] = 0;

while (priorityQueue.size() > 0) {
const [node, _] = priorityQueue.extract();
graph[node].forEach(([child, weight]) => {
let new_distance = distances[node] + weight;
if (new_distance < distances[child]) {
// Found a new shortest path to child node. Record its distance and add child to the queue.
// If the child already exists in the queue, the priority will be updated. This will make sure the queue will be at most size V (number of vertices).
priorityQueue.increasePriority(child, [child, weight]);
distances[child] = new_distance;
}
});
}

return distances;
}
61 changes: 61 additions & 0 deletions graph/test/dijkstra.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { dijkstra } from "../dijkstra";

describe("dijkstra", () => {

const init_graph = (N: number): [number, number][][] => {
let graph = Array(N);
for (let i = 0; i < N; ++i) {
graph[i] = [];
}
return graph;
}

const add_edge = (graph: [number, number][][], a: number, b: number, weight: number) => {
graph[a].push([b, weight]);
graph[b].push([a, weight]);
}

it("should return the correct value", () => {
let graph = init_graph(9);
add_edge(graph, 0, 1, 4);
add_edge(graph, 0, 7, 8);
add_edge(graph, 1, 2, 8);
add_edge(graph, 1, 7, 11);
add_edge(graph, 2, 3, 7);
add_edge(graph, 2, 5, 4);
add_edge(graph, 2, 8, 2);
add_edge(graph, 3, 4, 9);
add_edge(graph, 3, 5, 14);
add_edge(graph, 4, 5, 10);
add_edge(graph, 5, 6, 2);
add_edge(graph, 6, 7, 1);
add_edge(graph, 6, 8, 6);
add_edge(graph, 7, 8, 7);
expect(dijkstra(graph, 0)).toStrictEqual([0, 4, 12, 19, 21, 11, 9, 8, 14]);
});

it("should return the correct value for single element graph", () => {
expect(dijkstra([[]], 0)).toStrictEqual([0]);
});

let linear_graph = init_graph(4);
add_edge(linear_graph, 0, 1, 1);
add_edge(linear_graph, 1, 2, 2);
add_edge(linear_graph, 2, 3, 3);
test.each([[0, [0, 1, 3, 6]], [1, [1, 0, 2, 5]], [2, [3, 2, 0, 3]], [3, [6, 5, 3, 0]]])(
"correct result for linear graph with source node %i",
(source, result) => {
expect(dijkstra(linear_graph, source)).toStrictEqual(result);
}
);

let unreachable_graph = init_graph(3);
add_edge(unreachable_graph, 0, 1, 1);
test.each([[0, [0, 1, Infinity]], [1, [1, 0, Infinity]], [2, [Infinity, Infinity, 0]]])(
"correct result for graph with unreachable nodes with source node %i",
(source, result) => {
expect(dijkstra(unreachable_graph, source)).toStrictEqual(result);
}
);
})