Lets say we are given a set of
n points and we need to find every line segment that connects 4 or more of the points.
We have an immutable data type for Point:
public class Point implements Comparable {
private final int x;
private final int y;
//...
public int compareTo(Point that) {
if (that.y == y && that.x == x) {
return 0;
}
if (y < that.y) {
return -1;
}
if (y > that.y) {
return 1;
}
if (x < that.x) {
return -1;
}
// x > that.x
return 1;
}
}
So the comparison first looks at the y coordinate and then there is a tie-break on the x coordinate.
public double slopeTo(Point that) {
if (that.x == x && that.y == y) {
return Double.NEGATIVE_INFINITY;
}
return ((double) (that.y - y)) / (that.x - x);
}
We define that if the coordinates are the same, slope is negative infinity. It is a good idea to search internet for detailed explanations of a slope and types of infinities.
One of the key insights is that we can use the order of the slopes to find points that form a line segment:
public Comparator slopeOrder() {
return (o1, o2) -> {
if (o1 == null) {
return 1;
}
if (o2 == null) {
return 1;
}
double slope1 = o1.slopeTo(this);
double slope2 = o2.slopeTo(this);
if (slope1 == slope2) {
return o1.compareTo(o2);
}
if (slope1 > slope2) {
return 1;
}
else {
return -1;
}
};
}
One by one, simple but important abstractions are guiding us to the solution. This method is a very nice example of using a Comparator.
We will of course have a line segment class:
public class LineSegment {
private final Point p;
private final Point q;
Now here comes the first attempt to find the line segments: BRUTE FORCE!
public class BruteCollinearPoints {
public BruteCollinearPoints(Point[] points) { // finds all line segments containing 4 points
// some argument validations
//...
// Here we have O(N logN) complexity
Arrays.sort(points);
ArrayList result = new ArrayList<>();
ArrayList lines = new ArrayList<>();
// Here we have O(N^4) complexity.
for (int i = 0; i < points.length; ++i) {
for (int j = i + 1; j < points.length; ++j) {
double slope1 = points[i].slopeTo(points[j]);
for (int k = j + 1; k < points.length; ++k) {
double slope2 = points[i].slopeTo(points[k]);
if (slope1 != slope2) {
continue;
}
for (int l = k + 1; l < points.length; ++l) {
double slope3 = points[i].slopeTo(points[l]);
if (slope1 == slope2 && slope2 == slope3) {
Point[] segPoints = new Point[] {
points[i], points[j], points[k], points[l]
};
lines.add(new Line(segPoints[0], segPoints[3]));
}
}
}
}
}
for (Line line : lines) {
LineSegment lineSegment = new LineSegment(line.p1, line.p2);
result.add(lineSegment);
}
LineSegment[] lineSegments = new LineSegment[result.size()];
this.lineSegments = result.toArray(lineSegments);
}
}
Now this brute force algorithm tries to evaluate all the possible combinations. And it is not a suprise to get a very slow runtime, the result of a O(N^4) complexity.
The good news is, we have a working solution. We evaluated the performance and came to the conclusion that the performance is not acceptible. It is time to find another algorithm. This is the scientific approach that gave human race the ability to send rockets to the sky.
Next: A sorting based solution that is faster:
- We will sort the points according to the slopes they makes with a point P.
- Then we will check if any 3 or more adjacent points in the sorted order have equal slopes with respect to
P. If their slopes are equal then they form a line segment.
- We need to apply this method for each point. This algorithm is fast because it brings together the points that have the potential of creating a line segment.
public FastCollinearPoints(Point[] points) {
// argument validations
//...
Arrays.sort(points);
ArrayList allLines = new ArrayList<>();
for (int i = 0; i < points.length; ++i) {
ArrayList copy = new ArrayList<>(Arrays.asList(points));
Point origin = copy.get(i);
copy.sort(copy.get(i).slopeOrder());
List currentPoints = new ArrayList<>();
Double slope = null;
for (int j = 0; j < copy.size(); ++j) {
if (origin.equals(copy.get(j))) {
continue;
}
if (slope == null) {
slope = copy.get(j).slopeTo(origin);
currentPoints.add(copy.get(j));
} else {
if (copy.get(j).slopeTo(origin) == slope) {
currentPoints.add(copy.get(j));
} else {
if (currentPoints.size() >= 3) {
Point start, end;
if (origin.compareTo(currentPoints.get(0)) < 0) {
start = origin;
} else {
start = currentPoints.get(0);
}
if (origin.compareTo(currentPoints.get(currentPoints.size() - 1)) > 0) {
end = origin;
} else {
end = currentPoints.get(currentPoints.size() - 1);
}
allLines.add(new Line(start, end));
}
currentPoints = new ArrayList<>();
currentPoints.add(copy.get(j));
slope = copy.get(j).slopeTo(origin);
}
}
}
if (currentPoints.size() >= 3) {
Point start, end;
if (origin.compareTo(currentPoints.get(0)) < 0) {
start = origin;
} else {
start = currentPoints.get(0);
}
if (origin.compareTo(currentPoints.get(currentPoints.size() - 1)) > 0) {
end = origin;
} else {
end = currentPoints.get(currentPoints.size() - 1);
}
allLines.add(new Line(start, end));
}
}
List usedLines = new ArrayList<>();
ArrayList lineSegmentList = new ArrayList<>();
for (Line line : allLines) {
if (!usedLines.contains(line)) {
LineSegment lineSegment = new LineSegment(line.start, line.end);
lineSegmentList.add(lineSegment);
usedLines.add(line);
}
}
lineSegments = new LineSegment[lineSegmentList.size()];
lineSegments = lineSegmentList.toArray(lineSegments);
}
There is a little code duplication here and let us leave removing it to the reader as an exercise. There are of course implementation details that can best be understood by getting our hands dirty. Below are some unit tests:
@Test
public void shouldFindCollinearPoints() {
Point[] points = new Point[] {
new Point(0, 0),
new Point(10, 10),
new Point(20, 25),
new Point(30, 30),
new Point(40, 40)
};
FastCollinearPoints fastCollinearPoints = new FastCollinearPoints(points);
assertEquals(1, fastCollinearPoints.numberOfSegments());
assertEquals("(0, 0) -> (40, 40)", fastCollinearPoints.segments()[0].toString());
}
@Test
public void shouldFindLongestSegment() {
FastCollinearPoints collinearPoints = new FastCollinearPoints(
new Point[] {
new Point(9000, 9000),
new Point(8000, 8000),
new Point(7000, 7000),
new Point(6000, 6000),
new Point(5000, 5000),
new Point(4000, 4000),
new Point(3000, 3000),
new Point(2000, 2000),
new Point(1000, 1000),
});
for (LineSegment segment : collinearPoints.segments()) {
System.out.println(segment.toString());
segment.draw();
}
assertEquals(1, collinearPoints.numberOfSegments());
assertTrue("(9000, 9000) -> (1000, 1000)".equals(collinearPoints.segments()[0].toString())
|| "(1000, 1000) -> (9000, 9000)".equals(collinearPoints.segments()[0].toString()
));
}
@Test
public void shouldHandleAPointThatBelongsToMultipleLines() {
FastCollinearPoints collinearPoints = new FastCollinearPoints(
new Point[] {
new Point(0, 0),
new Point(1, 1),
new Point(2, 2),
new Point(3, 3),
new Point(0, 1),
new Point(0, 2),
new Point(0, 3),
});
assertEquals(2, collinearPoints.numberOfSegments());
}
@Test
public void shouldFindLinesWithSameSlope() {
FastCollinearPoints collinearPoints = new FastCollinearPoints(
new Point[] {
new Point(0, 0),
new Point(0, 1),
new Point(0, 2),
new Point(0, 3),
new Point(5, 1),
new Point(5, 2),
new Point(5, 3),
new Point(5, 4),
});
assertEquals(2, collinearPoints.numberOfSegments());
}
Complexity of this sorting based algorithm is O(N^2 log N). This is a very good example of using sorting to solve a pattern recognition problem. Sorting algorithms like merge sort and quick sort are one of the most important discoveries of our century.
* Thanks to Coursera and Princeton University for the beautiful assignments on Algorithms. Visit
Comments