Skip to content

Commit 8deeca2

Browse files
authored
feat: add dijkstra with adjacency list and heap increasePriority (#134)
* feat: add dijkstra with adjacency matrix * Renames, initialize distances to Infinity, remove external link * dijkstra implementation uses adjacency list and min heap * Implement heap increasePriority and let Dijkstra use it
1 parent 24c861f commit 8deeca2

File tree

5 files changed

+195
-26
lines changed

5 files changed

+195
-26
lines changed

data_structures/heap/heap.ts

+64-20
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,13 @@
1212
*/
1313

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

19-
constructor(elements: T[] = [], compare: (a: T, b: T) => boolean) {
19+
constructor(compare: (a: T, b: T) => boolean) {
2020
this.heap = [];
2121
this.compare = compare;
22-
for (let element of elements) {
23-
this.insert(element);
24-
}
2522
}
2623

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

71-
private bubbleUp(): void {
72-
let index = this.size() - 1;
68+
protected swap(a: number, b: number) {
69+
[this.heap[a], this.heap[b]] = [
70+
this.heap[b],
71+
this.heap[a],
72+
];
73+
}
74+
75+
protected bubbleUp(index = this.size() - 1): void {
7376
let parentIndex;
7477

7578
while (index > 0) {
7679
parentIndex = Math.floor((index - 1) / 2);
7780
if (this.isRightlyPlaced(index, parentIndex)) break;
78-
[this.heap[parentIndex], this.heap[index]] = [
79-
this.heap[index],
80-
this.heap[parentIndex],
81-
];
81+
this.swap(parentIndex, index);
8282
index = parentIndex;
8383
}
8484
}
@@ -95,10 +95,7 @@ export abstract class Heap<T> {
9595
rightChildIndex
9696
);
9797
if (this.isRightlyPlaced(childIndexToSwap, index)) break;
98-
[this.heap[childIndexToSwap], this.heap[index]] = [
99-
this.heap[index],
100-
this.heap[childIndexToSwap],
101-
];
98+
this.swap(childIndexToSwap, index);
10299
index = childIndexToSwap;
103100
leftChildIndex = this.getLeftChildIndex(index);
104101
rightChildIndex = this.getRightChildIndex(index);
@@ -140,13 +137,60 @@ export abstract class Heap<T> {
140137
}
141138

142139
export class MinHeap<T> extends Heap<T> {
143-
constructor(elements: T[] = [], compare = (a: T, b: T) => { return a < b }) {
144-
super(elements, compare);
140+
constructor(compare = (a: T, b: T) => { return a < b }) {
141+
super(compare);
145142
}
146143
}
147144

148145
export class MaxHeap<T> extends Heap<T> {
149-
constructor(elements: T[] = [], compare = (a: T, b: T) => { return a > b }) {
150-
super(elements, compare);
146+
constructor(compare = (a: T, b: T) => { return a > b }) {
147+
super(compare);
148+
}
149+
}
150+
151+
// 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.
152+
export class PriorityQueue<T> extends MinHeap<T> {
153+
// Maps from the n'th node to its index within the heap.
154+
private keys: number[];
155+
// Maps from element to its index with keys.
156+
private keys_index: (a: T) => number;
157+
158+
constructor(keys_index: (a: T) => number, num_keys: number, compare = (a: T, b: T) => { return a < b }) {
159+
super(compare);
160+
this.keys = Array(num_keys).fill(-1);
161+
this.keys_index = keys_index;
162+
}
163+
164+
protected swap(a: number, b: number) {
165+
let akey = this.keys_index(this.heap[a]);
166+
let bkey = this.keys_index(this.heap[b]);
167+
[this.keys[akey], this.keys[bkey]] = [this.keys[bkey], this.keys[akey]];
168+
super.swap(a, b);
169+
}
170+
171+
public insert(value: T) {
172+
this.keys[this.keys_index(value)] = this.size();
173+
super.insert(value);
174+
}
175+
176+
public extract(): T {
177+
// Unmark the the highest priority element and set key to zero for the last element in the heap.
178+
this.keys[this.keys_index(this.heap[0])] = -1;
179+
if (this.size() > 1) {
180+
this.keys[this.keys_index(this.heap[this.size() - 1])] = 0;
181+
}
182+
return super.extract();
183+
}
184+
185+
public increasePriority(idx: number, value: T) {
186+
if (this.keys[idx] == -1) {
187+
// If the key does not exist, insert the value.
188+
this.insert(value);
189+
return;
190+
}
191+
let key = this.keys[idx];
192+
// Increase the priority and bubble it up the heap.
193+
this.heap[key] = value;
194+
this.bubbleUp(key);
151195
}
152196
}

data_structures/heap/test/max_heap.test.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ describe("MaxHeap", () => {
77
];
88

99
beforeEach(() => {
10-
heap = new MaxHeap(elements);
10+
heap = new MaxHeap();
11+
for (let element of elements) {
12+
heap.insert(element);
13+
}
1114
});
1215

1316
it("should initialize a heap from input array", () => {

data_structures/heap/test/min_heap.test.ts

+29-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MinHeap } from "../heap";
1+
import { MinHeap, PriorityQueue } from "../heap";
22

33
describe("MinHeap", () => {
44
let heap: MinHeap<number>;
@@ -7,7 +7,10 @@ describe("MinHeap", () => {
77
];
88

99
beforeEach(() => {
10-
heap = new MinHeap(elements);
10+
heap = new MinHeap();
11+
for (let element of elements) {
12+
heap.insert(element);
13+
}
1114
});
1215

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

30-
const extract_all = (heap: MinHeap<number>) => {
33+
const extract_all = (heap: MinHeap<number>, elements: number[]) => {
3134
[...elements].sort((a, b) => a - b).forEach((element: number) => {
3235
expect(heap.extract()).toEqual(element);
3336
});
@@ -36,7 +39,7 @@ describe("MinHeap", () => {
3639
}
3740

3841
it("should remove and return the min elements in order", () => {
39-
extract_all(heap);
42+
extract_all(heap, elements);
4043
});
4144

4245
it("should insert all, then remove and return the min elements in order", () => {
@@ -46,6 +49,27 @@ describe("MinHeap", () => {
4649
});
4750
heap.check();
4851
expect(heap.size()).toEqual(elements.length);
49-
extract_all(heap);
52+
extract_all(heap, elements);
53+
});
54+
55+
it("should increase priority", () => {
56+
let heap = new PriorityQueue((a: number) => { return a; }, elements.length);
57+
elements.forEach((element: number) => {
58+
heap.insert(element);
59+
});
60+
heap.check();
61+
expect(heap.size()).toEqual(elements.length);
62+
63+
heap.increasePriority(55, 14);
64+
heap.increasePriority(18, 16);
65+
heap.increasePriority(81, 72);
66+
heap.increasePriority(9, 0);
67+
heap.increasePriority(43, 33);
68+
heap.check();
69+
// Elements after increasing priority
70+
const newElements: number[] = [
71+
12, 4, 33, 42, 0, 7, 39, 16, 14, 1, 51, 34, 72, 16,
72+
];
73+
extract_all(heap, newElements);
5074
});
5175
});

graph/dijkstra.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { MinHeap, PriorityQueue } from '../data_structures/heap/heap';
2+
/**
3+
* @function dijkstra
4+
* @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.
5+
* @Complexity_Analysis
6+
* Time complexity: O((V+E)*log(V)). For fully connected graphs, it is O(E*log(V)).
7+
* Space Complexity: O(V)
8+
* @param {[number, number][][]} graph - The graph in adjacency list form
9+
* @param {number} start - The source node
10+
* @return {number[]} - The shortest path to each node
11+
* @see https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
12+
*/
13+
export const dijkstra = (graph: [number, number][][], start: number): number[] => {
14+
// We use a priority queue to make sure we always visit the closest node. The
15+
// queue makes comparisons based on path weights.
16+
let priorityQueue = new PriorityQueue((a: [number, number]) => { return a[0] }, graph.length, (a: [number, number], b: [number, number]) => { return a[1] < b[1] });
17+
priorityQueue.insert([start, 0]);
18+
// We save the shortest distance to each node in `distances`. If a node is
19+
// unreachable from the start node, its distance is Infinity.
20+
let distances = Array(graph.length).fill(Infinity);
21+
distances[start] = 0;
22+
23+
while (priorityQueue.size() > 0) {
24+
const [node, _] = priorityQueue.extract();
25+
graph[node].forEach(([child, weight]) => {
26+
let new_distance = distances[node] + weight;
27+
if (new_distance < distances[child]) {
28+
// Found a new shortest path to child node. Record its distance and add child to the queue.
29+
// 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).
30+
priorityQueue.increasePriority(child, [child, weight]);
31+
distances[child] = new_distance;
32+
}
33+
});
34+
}
35+
36+
return distances;
37+
}

graph/test/dijkstra.test.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { dijkstra } from "../dijkstra";
2+
3+
describe("dijkstra", () => {
4+
5+
const init_graph = (N: number): [number, number][][] => {
6+
let graph = Array(N);
7+
for (let i = 0; i < N; ++i) {
8+
graph[i] = [];
9+
}
10+
return graph;
11+
}
12+
13+
const add_edge = (graph: [number, number][][], a: number, b: number, weight: number) => {
14+
graph[a].push([b, weight]);
15+
graph[b].push([a, weight]);
16+
}
17+
18+
it("should return the correct value", () => {
19+
let graph = init_graph(9);
20+
add_edge(graph, 0, 1, 4);
21+
add_edge(graph, 0, 7, 8);
22+
add_edge(graph, 1, 2, 8);
23+
add_edge(graph, 1, 7, 11);
24+
add_edge(graph, 2, 3, 7);
25+
add_edge(graph, 2, 5, 4);
26+
add_edge(graph, 2, 8, 2);
27+
add_edge(graph, 3, 4, 9);
28+
add_edge(graph, 3, 5, 14);
29+
add_edge(graph, 4, 5, 10);
30+
add_edge(graph, 5, 6, 2);
31+
add_edge(graph, 6, 7, 1);
32+
add_edge(graph, 6, 8, 6);
33+
add_edge(graph, 7, 8, 7);
34+
expect(dijkstra(graph, 0)).toStrictEqual([0, 4, 12, 19, 21, 11, 9, 8, 14]);
35+
});
36+
37+
it("should return the correct value for single element graph", () => {
38+
expect(dijkstra([[]], 0)).toStrictEqual([0]);
39+
});
40+
41+
let linear_graph = init_graph(4);
42+
add_edge(linear_graph, 0, 1, 1);
43+
add_edge(linear_graph, 1, 2, 2);
44+
add_edge(linear_graph, 2, 3, 3);
45+
test.each([[0, [0, 1, 3, 6]], [1, [1, 0, 2, 5]], [2, [3, 2, 0, 3]], [3, [6, 5, 3, 0]]])(
46+
"correct result for linear graph with source node %i",
47+
(source, result) => {
48+
expect(dijkstra(linear_graph, source)).toStrictEqual(result);
49+
}
50+
);
51+
52+
let unreachable_graph = init_graph(3);
53+
add_edge(unreachable_graph, 0, 1, 1);
54+
test.each([[0, [0, 1, Infinity]], [1, [1, 0, Infinity]], [2, [Infinity, Infinity, 0]]])(
55+
"correct result for graph with unreachable nodes with source node %i",
56+
(source, result) => {
57+
expect(dijkstra(unreachable_graph, source)).toStrictEqual(result);
58+
}
59+
);
60+
})
61+

0 commit comments

Comments
 (0)