Skip to content

Commit 322ca8e

Browse files
committed
fix: style
1 parent 9010481 commit 322ca8e

File tree

2 files changed

+280
-0
lines changed

2 files changed

+280
-0
lines changed

Geometry/ClosestPairOfPoints.js

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

0 commit comments

Comments
 (0)