-
-
Notifications
You must be signed in to change notification settings - Fork 5.7k
/
Copy pathClosestPairOfPoints.js
160 lines (141 loc) · 5.1 KB
/
ClosestPairOfPoints.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
/**
* This class implements the Closest Pair of Points algorithm using a divide-and-conquer approach.
* @see {@link https://en.wikipedia.org/wiki/Closest_pair_of_points_problem}
* @class
*/
export default class ClosestPair {
/** @private */
#points
/**
* Creates a Closest Pair instance.
* @constructor
* @param {Array<{x: number, y: number}>} points - An array of points represented as objects with x and y coordinates.
* @throws {Error} Will throw an error if the points array is empty or invalid.
*/
constructor(points) {
this.#validatePoints(points)
this.#points = points
}
/**
* Validates that the input is a non-empty array of points.
* @private
* @param {Array} points - The array of points to validate.
* @throws {Error} Will throw an error if the input is not a valid array of points.
*/
#validatePoints(points) {
if (
!Array.isArray(points) ||
points.length === 0 ||
points.some((p) => isNaN(p.x) || isNaN(p.y)) ||
!points.every((p) => typeof p.x === 'number' && typeof p.y === 'number')
) {
throw new Error(
'points must be a non-empty array of objects with x and y properties.'
)
}
}
/**
* Calculates the distance between two points.
* @private
* @param {{x: number, y: number}} p1 - The first point.
* @param {{x: number, y: number}} p2 - The second point.
* @returns {number} The distance between the two points.
*/
#distance(p1, p2) {
return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
}
/**
* Finds the closest pair of points in a small set (3 or fewer).
* @private
* @param {Array<{x: number, y: number}>} points - An array of points with size <= 3.
* @returns {{pair: Array<{x: number, y: number}>, distance: number}} The closest pair and their distance.
*/
#bruteForceClosestPair(points) {
let minDist = Infinity
let pair = []
for (let i = 0; i < points.length; i++) {
for (let j = i + 1; j < points.length; j++) {
const dist = this.#distance(points[i], points[j])
if (dist < minDist) {
minDist = dist
pair = [points[i], points[j]]
}
}
}
return { pair, distance: minDist }
}
/**
* Finds the closest pair of points using divide-and-conquer.
* @private
* @param {Array<{x: number, y: number}>} points - An array of points sorted by x-coordinate.
* @returns {{pair: Array<{x: number, y: number}>, distance: number}} The closest pair and their distance.
*/
#closestPair(points) {
const n = points.length
if (n <= 3) {
return this.#bruteForceClosestPair(points)
}
const mid = Math.floor(n / 2)
const midPoint = points[mid]
// Recursive calls for left and right halves
const leftResult = this.#closestPair(points.slice(0, mid))
const rightResult = this.#closestPair(points.slice(mid))
// Find the overall closest pair
let minResult =
leftResult.distance < rightResult.distance ? leftResult : rightResult
// Create an array for strip points within min distance from midPoint
const strip = this.#getStripPoints(points, midPoint, minResult.distance)
// Check strip for closer pairs
const stripResult = this.#stripClosestPair(strip, minResult.distance)
return stripResult.distance < minResult.distance ? stripResult : minResult
}
/**
* Gets the strip of points within a certain distance from a midpoint.
* @private
* @param {Array<{x: number, y: number}>} points - An array of sorted points.
* @param {{x: number, y: number}} midPoint - The midpoint used for filtering.
* @param {number} minDistance - The minimum distance to filter by.
* @returns {Array<{x: number, y: number}>} The filtered strip of points.
*/
#getStripPoints(points, midPoint, minDistance) {
return points.filter(
(point) => Math.abs(point.x - midPoint.x) < minDistance
)
}
/**
* Finds the closest pair in a strip of points sorted by y-coordinate.
* @private
* @param {Array<{x: number, y: number}>} strip - An array of strip points sorted by y-coordinate.
* @param {number} minDistance - The minimum distance to check against.
* @returns {{pair: Array<{x: number, y: number}>, distance: number}} The closest pair and their distance from the strip.
*/
#stripClosestPair(strip, minDistance) {
let minDist = minDistance
let pair = []
// Sort by y-coordinate
strip.sort((a, b) => a.y - b.y)
for (let i = 0; i < strip.length; i++) {
for (
let j = i + 1;
j < strip.length && strip[j].y - strip[i].y < minDist;
j++
) {
const dist = this.#distance(strip[i], strip[j])
if (dist < minDist) {
minDist = dist
pair = [strip[i], strip[j]]
}
}
}
return { pair, distance: minDist }
}
/**
* Finds the closest pair of points in the provided set.
* @public
* @returns {{pair: Array<{x: number, y: number}>, distance: number}} The closest pair and their distance.
*/
findClosestPair() {
const sortedPoints = this.#points.slice().sort((a, b) => a.x - b.x)
return this.#closestPair(sortedPoints)
}
}