Skip to content

Randomized closest 2 points algorithm #6251

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

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
82fb1f5
closest pair program
bri-harris May 29, 2025
f1391bd
closest pair program
bri-harris May 30, 2025
63ddb36
As required for PRs, formatted with clang-format
bri-harris May 30, 2025
21fed7d
As required for PRs, URL of algorithm solution included in a comment
bri-harris May 30, 2025
b4c88a7
Fixed build errors
bri-harris May 30, 2025
d7949d6
Private constructor to prevent instanciation
bri-harris May 30, 2025
a7c2163
Fix bigDecimal erroref
bri-harris May 30, 2025
852c2e7
fix infer issue?
bri-harris May 30, 2025
f854cf7
Update .inferconfig
bri-harris May 30, 2025
b18bce9
Update infer.yml
bri-harris May 30, 2025
d1f7ec5
Trying to syncronise dependancies
bri-harris May 30, 2025
f49710a
fix: Remove pyml pin to resolve dependency conflict with Infer
DenizAltunkapan Jun 2, 2025
94c5ad4
Use a true randomized algorithm solution, Rabin's randomized
bri-harris Jun 3, 2025
b5c62ab
fix test case for identical points
bri-harris Jun 3, 2025
e89e6c4
used clang formatter on necessary file
bri-harris Jun 3, 2025
c93383b
fix build issues
bri-harris Jun 3, 2025
0e32962
rebase and fix build issues
bri-harris Jun 3, 2025
cac9706
remove * from import statments
bri-harris Jun 3, 2025
e05cfdd
no public or default constructors allowed
bri-harris Jun 3, 2025
c386ec9
closest pair class must be final
bri-harris Jun 3, 2025
bb66f17
not null error from workflow fixed
bri-harris Jun 3, 2025
92ff833
add null checker in algorithm
bri-harris Jun 3, 2025
20d0d31
add null checker in test and more distinct in rabin function
bri-harris Jun 3, 2025
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
Prev Previous commit
Next Next commit
Use a true randomized algorithm solution, Rabin's randomized
  • Loading branch information
bri-harris committed Jun 3, 2025
commit 94c5ad48d1a0793042d29f3a6a463ef5c962202e
150 changes: 77 additions & 73 deletions src/main/java/com/thealgorithms/randomized/ClosestPair.java
Original file line number Diff line number Diff line change
@@ -1,102 +1,106 @@
package com.thealgorithms.randomized;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

