terra-route 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/dist/distance/haversine.d.ts +1 -0
- package/dist/heap/min-heap.d.ts +9 -0
- package/dist/terra-route.cjs +1 -1
- package/dist/terra-route.cjs.map +1 -1
- package/dist/terra-route.d.ts +18 -26
- package/dist/terra-route.modern.js +1 -1
- package/dist/terra-route.modern.js.map +1 -1
- package/dist/terra-route.module.js +1 -1
- package/dist/terra-route.module.js.map +1 -1
- package/dist/terra-route.umd.js +1 -1
- package/dist/terra-route.umd.js.map +1 -1
- package/jest.config.js +1 -1
- package/package.json +1 -1
- package/src/data/network.geojson +822 -0
- package/src/distance/haversine.ts +2 -1
- package/src/heap/fibonacci-heap.spec.ts +98 -0
- package/src/heap/fibonacci-heap.ts +210 -0
- package/src/heap/heap.d.ts +10 -0
- package/src/heap/min-heap.spec.ts +127 -0
- package/src/heap/min-heap.ts +94 -0
- package/src/heap/pairing-heap.spec.ts +101 -0
- package/src/heap/pairing-heap.ts +109 -0
- package/src/terra-route.compare.spec.ts +89 -0
- package/src/terra-route.spec.ts +500 -446
- package/src/terra-route.ts +134 -127
- package/src/test-utils/test-utils.ts +77 -3
- package/src/fibonacci-heap.spec.ts +0 -55
- package/src/fibonacci-heap.ts +0 -131
package/src/terra-route.ts
CHANGED
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
import { FeatureCollection, LineString, Point, Feature, Position } from "geojson";
|
|
2
|
-
import { FibonacciHeap } from "./fibonacci-heap";
|
|
3
2
|
import { haversineDistance } from "./distance/haversine";
|
|
4
3
|
import { createCheapRuler } from "./distance/cheap-ruler";
|
|
4
|
+
import { MinHeap } from "./heap/min-heap";
|
|
5
|
+
import { HeapConstructor } from "./heap/heap";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* The class builds an internal graph structure based on the provided network,
|
|
11
|
-
* then applies A* algorithm to compute the shortest route.
|
|
12
|
-
*/
|
|
13
|
-
class TerraRoute {
|
|
14
|
-
private network: FeatureCollection<LineString> | undefined;
|
|
15
|
-
private distanceMeasurement: (positionA: Position, positionB: Position) => number;
|
|
16
|
-
private adjacencyList: Map<number, Array<{ node: number; distance: number }>> = new Map();
|
|
17
|
-
private coords: Position[] = []
|
|
18
|
-
private coordMap: Map<number, Map<number, number>> = new Map();
|
|
7
|
+
interface Router {
|
|
8
|
+
buildRouteGraph(network: FeatureCollection<LineString>): void;
|
|
9
|
+
getRoute(start: Feature<Point>, end: Feature<Point>): Feature<LineString> | null;
|
|
10
|
+
}
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
12
|
+
class TerraRoute implements Router {
|
|
13
|
+
private network: FeatureCollection<LineString> | null = null;
|
|
14
|
+
private distanceMeasurement: (a: Position, b: Position) => number;
|
|
15
|
+
private heapConstructor: HeapConstructor;
|
|
16
|
+
|
|
17
|
+
// Map from longitude → (map from latitude → index)
|
|
18
|
+
private coordinateIndexMap: Map<number, Map<number, number>> = new Map();
|
|
19
|
+
private coordinates: Position[] = [];
|
|
20
|
+
private adjacencyList: Array<Array<{ node: number; distance: number }>> = [];
|
|
21
|
+
|
|
22
|
+
constructor(options?: {
|
|
23
|
+
distanceMeasurement?: (a: Position, b: Position) => number;
|
|
24
|
+
heap?: HeapConstructor;
|
|
25
|
+
}) {
|
|
26
|
+
this.distanceMeasurement = options?.distanceMeasurement ?? haversineDistance;
|
|
27
|
+
this.heapConstructor = options?.heap ?? MinHeap;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
/**
|
|
@@ -35,54 +34,65 @@ class TerraRoute {
|
|
|
35
34
|
* @param coord - A GeoJSON Position array representing [longitude, latitude].
|
|
36
35
|
* @returns A unique numeric index for the coordinate.
|
|
37
36
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (!this.coordMap.has(lng)) this.coordMap.set(lng, new Map());
|
|
41
|
-
|
|
42
|
-
const latMap = this.coordMap.get(lng)!;
|
|
43
|
-
if (latMap.has(lat)) {
|
|
44
|
-
return latMap.get(lat)!;
|
|
45
|
-
}
|
|
37
|
+
public buildRouteGraph(network: FeatureCollection<LineString>): void {
|
|
38
|
+
this.network = network;
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
this.
|
|
49
|
-
|
|
40
|
+
// Reset everything
|
|
41
|
+
this.coordinateIndexMap = new Map();
|
|
42
|
+
this.coordinates = [];
|
|
43
|
+
this.adjacencyList = [];
|
|
44
|
+
|
|
45
|
+
// Hoist to locals for speed
|
|
46
|
+
const coordIndexMapLocal = this.coordinateIndexMap;
|
|
47
|
+
const coordsLocal = this.coordinates;
|
|
48
|
+
const adjListLocal = this.adjacencyList;
|
|
49
|
+
const measureDistance = this.distanceMeasurement;
|
|
50
|
+
|
|
51
|
+
for (const feature of network.features) {
|
|
52
|
+
const lineCoords = feature.geometry.coordinates;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < lineCoords.length - 1; i++) {
|
|
55
|
+
const [lngA, latA] = lineCoords[i];
|
|
56
|
+
const [lngB, latB] = lineCoords[i + 1];
|
|
57
|
+
|
|
58
|
+
// get or assign index for A
|
|
59
|
+
let latMapA = coordIndexMapLocal.get(lngA);
|
|
60
|
+
if (!latMapA) {
|
|
61
|
+
latMapA = new Map<number, number>();
|
|
62
|
+
coordIndexMapLocal.set(lngA, latMapA);
|
|
63
|
+
}
|
|
64
|
+
let indexA = latMapA.get(latA);
|
|
65
|
+
if (indexA === undefined) {
|
|
66
|
+
indexA = coordsLocal.length;
|
|
67
|
+
coordsLocal.push(lineCoords[i]);
|
|
68
|
+
latMapA.set(latA, indexA);
|
|
69
|
+
adjListLocal[indexA] = [];
|
|
70
|
+
}
|
|
50
71
|
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
// get or assign index for B
|
|
73
|
+
let latMapB = coordIndexMapLocal.get(lngB);
|
|
74
|
+
if (!latMapB) {
|
|
75
|
+
latMapB = new Map<number, number>();
|
|
76
|
+
coordIndexMapLocal.set(lngB, latMapB);
|
|
77
|
+
}
|
|
78
|
+
let indexB = latMapB.get(latB);
|
|
79
|
+
if (indexB === undefined) {
|
|
80
|
+
indexB = coordsLocal.length;
|
|
81
|
+
coordsLocal.push(lineCoords[i + 1]);
|
|
82
|
+
latMapB.set(latB, indexB);
|
|
83
|
+
adjListLocal[indexB] = [];
|
|
84
|
+
}
|
|
53
85
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
* method with a new network overwrite any existing network and reset all internal data structures.
|
|
59
|
-
*
|
|
60
|
-
* @param network - A GeoJSON FeatureCollection of LineStrings representing the road network.
|
|
61
|
-
*/
|
|
62
|
-
public buildRouteGraph(network: FeatureCollection<LineString>): void {
|
|
63
|
-
this.network = network;
|
|
64
|
-
this.adjacencyList = new Map();
|
|
65
|
-
this.coords = [];
|
|
66
|
-
this.coordMap = new Map();
|
|
67
|
-
|
|
68
|
-
for (const feature of this.network.features) {
|
|
69
|
-
const coords = feature.geometry.coordinates;
|
|
70
|
-
for (let i = 0; i < coords.length - 1; i++) {
|
|
71
|
-
const aIndex = this.coordinateIndex(coords[i]);
|
|
72
|
-
const bIndex = this.coordinateIndex(coords[i + 1]);
|
|
73
|
-
const distance = this.distanceMeasurement(coords[i], coords[i + 1]);
|
|
74
|
-
|
|
75
|
-
if (!this.adjacencyList.has(aIndex)) this.adjacencyList.set(aIndex, []);
|
|
76
|
-
if (!this.adjacencyList.has(bIndex)) this.adjacencyList.set(bIndex, []);
|
|
77
|
-
|
|
78
|
-
this.adjacencyList.get(aIndex)!.push({ node: bIndex, distance });
|
|
79
|
-
this.adjacencyList.get(bIndex)!.push({ node: aIndex, distance });
|
|
86
|
+
// record the bidirectional edge
|
|
87
|
+
const segmentDistance = measureDistance(lineCoords[i], lineCoords[i + 1]);
|
|
88
|
+
adjListLocal[indexA].push({ node: indexB, distance: segmentDistance });
|
|
89
|
+
adjListLocal[indexB].push({ node: indexA, distance: segmentDistance });
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
/**
|
|
85
|
-
* Computes the shortest route between two points in the network using
|
|
95
|
+
* Computes the shortest route between two points in the network using the A* algorithm.
|
|
86
96
|
*
|
|
87
97
|
* @param start - A GeoJSON Point Feature representing the start location.
|
|
88
98
|
* @param end - A GeoJSON Point Feature representing the end location.
|
|
@@ -90,99 +100,96 @@ class TerraRoute {
|
|
|
90
100
|
*
|
|
91
101
|
* @throws Error if the network has not been built yet with buildRouteGraph(network).
|
|
92
102
|
*/
|
|
93
|
-
public getRoute(
|
|
103
|
+
public getRoute(
|
|
104
|
+
start: Feature<Point>,
|
|
105
|
+
end: Feature<Point>
|
|
106
|
+
): Feature<LineString> | null {
|
|
94
107
|
if (!this.network) {
|
|
95
108
|
throw new Error("Network not built. Please call buildRouteGraph(network) first.");
|
|
96
109
|
}
|
|
97
110
|
|
|
98
|
-
|
|
99
|
-
const
|
|
111
|
+
// ensure start/end are in the index maps
|
|
112
|
+
const startIndex = this.getOrCreateIndex(start.geometry.coordinates);
|
|
113
|
+
const endIndex = this.getOrCreateIndex(end.geometry.coordinates);
|
|
100
114
|
|
|
101
115
|
if (startIndex === endIndex) {
|
|
102
116
|
return null;
|
|
103
117
|
}
|
|
104
118
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
openSetForward.insert(0, startIndex);
|
|
108
|
-
openSetBackward.insert(0, endIndex);
|
|
119
|
+
const openSet = new this.heapConstructor();
|
|
120
|
+
openSet.insert(0, startIndex);
|
|
109
121
|
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
122
|
+
const nodeCount = this.coordinates.length;
|
|
123
|
+
const gScore = new Array<number>(nodeCount).fill(Infinity);
|
|
124
|
+
const cameFrom = new Array<number>(nodeCount).fill(-1);
|
|
125
|
+
const visited = new Array<boolean>(nodeCount).fill(false);
|
|
114
126
|
|
|
115
|
-
|
|
116
|
-
const visitedBackward = new Set<number>();
|
|
127
|
+
gScore[startIndex] = 0;
|
|
117
128
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
visitedForward.add(currentForward);
|
|
123
|
-
|
|
124
|
-
if (visitedBackward.has(currentForward)) {
|
|
125
|
-
meetingNode = currentForward;
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
for (const neighbor of this.adjacencyList.get(currentForward) || []) {
|
|
130
|
-
const tentativeG = (gScoreForward.get(currentForward) ?? Infinity) + neighbor.distance;
|
|
131
|
-
if (tentativeG < (gScoreForward.get(neighbor.node) ?? Infinity)) {
|
|
132
|
-
cameFromForward.set(neighbor.node, currentForward);
|
|
133
|
-
gScoreForward.set(neighbor.node, tentativeG);
|
|
134
|
-
const fScore = tentativeG + this.distanceMeasurement(this.coords[neighbor.node], this.coords[endIndex]);
|
|
135
|
-
openSetForward.insert(fScore, neighbor.node);
|
|
136
|
-
}
|
|
129
|
+
while (openSet.size() > 0) {
|
|
130
|
+
const current = openSet.extractMin()!;
|
|
131
|
+
if (visited[current]) {
|
|
132
|
+
continue;
|
|
137
133
|
}
|
|
138
|
-
|
|
139
|
-
const currentBackward = openSetBackward.extractMin()!;
|
|
140
|
-
visitedBackward.add(currentBackward);
|
|
141
|
-
|
|
142
|
-
if (visitedForward.has(currentBackward)) {
|
|
143
|
-
meetingNode = currentBackward;
|
|
134
|
+
if (current === endIndex) {
|
|
144
135
|
break;
|
|
145
136
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
137
|
+
visited[current] = true;
|
|
138
|
+
|
|
139
|
+
for (const neighbor of this.adjacencyList[current] || []) {
|
|
140
|
+
const tentativeG = gScore[current] + neighbor.distance;
|
|
141
|
+
if (tentativeG < gScore[neighbor.node]) {
|
|
142
|
+
gScore[neighbor.node] = tentativeG;
|
|
143
|
+
cameFrom[neighbor.node] = current;
|
|
144
|
+
const heuristic = this.distanceMeasurement(
|
|
145
|
+
this.coordinates[neighbor.node],
|
|
146
|
+
this.coordinates[endIndex]
|
|
147
|
+
);
|
|
148
|
+
openSet.insert(tentativeG + heuristic, neighbor.node);
|
|
154
149
|
}
|
|
155
150
|
}
|
|
156
151
|
}
|
|
157
152
|
|
|
158
|
-
if (
|
|
153
|
+
if (cameFrom[endIndex] < 0) {
|
|
159
154
|
return null;
|
|
160
155
|
}
|
|
161
156
|
|
|
162
|
-
// Reconstruct
|
|
163
|
-
const
|
|
164
|
-
let
|
|
165
|
-
while (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Reconstruct backward path (omit meeting node to avoid duplication)
|
|
171
|
-
const pathBackward: Position[] = [];
|
|
172
|
-
node = cameFromBackward.get(meetingNode)!;
|
|
173
|
-
while (node !== undefined) {
|
|
174
|
-
pathBackward.push(this.coords[node]);
|
|
175
|
-
node = cameFromBackward.get(node)!;
|
|
157
|
+
// Reconstruct path
|
|
158
|
+
const path: Position[] = [];
|
|
159
|
+
let current = endIndex;
|
|
160
|
+
while (current !== startIndex) {
|
|
161
|
+
path.unshift(this.coordinates[current]);
|
|
162
|
+
current = cameFrom[current];
|
|
176
163
|
}
|
|
177
|
-
|
|
178
|
-
const fullPath = [...pathForward, ...pathBackward];
|
|
164
|
+
path.unshift(this.coordinates[startIndex]);
|
|
179
165
|
|
|
180
166
|
return {
|
|
181
167
|
type: "Feature",
|
|
182
|
-
geometry: { type: "LineString", coordinates:
|
|
168
|
+
geometry: { type: "LineString", coordinates: path },
|
|
183
169
|
properties: {},
|
|
184
170
|
};
|
|
185
171
|
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Helper to index start/end in getRoute.
|
|
175
|
+
*/
|
|
176
|
+
private getOrCreateIndex(coord: Position): number {
|
|
177
|
+
const [lng, lat] = coord;
|
|
178
|
+
let latMap = this.coordinateIndexMap.get(lng);
|
|
179
|
+
if (!latMap) {
|
|
180
|
+
latMap = new Map<number, number>();
|
|
181
|
+
this.coordinateIndexMap.set(lng, latMap);
|
|
182
|
+
}
|
|
183
|
+
let index = latMap.get(lat);
|
|
184
|
+
if (index === undefined) {
|
|
185
|
+
index = this.coordinates.length;
|
|
186
|
+
this.coordinates.push(coord);
|
|
187
|
+
latMap.set(lat, index);
|
|
188
|
+
// ensure adjacencyList covers this new node
|
|
189
|
+
this.adjacencyList[index] = [];
|
|
190
|
+
}
|
|
191
|
+
return index;
|
|
192
|
+
}
|
|
186
193
|
}
|
|
187
194
|
|
|
188
195
|
export { TerraRoute, createCheapRuler, haversineDistance }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Position, Feature, Point, LineString, FeatureCollection } from "geojson";
|
|
2
|
+
import { haversineDistance } from "../terra-route";
|
|
2
3
|
|
|
3
4
|
export const createPointFeature = (coord: Position): Feature<Point> => ({
|
|
4
5
|
type: "Feature",
|
|
@@ -23,6 +24,20 @@ export const createLineStringFeature = (coordinates: Position[]): Feature<LineSt
|
|
|
23
24
|
properties: {},
|
|
24
25
|
});
|
|
25
26
|
|
|
27
|
+
export function routeLength(
|
|
28
|
+
line: Feature<LineString>,
|
|
29
|
+
|
|
30
|
+
) {
|
|
31
|
+
const lineCoords = line.geometry.coordinates;
|
|
32
|
+
|
|
33
|
+
// Calculate the total route distance
|
|
34
|
+
let routeDistance = 0;
|
|
35
|
+
for (let i = 0; i < lineCoords.length - 1; i++) {
|
|
36
|
+
routeDistance += haversineDistance(lineCoords[i], lineCoords[i + 1]);
|
|
37
|
+
}
|
|
38
|
+
return routeDistance
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
export function generateGridWithDiagonals(n: number, spacing: number): FeatureCollection<LineString> {
|
|
27
42
|
const features: Feature<LineString>[] = [];
|
|
28
43
|
|
|
@@ -111,9 +126,9 @@ export function generateGridWithDiagonals(n: number, spacing: number): FeatureCo
|
|
|
111
126
|
*/
|
|
112
127
|
export function generateStarPolygon(
|
|
113
128
|
n: number,
|
|
114
|
-
radius
|
|
129
|
+
radius = 0.01,
|
|
115
130
|
center: Position = [0, 0],
|
|
116
|
-
connectAll
|
|
131
|
+
connectAll = true
|
|
117
132
|
): FeatureCollection<LineString> {
|
|
118
133
|
if (n < 3) {
|
|
119
134
|
throw new Error("Star polygon requires at least 3 vertices.");
|
|
@@ -212,7 +227,7 @@ export function generateTreeFeatureCollection(
|
|
|
212
227
|
depth: number,
|
|
213
228
|
branchingFactor: number,
|
|
214
229
|
root: Position = [0, 0],
|
|
215
|
-
length
|
|
230
|
+
length = 0.01
|
|
216
231
|
): FeatureCollection<LineString> {
|
|
217
232
|
if (depth < 1) {
|
|
218
233
|
throw new Error("Tree must have at least depth 1.");
|
|
@@ -400,3 +415,62 @@ export function getReasonIfLineStringInvalid(
|
|
|
400
415
|
seen.add(key);
|
|
401
416
|
}
|
|
402
417
|
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Checks if the start and end coordinates of a LineString match the given start and end points.
|
|
421
|
+
*
|
|
422
|
+
* @param line - The LineString feature to check
|
|
423
|
+
* @param start - The start point feature
|
|
424
|
+
* @param end - The end point feature
|
|
425
|
+
* @return True if the start and end coordinates match, false otherwise
|
|
426
|
+
* */
|
|
427
|
+
export function startAndEndAreCorrect(line: Feature<LineString>, start: Feature<Point>, end: Feature<Point>) {
|
|
428
|
+
const lineCoords = line.geometry.coordinates;
|
|
429
|
+
const startCoords = start.geometry.coordinates;
|
|
430
|
+
const endCoords = end.geometry.coordinates;
|
|
431
|
+
|
|
432
|
+
// Check if the first coordinate of the LineString matches the start point
|
|
433
|
+
const startMatches = lineCoords[0][0] === startCoords[0] && lineCoords[0][1] === startCoords[1];
|
|
434
|
+
|
|
435
|
+
// Check if the last coordinate of the LineString matches the end point
|
|
436
|
+
const endMatches = lineCoords[lineCoords.length - 1][0] === endCoords[0] && lineCoords[lineCoords.length - 1][1] === endCoords[1];
|
|
437
|
+
|
|
438
|
+
return startMatches && endMatches;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function routeIsLongerThanDirectPath(line: Feature<LineString>, start: Feature<Point>, end: Feature<Point>) {
|
|
442
|
+
const lineCoords = line.geometry.coordinates;
|
|
443
|
+
const startCoords = start.geometry.coordinates;
|
|
444
|
+
const endCoords = end.geometry.coordinates;
|
|
445
|
+
|
|
446
|
+
if (lineCoords.length <= 2) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Calculate the direct distance between the start and end points
|
|
451
|
+
const directDistance = haversineDistance(startCoords, endCoords);
|
|
452
|
+
|
|
453
|
+
// Calculate the route distance
|
|
454
|
+
let routeDistance = 0;
|
|
455
|
+
for (let i = 0; i < lineCoords.length - 1; i++) {
|
|
456
|
+
routeDistance += haversineDistance(lineCoords[i], lineCoords[i + 1]);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// If the route distance is 0, it means the start and end points are the same
|
|
460
|
+
if (routeDistance === 0) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (routeDistance < directDistance) {
|
|
465
|
+
|
|
466
|
+
// Check if the route distance is very close to the direct distance
|
|
467
|
+
const absoluteDifference = Math.abs(routeDistance - directDistance);
|
|
468
|
+
if (absoluteDifference < 0.000000000001) {
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return true
|
|
476
|
+
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { FibonacciHeap } from './fibonacci-heap';
|
|
2
|
-
|
|
3
|
-
describe('FibonacciHeap', () => {
|
|
4
|
-
let heap: FibonacciHeap;
|
|
5
|
-
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
heap = new FibonacciHeap();
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test('insert() and size()', () => {
|
|
11
|
-
expect(heap.size()).toBe(0);
|
|
12
|
-
heap.insert(10, 1);
|
|
13
|
-
expect(heap.size()).toBe(1);
|
|
14
|
-
heap.insert(5, 2);
|
|
15
|
-
expect(heap.size()).toBe(2);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test('extractMin() returns the correct value', () => {
|
|
19
|
-
heap.insert(20, 101);
|
|
20
|
-
heap.insert(5, 102);
|
|
21
|
-
heap.insert(15, 103);
|
|
22
|
-
|
|
23
|
-
expect(heap.extractMin()).toBe(102); // key 5
|
|
24
|
-
expect(heap.size()).toBe(2);
|
|
25
|
-
expect(heap.extractMin()).toBe(103); // key 15
|
|
26
|
-
expect(heap.extractMin()).toBe(101); // key 20
|
|
27
|
-
expect(heap.extractMin()).toBeNull();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('extractMin() on empty heap returns null', () => {
|
|
31
|
-
expect(heap.extractMin()).toBeNull();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('insert() handles duplicate keys', () => {
|
|
35
|
-
heap.insert(10, 201);
|
|
36
|
-
heap.insert(10, 202);
|
|
37
|
-
heap.insert(10, 203);
|
|
38
|
-
|
|
39
|
-
const values = [heap.extractMin(), heap.extractMin(), heap.extractMin()];
|
|
40
|
-
expect(values).toContain(201);
|
|
41
|
-
expect(values).toContain(202);
|
|
42
|
-
expect(values).toContain(203);
|
|
43
|
-
expect(heap.size()).toBe(0);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('interleaved insert and extractMin()', () => {
|
|
47
|
-
heap.insert(30, 301);
|
|
48
|
-
heap.insert(10, 302);
|
|
49
|
-
expect(heap.extractMin()).toBe(302);
|
|
50
|
-
heap.insert(20, 303);
|
|
51
|
-
expect(heap.extractMin()).toBe(303);
|
|
52
|
-
expect(heap.extractMin()).toBe(301);
|
|
53
|
-
expect(heap.extractMin()).toBeNull();
|
|
54
|
-
});
|
|
55
|
-
});
|
package/src/fibonacci-heap.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
type FibNode = {
|
|
2
|
-
key: number;
|
|
3
|
-
value: number;
|
|
4
|
-
degree: number;
|
|
5
|
-
mark: boolean;
|
|
6
|
-
parent: FibNode | null;
|
|
7
|
-
child: FibNode | null;
|
|
8
|
-
left: FibNode;
|
|
9
|
-
right: FibNode;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export class FibonacciHeap {
|
|
13
|
-
private nodeCount = 0;
|
|
14
|
-
private minNode: FibNode | null = null;
|
|
15
|
-
|
|
16
|
-
insert(key: number, value: number): void {
|
|
17
|
-
const node: FibNode = {
|
|
18
|
-
key,
|
|
19
|
-
value,
|
|
20
|
-
degree: 0,
|
|
21
|
-
mark: false,
|
|
22
|
-
parent: null,
|
|
23
|
-
child: null,
|
|
24
|
-
left: null as any,
|
|
25
|
-
right: null as any,
|
|
26
|
-
};
|
|
27
|
-
node.left = node;
|
|
28
|
-
node.right = node;
|
|
29
|
-
|
|
30
|
-
this.minNode = this.mergeLists(this.minNode, node);
|
|
31
|
-
this.nodeCount++;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
extractMin(): number | null {
|
|
35
|
-
const z = this.minNode;
|
|
36
|
-
if (!z) return null;
|
|
37
|
-
|
|
38
|
-
// Add children to root list
|
|
39
|
-
if (z.child) {
|
|
40
|
-
let child = z.child;
|
|
41
|
-
do {
|
|
42
|
-
child.parent = null;
|
|
43
|
-
child = child.right!;
|
|
44
|
-
} while (child !== z.child);
|
|
45
|
-
this.minNode = this.mergeLists(this.minNode, z.child);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
this.removeFromList(z);
|
|
49
|
-
|
|
50
|
-
if (z === z.right) {
|
|
51
|
-
this.minNode = null;
|
|
52
|
-
} else {
|
|
53
|
-
this.minNode = z.right;
|
|
54
|
-
this.consolidate();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
this.nodeCount--;
|
|
58
|
-
return z.value;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
size(): number {
|
|
62
|
-
return this.nodeCount;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ========== Internal Methods ==========
|
|
66
|
-
|
|
67
|
-
private consolidate(): void {
|
|
68
|
-
const maxDegree = Math.floor(Math.log2(this.nodeCount)) + 1;
|
|
69
|
-
const A = new Array<FibNode | null>(maxDegree).fill(null);
|
|
70
|
-
|
|
71
|
-
const rootList: FibNode[] = [];
|
|
72
|
-
let curr = this.minNode!;
|
|
73
|
-
do {
|
|
74
|
-
rootList.push(curr);
|
|
75
|
-
curr = curr.right!;
|
|
76
|
-
} while (curr !== this.minNode);
|
|
77
|
-
|
|
78
|
-
for (const w of rootList) {
|
|
79
|
-
let x = w;
|
|
80
|
-
let d = x.degree;
|
|
81
|
-
while (A[d]) {
|
|
82
|
-
let y = A[d]!;
|
|
83
|
-
if (x.key > y.key) {
|
|
84
|
-
const temp = x;
|
|
85
|
-
x = y;
|
|
86
|
-
y = temp;
|
|
87
|
-
}
|
|
88
|
-
this.link(y, x);
|
|
89
|
-
A[d] = null;
|
|
90
|
-
d++;
|
|
91
|
-
}
|
|
92
|
-
A[d] = x;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
this.minNode = null;
|
|
96
|
-
for (const node of A) {
|
|
97
|
-
if (node) {
|
|
98
|
-
this.minNode = this.mergeLists(this.minNode, node);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private link(y: FibNode, x: FibNode): void {
|
|
104
|
-
this.removeFromList(y);
|
|
105
|
-
y.left = y.right = y;
|
|
106
|
-
x.child = this.mergeLists(x.child, y);
|
|
107
|
-
y.parent = x;
|
|
108
|
-
x.degree++;
|
|
109
|
-
y.mark = false;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private mergeLists(a: FibNode | null, b: FibNode | null): FibNode {
|
|
113
|
-
if (!a) return b!;
|
|
114
|
-
if (!b) return a;
|
|
115
|
-
|
|
116
|
-
const aRight = a.right!;
|
|
117
|
-
const bRight = b.right!;
|
|
118
|
-
|
|
119
|
-
a.right = bRight;
|
|
120
|
-
bRight.left = a;
|
|
121
|
-
b.right = aRight;
|
|
122
|
-
aRight.left = b;
|
|
123
|
-
|
|
124
|
-
return a.key < b.key ? a : b;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
private removeFromList(node: FibNode): void {
|
|
128
|
-
node.left.right = node.right;
|
|
129
|
-
node.right.left = node.left;
|
|
130
|
-
}
|
|
131
|
-
}
|