Pattern Recognition
给定点的集合,实现对于线段的识别。
Point.java
Point类中主要实现了关于点和斜率的比较方法
import java.util.Arrays;
import java.util.Comparator;
import edu.princeton.cs.algs4.StdDraw;
import edu.princeton.cs.algs4.StdRandom;
/**
* The {@code Point} class represents a point in the plane
*
* @author zhangyu
* @date 2017.3.19
*/
public class Point implements Comparable<Point>
{
private final int x; // x-coordinate of this point
private final int y; // y-coordinate of this point
/**
* Initializes a new point.
*
* @param x the <em>x</em>-coordinate of the point
* @param y the <em>y</em>-coordinate of the point
*/
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
/**
* Draws this point to standard draw.
*/
public void draw()
{
StdDraw.point(x, y);
}
/**
* Draws the line segment between this point and the specified point
* to standard draw.
*
* @param that the other point
*/
public void drawTo(Point that)
{
StdDraw.line(this.x, this.y, that.x, that.y);
}
/**
* Returns the slope between this point and the specified point.
* Formally, if the two points are (x0, y0) and (x1, y1), then the slope
* is (y1 - y0) / (x1 - x0). For completeness, the slope is defined to be
* +0.0 if the line segment connecting the two points is horizontal;
* Double.POSITIVE_INFINITY if the line segment is vertical;
* and Double.NEGATIVE_INFINITY if (x0, y0) and (x1, y1) are equal.
*
* @param that the other point
* @return the slope between this point and the specified point
*/
public double slopeTo(Point that)
{
if (x == that.x)
{
if (y == that.y) return Double.NEGATIVE_INFINITY;
else return Double.POSITIVE_INFINITY;
}
if (y == that.y) return 0 / 1.0;
return (y - that.y) * 1.0 / (x - that.x);
}
/**
* Compares two points by y-coordinate, breaking ties by x-coordinate.
* Formally, the invoking point (x0, y0) is less than the argument point
* (x1, y1) if and only if either y0 < y1 or if y0 = y1 and x0 < x1.
*
* @param that the other point
* @return the value <tt>0</tt> if this point is equal to the argument
* point (x0 = x1 and y0 = y1);
* a negative integer if this point is less than the argument
* point; and a positive integer if this point is greater than the
* argument point
*/
public int compareTo(Point that)
{
if (y > that.y) return 1;
else if (y < that.y) return -1;
else if (x > that.x) return 1;
else return x < that.x ? -1 : 0;
}
/**
* Compares two points by the slope they make with this point.
* The slope is defined as in the slopeTo() method.
*
* @return the Comparator that defines this ordering on points
*/
public Comparator<Point> slopeOrder()
{
return new SlopeOrder();
}
private class SlopeOrder implements Comparator<Point>
{
public int compare(Point p, Point q)
{
if (slopeTo(p) < slopeTo(q)) return -1;
if (slopeTo(p) > slopeTo(q)) return +1;
return 0;
}
}
/**
* Returns a string representation of this point.
* This method is provide for debugging;
* your program should not rely on the format of the string representation.
*
* @return a string representation of this point
*/
public String toString()
{
return "(" + x + ", " + y + ")";
}
/**
* Unit tests the Point data type.
*
* @param args the command-line arguments
*/
public static void main(String[] args)
{
int x0 = Integer.parseInt(args[0]);
int y0 = Integer.parseInt(args[1]);
int n = Integer.parseInt(args[2]);
StdDraw.setCanvasSize(800, 800);
StdDraw.setXscale(0, 50);
StdDraw.setYscale(0, 50);
StdDraw.setPenRadius(0.005);
StdDraw.enableDoubleBuffering();
Point[] points = new Point[n];
for (int i = 0; i < n; i++)
{
int x = StdRandom.uniform(50);
int y = StdRandom.uniform(50);
points[i] = new Point(x, y);
points[i].draw();
}
// draw p = (x0, x1) in red
Point p = new Point(x0, y0);
StdDraw.setPenColor(StdDraw.RED);
StdDraw.setPenRadius(0.02);
p.draw();
// draw line segments from p to each point, one at a time, in polar order
StdDraw.setPenRadius();
StdDraw.setPenColor(StdDraw.BLUE);
Arrays.sort(points, p.slopeOrder());
for (int i = 0; i < n; i++)
{
p.drawTo(points[i]);
StdDraw.show();
StdDraw.pause(100);
}
}
}
BruteCollinearPoints.java
BruteCollinearPoints使用暴力方法查找所有满足的结果,但是对于4个点以上的线段无能为力,时间复杂度为
O(n4)
O
(
n
4
)
。
找到4个点,还要找到其端点,因为是长度为4的小数组,这里使用插入排序。找到端点,创建线段实例,加入集合。
一点小优化:首先,不是遍历所有4个的元组,而是找出所有4个点的组合。还有,发现三个点不共线,第四个点就不必找了。
总之,这是暴力算法,微调优化的意义不大。
import java.util.ArrayList;
import java.util.Arrays;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.Insertion;
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.StdDraw;
/**
* The {@code BruteCollinearPoints} class examines 4 points at a time
* and checks whether they all lie on the same line segment, returning
* all such line segments.
* <p>
* To check whether the 4 points p, q, r, and s are collinear,
* check whether the three slopes between p and q, between p and r,
* and between p and s are all equal.
*
* @author zhangyu
* @date 2017.3.19
*/
public class BruteCollinearPoints
{
private ArrayList<LineSegment> lineSgmts; // ArrayList to save line segments
private Point[] pointSet; // copy of the give array points
/**
* Finds all line segments containing 4 points.
*
* @param points the given Point array
* @throws NullPointerException if the Point array is null
*/
public BruteCollinearPoints(Point[] points)
{
if (points == null) throw new NullPointerException("Null Point array");
int len = points.length;
pointSet = Arrays.copyOf(points, points.length); // copy original array
lineSgmts = new ArrayList<LineSegment>();
for (int i = 0; i < len; ++i)
{
for (int j = i + 1; j < len; ++j)
{
checkForDuplicates(pointSet[i], pointSet[j]); // avoid duplicate or empty point
for (int k = j + 1; k < len; ++k)
{
// only when three points are collinear, can the forth point be compared
if (pointSet[i].slopeTo(pointSet[j]) == pointSet[i].slopeTo(pointSet[k]))
{
for (int l = k + 1; l < len; ++l)
{
// if four points are collinear
if (pointSet[i].slopeTo(pointSet[k]) == pointSet[i].slopeTo(pointSet[l]))
{
Point[] tuple = {pointSet[i], pointSet[j],
pointSet[k], pointSet[l]};
// sort the array to find two end points of the segment
Insertion.sort(tuple); // Insertion sort is suitable for this small array
lineSgmts.add(new LineSegment(tuple[0], tuple[3]));
}
}
}
}
}
}
}
// check whether duplicate point exists
private void checkForDuplicates(Point p, Point q)
{
// ensure each point is not null
if (p == null || q == null) throw new NullPointerException("Null Point element");
if (p.compareTo(q) == 0) throw new IllegalArgumentException("Duplicate point");
}
/**
* Returns the number of line segments.
*
* @return the number of line segments
*/
public int numberOfSegments()
{
return lineSgmts.size();
}
/**
* Returns the line segments in array.
*
* @return a LineSegment array
*/
public LineSegment[] segments()
{
return lineSgmts.toArray(new LineSegment[numberOfSegments()]);
}
/**
* Unit tests the {@code BruteCollinearPoints} data type.
*
* @param args the command-line arguments
*/
public static void main(String[] args)
{
// read the n points from a file
In in = new In(args[0]);
int n = in.readInt();
Point[] points = new Point[n];
for (int i = 0; i < n; i++)
{
int x = in.readInt();
int y = in.readInt();
points[i] = new Point(x, y);
}
// draw the points
StdDraw.enableDoubleBuffering();
StdDraw.setXscale(0, 32768);
StdDraw.setYscale(0, 32768);
for (Point p : points) { p.draw(); }
StdDraw.show();
// print and draw the line segments
BruteCollinearPoints collinear = new BruteCollinearPoints(points);
for (LineSegment segment : collinear.segments())
{
StdOut.println(segment);
segment.draw();
}
StdDraw.show();
}
}
FastCollinearPoints.java
FastCollinearPoints比BruteCollinearPoints要快,时间复杂度为
O(n2logn)
O
(
n
2
log
n
)
。
与BruteCollinearPoints不同,FastCollinearPoints在循环前先排序,用归并排序或希尔排序都行,顺序由实现Comparable接口Point的compareTo()
方法规定。这确保首先查找线段的起始点是在左下。
进入循环后,对于每个起始点pointSet[i]
,对前面和后面的点根据斜率再排一次序,之所以对前面的点排序是因为后面需要判断找到的线段是否是子段,对后面的点排序则是确保和起始点相同斜率的点挨在一块。
这样子进行循环,找到4个或4个以上的点共线的线段,找到之后还要判断该线段是否是之前已经找到的线段的子线段。在这个地方我花了很长时间了,这里不准用hash,因为还没学到,还有就是hash不保证最坏情况下的性能保证,所以要保证运行时间有点困难。我想了几种思路。
第一种思路是保存线段的同时保存其末点和斜率。若找到下一个线段,首先看这个斜率是不是已经存在,不存在直接加入线段集合,存在就再看末点是不是相同,相同则说明这肯定是子线段(这是由之前的归并排序所保证的)。第二种思路是找到线段,就遍历看看前面的点与该起始点的斜率和找到的线段的斜率是不是一样,一样就说明是子线段。这两种都不行,因为都是线性时间,还有根据起始点找另一个端点这个操作在点很多时会重复很多次,重复多次的线性时间会让总时间很容易达到立方时间,成为性能瓶颈。
所以在这里我用了二分查找,这要求查找的数组有序,这就是之前对起始点前面的点排序的原因。如果找到符合的线段了,就二分查找前面的点与起始点的斜率和找到的线段的斜率是否相等,相等就说明肯定是子线段。二分查找的排序每个起始点一次,查找操作每个起始点多次,而时间是对数级别的。
lg ratio(slopeTo() + 2*compare()) = lg (60568797 / 15326643) = 1.98
=> passed
可以看到时间没有超过。slopeTo() + 2*compare()
约为平方时间。
import java.util.ArrayList;
import java.util.Arrays;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.Merge;
import edu.princeton.cs.algs4.StdDraw;
import edu.princeton.cs.algs4.StdOut;
/**
* The {@code FastCollinearPoints} class examines 4 or more points
* at a time and checks whether they all lie on the same line segment,
* returning all such line segments.
*
* @author zhangyu
* @date 2017.3.19
*/
public class FastCollinearPoints
{
private ArrayList<LineSegment> lineSgmts; // ArrayList to save line segments
private Point[] pointSet; // copy of the give Point array
/**
* Finds all line segments containing 4 or more points.
*
* @param points the given Point array
* @throws NullPointerException if the Point array is null
*/
public FastCollinearPoints(Point[] points)
{
if (points == null) throw new NullPointerException("Null Point array");
int len = points.length;
pointSet = Arrays.copyOf(points, points.length);
lineSgmts = new ArrayList<LineSegment>();
Merge.sort(pointSet); // sort the array by compareTo() method in Point class
for (int i = 0; i < len - 1; ++i)
{
double[] slopesBefore = new double[i]; // slopes of previous points to pointSet[i]
Point[] pointsAfter = new Point[len - i - 1]; // points after pointSet[i]
for (int j = 0; j < i; ++j)
slopesBefore[j] = pointSet[i].slopeTo(pointSet[j]);
for (int j = 0; j < len - i - 1; ++j) pointsAfter[j] = pointSet[i + j + 1];
Arrays.sort(slopesBefore); // for binary search
// sorting brings points collinear with pointSet[i] together
Arrays.sort(pointsAfter, pointSet[i].slopeOrder());
addNewSgmts(slopesBefore, pointSet[i], pointsAfter);
}
}
// add segments to the set if they are not sub-segments
private void addNewSgmts(double[] slopesBefore, Point srtPoint, Point[] pointsAfter)
{
int cnt = 1;
int lenOfSub = pointsAfter.length;
double slope = Double.NEGATIVE_INFINITY;
double lastSlope = Double.NEGATIVE_INFINITY;
for (int j = 0; j < lenOfSub; ++j)
{
checkForDuplicates(srtPoint, pointsAfter[j]);
slope = srtPoint.slopeTo(pointsAfter[j]);
if (lastSlope != slope) // if current slope isn't equal to the last
{
// 4 or more points are collinear, then add them to the sgmts set.
if (cnt >= 3 && !isSubSgmt(slopesBefore, lastSlope))
lineSgmts.add(new LineSegment(srtPoint, pointsAfter[j - 1]));
cnt = 1;
}
else cnt++;
lastSlope = slope;
}
// the last sgmt can be left out when the loop terminates
if (cnt >= 3 && !isSubSgmt(slopesBefore, lastSlope))
lineSgmts.add(new LineSegment(srtPoint, pointsAfter[lenOfSub - 1]));
}
// determine if the segment is a sub-segment of the previous segments
private boolean isSubSgmt(double[] slopesBefore, double slope)
{
int lo = 0;
int hi = slopesBefore.length - 1;
// use binary search
while (lo <= hi)
{
int mid = lo + (hi - lo) / 2;
if (slope < slopesBefore[mid]) hi = mid - 1;
else if (slope > slopesBefore[mid]) lo = mid + 1;
else return true;
}
return false;
}
// check whether duplicate point exists
private void checkForDuplicates(Point p, Point q)
{
// ensure each point is not null
if (p == null || q == null) throw new NullPointerException("Null Point element");
if (p.compareTo(q) == 0) throw new IllegalArgumentException("Duplicate point");
}
/**
* Returns the number of line segments.
*
* @return the number of line segments
*/
public int numberOfSegments()
{
return lineSgmts.size();
}
/**
* Returns the line segments in array.
*
* @return a LineSegment array
*/
public LineSegment[] segments() // the line segments
{
return lineSgmts.toArray(new LineSegment[numberOfSegments()]);
}
/**
* Unit tests the {@code FastCollinearPoints} data type.
*
* @param args the command-line arguments
*/
public static void main(String[] args)
{
// read the n points from a file
In in = new In(args[0]);
int n = in.readInt();
Point[] points = new Point[n];
for (int i = 0; i < n; i++)
{
int x = in.readInt();
int y = in.readInt();
points[i] = new Point(x, y);
}
// draw the points
StdDraw.enableDoubleBuffering();
StdDraw.setXscale(0, 32768);
StdDraw.setYscale(0, 32768);
for (Point p : points) { p.draw(); }
StdDraw.show();
// print and draw the line segments
FastCollinearPoints collinear = new FastCollinearPoints(points);
for (LineSegment segment : collinear.segments())
{
StdOut.println(segment);
segment.draw();
}
StdDraw.show();
}
}
ASSESSMENT SUMMARY
Compilation: PASSED
API: PASSED
Findbugs: FAILED (2 warnings)
Checkstyle: PASSED
Correctness: 41/41 tests passed
Memory: 1/1 tests passed
Timing: 41/41 tests passed
Aggregate score: 100.00%
[Compilation: 5%, API: 5%, Findbugs: 0%, Checkstyle: 0%, Correctness: 60%, Memory: 10%, Timing: 20%]