// As required by Repository, new algorithms have URL in comments with explanation
// https://www.geeksforgeeks.org/closest-pair-of-points-using-divide-and-conquer-algorithm
// Given 2 or more points on a 2-dimensional plane, find the closest 2 points in Euclidean distance
// This class uses the divide and conquer technique with recursion
class Point {
double x, y;

final class Point implements Comparable<Point> {
double x;
double y;

// Constructor to initialize a point with x and y coordinates
Point(double x, double y) {
public Point(double x, double y) {
this.x = x;
this.y = y;
}

public int compareTo(Point other) {
return Double.compare(this.x, other.x);
}

static double distance(Point p1, Point p2) {
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}

public final class ClosestPair {
// Private constructor to prevent instantiation
private ClosestPair() {
throw new AssertionError("Utility class should not be instantiated.");
}

public static double closest(List<Point> points) {
if (points == null || points.isEmpty()) {
throw new IllegalArgumentException("There are no pairs to compare.");
}

if (points.size() == 1) {
throw new IllegalArgumentException("There is only one pair.");
}

Collections.sort(points);
double result = closestRecursiveHelper(points, 0, points.size() - 1);
public class ClosestPair {
private static final double INFINITY = Double.MAX_VALUE;

// Return distance of closest pair rounded to 2 decimal places
return new BigDecimal(String.valueOf(result)).setScale(2, RoundingMode.HALF_UP).doubleValue();
public static double euclideanDistance(Point p1, Point p2) {
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
}

private static double closestRecursiveHelper(List<Point> points, int left, int right) {
// Base Case occurs with 3 or fewer points
if (right - left <= 2) {
return baseCase(points, left, right);
/**
* Algorithm Proof https://www.cs.toronto.edu/~anikolov/CSC473W20/kt-rabin.pdf
* Additional information: https://en.wikipedia.org/wiki/Closest_pair_of_points_problem
* This class uses Rabin's randomized approach to find the closest pair of points.
* Rabin's approach randomly selects a sample of points to estimate an initial closest distance
* (delta), then uses a grid for "probabilistic refinement". Finally, it updates the closest pair
* with the closest distance.
*/

public static Object[] rabinRandomizedClosestPair(List<Point> points) {
// Error handling, must have at least 2 points
if (points == null || points.size() < 2) {
return new Object[] {null, null, INFINITY};
}

// Divide and conquer
int mid = (left + right) / 2;
double midX = points.get(mid).x;

double leftDist = closestRecursiveHelper(points, left, mid);
double rightDist = closestRecursiveHelper(points, mid + 1, right);
Collections.shuffle(points, new Random()); // shuffle for required randomness

double minDist = Math.min(leftDist, rightDist);
double delta = INFINITY; // initialize distance
Point closestA = null;
Point closestB = null;

return checkBoundary(points, left, right, midX, minDist);
}
// without exceeding number of points, work with some sample
int sampleSize = Math.min(7, points.size());

private static double baseCase(List<Point> points, int left, int right) {
// Sub-problems fitting the base case can use brute force
double minDist = Double.MAX_VALUE;
for (int i = left; i <= right; i++) {
for (int j = i + 1; j <= right; j++) {
minDist = Math.min(minDist, Point.distance(points.get(i), points.get(j)));
}
Random random = new Random(); // select randomly
Set<Point> sampleSet = new HashSet<>(); // ensure unique pairs
while (sampleSet.size() < sampleSize) {
sampleSet.add(points.get(random.nextInt(points.size())));
}
return minDist;
}

private static double checkBoundary(List<Point> points, int left, int right, double midX, double minDist) {
// Consider a boundary by the dividing line
List<Point> boundary = new ArrayList<>();
for (int i = left; i <= right; i++) {
if (Math.abs(points.get(i).x - midX) < minDist) {
boundary.add(points.get(i));
List<Point> sample = new ArrayList<>(sampleSet);

// initially the closest points are found via brute force
for (int i = 0; i < sample.size(); i++) {
for (int j = i + 1; j < sample.size(); j++) {
double dist = euclideanDistance(sample.get(i), sample.get(j));
if (dist < delta) {
closestA = sample.get(i);
closestB = sample.get(j);
delta = dist; // update distance
}
}
}

// sort by y coordinate within the boundary and check for closer points
boundary.sort(Comparator.comparingDouble(p -> p.y));
for (int i = 0; i < boundary.size(); i++) {
for (int j = i + 1; j < boundary.size() && (boundary.get(j).y - boundary.get(i).y) < minDist; j++) {
minDist = Math.min(minDist, Point.distance(boundary.get(i), boundary.get(j)));
// Create a grid, We will use "Probabilistic Filtering" by only checking
// neighboring grids to prevent bruteforce checking outside initialization
Map<String, Point> grid = new HashMap<>();

// coordinates computed based on delta, estimated closest distance
for (Point p : points) {
int gridX = (int) (p.x / delta);
int gridY = (int) (p.y / delta);
String key = gridX + "," + gridY; // string for indexing

// check neighboring cells
for (int dX = -1; dX <= 1; dX++) {
for (int dY = -1; dY <= 1; dY++) {
String neighborKey = (gridX + dX) + "," + (gridY + dY);
Point neighborValue = grid.get(neighborKey);

// update points only if valid neighbor
if (neighborValue != null && p != neighborValue) {
double dist = euclideanDistance(p, neighborValue);
if (dist < delta) {
closestA = p;
closestB = neighborValue;
delta = dist;
}
}
}
}
grid.put(key, p);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think currently, only one point is stored per grid cell, overwriting any existing points. This can cause missed closest pairs when multiple points fall into the same cell. It is recommended to store a list of points per cell to ensure correctness...

}
return minDist;
return new Object[] {closestA, closestB, delta};
}
}
58 changes: 36 additions & 22 deletions src/test/java/com/thealgorithms/randomized/ClosestPairTest.java
Original file line number Diff line number Diff line change
@@ -1,43 +1,57 @@
package com.thealgorithms.randomized;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import org.junit.jupiter.api.Test;

public class ClosestPairTest {
class ClosestPairTest {

// Tests sorting of an array with multiple elements, including duplicates.
@Test
public void testMultiplePairs() {
List<Point> points = Arrays.asList(new Point(1, 2), new Point(3, 4), new Point(5, 1), new Point(7, 8), new Point(2, 3), new Point(6, 2));
double expected = 1.41;
assertEquals(expected, ClosestPair.closest(points));
void testStandardCaseClosestPair() {
List<Point> points = Arrays.asList(new Point(1, 4), new Point(2, 8), new Point(0, 1), new Point(4, 5), new Point(9, 4));
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points);
assertNotEquals(closestPair[0], closestPair[1], "Points are distinct");
assertTrue((double) closestPair[2] > 0, "Distance must be positive");
}

// Test if there are no pairs.
@Test
public void testNoPoints() {
List<Point> points = new ArrayList<>();
Exception exception = assertThrows(IllegalArgumentException.class, () -> { ClosestPair.closest(points); });
assertEquals("There are no pairs to compare.", exception.getMessage());
void testTwoDistinctPoints() {
List<Point> points = Arrays.asList(new Point(1, 2), new Point(2, 3));
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points);
assertTrue((closestPair[0].equals(points.get(0)) && closestPair[1].equals(points.get(1))) || (closestPair[1].equals(points.get(0)) && closestPair[0].equals(points.get(1))));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this test is correct? Because the .equals method is not overwritten in the Point class

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please refine and improve the test a bit? The likely cause of the NullPointer warning is that the analysis tool doesn't recognize assertNotNull() as a safe null check—assigning the value to a variable before asserting might help. Thats probably why infer is failing

assertEquals(closestPair[2], ClosestPair.euclideanDistance(points.get(0), points.get(1)));
}

// Test if there is one point, no pairs.
@Test
public void testOnePoint() {
List<Point> points = Arrays.asList(new Point(1, 2));
Exception exception = assertThrows(IllegalArgumentException.class, () -> { ClosestPair.closest(points); });
assertEquals("There is only one pair.", exception.getMessage());
void testIdenticalPointsPairWithDistanceZero() {
List<Point> points = Arrays.asList(new Point(1.0, 2.0), new Point(1.0, 2.0), new Point(1.0, 1.0));
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points);
assertTrue((closestPair[0].equals(points.get(0)) && closestPair[1].equals(points.get(1))));
assertEquals(0, (double) closestPair[2], "Distance is zero");
}

@Test
void testLargeDatasetRandomPoints() {
List<Point> points = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < 1000; i++) {
points.add(new Point(random.nextDouble() * 100, random.nextDouble() * 100));
}
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points);
assertNotNull(closestPair[0]);
assertNotNull(closestPair[1]);
assertTrue((double) closestPair[2] > 0, "Distance must be positive");
}

// Test if there is a duplicate points as a pair
@Test
public void testPoints() {
List<Point> points = Arrays.asList(new Point(1, 2), new Point(5, 1), new Point(5, 1), new Point(7, 8), new Point(2, 3), new Point(6, 2));
double expected = 0.00;
assertEquals(expected, ClosestPair.closest(points));
void testSinglePointShouldReturnNoPair() {
List<Point> points = Arrays.asList(new Point(5.0, 5.0));
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points);
assertNull(closestPair[0]);
assertNull(closestPair[1]);
}
}