這本來應該是第三周的作業,但是由于其他作業逼近deadline,暫時推后了一周完成。
這周的assignment大大提高了我對這門課的看法,不得不說,Algorithms這門課的assignment部分設計得很好。為什么好?個人認為有以下幾點:
- 程序要求解耦
- 循序漸進,這周的作業不允許使用hashcode()
- 輸入變量不允許被程序改變
能做到1、3兩點,在工程中也是高質量代碼的體現
?
題目很簡單,即給出一個點集,找所有出任意四點或以上共線的線段,將該線段的端點new 一個segment()對象存起來。看起來很簡單,實際上到處都是坑TAT
第一部分是用brute force的方法做,由于4個及以上的點才是共線,這里要求中提出為了簡化,brute方法不考慮5個及以上的點。所以既是對點集進行4層循環即可,時間復雜度是O(n4)。
但是這里需要注意的是,輸入的點集,不能被直接操作,必須被賦值為拷貝才行,否則,外界如果更改了點集中的點,則會影響結果。這一點還說明了,java都是值傳遞,傳進來的是一個數組引用的copy。
另外辯解條件必須要注意,其實挺容易出錯的。


1 public class BruteCollinearPoints { 2 // finds all line segments containing 4 points 3 private int N; 4 private ArrayList<LineSegment> segment = new ArrayList<LineSegment>(); 5 6 public BruteCollinearPoints(Point[] points) { 7 if (points == null) 8 throw new java.lang.NullPointerException(); 9 N = 0; 10 Point[] copy = new Point[points.length]; 11 for (int i = 0; i < points.length; i++) { 12 copy[i] = points[i]; 13 } 14 Arrays.sort(copy); 15 for (int i = 0; i < copy.length - 1; i++) { 16 if (copy[i].compareTo(copy[i + 1]) == 0) { 17 throw new java.lang.IllegalArgumentException(); 18 } 19 } 20 for (int i = 0; i < copy.length - 3; i++) { 21 for (int j = i + 1; j < copy.length - 2; j++) { 22 double slope1 = copy[i].slopeTo(copy[j]); 23 for (int k = j + 1; k < copy.length - 1; k++) { 24 double slope2 = copy[i].slopeTo(copy[k]); 25 if (slope1 != slope2) 26 continue; 27 int temp = 0; 28 for (int l = k + 1; l < copy.length; l++) { 29 double slope3 = copy[i].slopeTo(copy[l]); 30 if (slope1 == slope3) 31 temp = l; 32 if ((l == copy.length - 1) && (temp != 0)) { 33 N++; 34 segment.add(new LineSegment(copy[i], copy[temp])); 35 } 36 } 37 } 38 } 39 } 40 } 41 42 // the number of line segments 43 public int numberOfSegments() { 44 return N; 45 } 46 47 // the line segments 48 public LineSegment[] segments() { 49 LineSegment[] results = new LineSegment[N]; 50 for (int i = 0; i < N; i++) { 51 results[i] = segment.get(i); 52 } 53 return results; 54 } 55 56 public static void main(String[] args) { 57 58 // read the N points from a file 59 // In in = new In(args[0]); 60 In in = new In("./collinear/rs1423.txt"); 61 int N = in.readInt(); 62 System.out.println(N); 63 Point[] points = new Point[N]; 64 for (int i = 0; i < N; i++) { 65 int x = in.readInt(); 66 int y = in.readInt(); 67 System.out.println("x:" + x + " y:" + y); 68 points[i] = new Point(x, y); 69 } 70 71 // draw the points 72 StdDraw.show(0); 73 StdDraw.setXscale(0, 32768); 74 StdDraw.setYscale(0, 32768); 75 for (Point p : points) { 76 p.draw(); 77 } 78 StdDraw.show(); 79 80 // print and draw the line segments 81 BruteCollinearPoints collinear = new BruteCollinearPoints(points); 82 for (LineSegment segment : collinear.segments()) { 83 StdOut.println(segment); 84 segment.draw(); 85 } 86 } 87 }
由于這個方法時間復雜度太高,于是提出一種N2?log?N?的方法,核心思路就是,通過實現一個排序方法,使點始終保持有序性來提高效率。看到N*NlogN,就想到了遍歷點后排序,orz。下面是官方給出的算法描述:
- Think of?p?as the origin.
- For each other point?q, determine the slope it makes with?p.
- Sort the points according to the slopes they makes with?p.
- Check if any 3 (or more) adjacent points in the sorted order have equal slopes with respect to?p. If so, these points, together with?p, are collinear.
實現的思路就是:
- 對所有點排序,越靠近左下角的點越小
- 遍歷每一個點,遍歷點P過程中,先將其他點根據與P的斜率進行排序
- 對排序后的其他點進行遍歷,若有斜率相同超過4個以上的點,即是要找的線段
按照這個思路,很快就實現除了代碼,需要注意的除了邊界等細節問題,再有就是,如何避免子線段和重復線段。這兩個問題把我卡住很久。
先說如何解決子線,所謂子線段就是原本線段:a->b->c->d->e是符合條件的,我將其取出,但是我又把b->c->d->e也當作新的線段取出來了。一開始的思路是,每次遍歷一個點,都將其所有在一條線段上的點找出來,然后將這些點集sort一下,取最大最小。即在搜尋b的時候,將a,c,d,e和其本身b進行排序,取最小a和最大e。這樣問題就解決了,但是同樣還有一個問題是,由于每個點都進行了這樣的操作,會導致有很多重復的線段。通過a得到線段(a,e),通過b又得到一邊(a,e),所以結果是需要去重的。非常自然的想到了hashset,保證了時間復雜度不高。
然而使用hashcode的思路都無法實現...原因是poin和segment兩個class都繼承了Object的hashcode()方法,但是僅僅是在調用時拋出異常...至于為何會這樣,原因是第三周還沒有講到hashtable這個數據結構orz
這條路不行,退一步,用toString()方法,將其toString后,存入hashset,來檢查是否有重復的線段。我太機智了。不過這樣依然是不是100%的通過,看了下testcase,竟然有一條fail是說我不應該依賴于toString()方法...
?
為什么不讓?我陷入了沉思,comment中提到,這個方法僅供debug用,不要用于實現算法...不過細想其實是非常有道理的。我的算法本身實際上是依賴于comparable接口以及camparator的實現。而不是依賴于point和segment這兩個具體的類。所以本身我就不應該去依賴于這兩個類其他的方法,用了toString()只能說是非常tricky的方法。這樣的做法,做到了解耦。看到這個testcase,感覺自己和名校的學生差距就是在這些細節上拉開的。不做這樣的題,意識不到interface到底有啥用,不就是定義了幾個未實現的方法嗎?可是正是通過約定好的接口,做到了解耦。以后如果有人要修改point toString(),依然不會影響到我的程序的正確性。這就無形中提高了效率。
?
好了,回來說說我是怎么解決這個問題的。注意到,我在遍歷之前,就先sort了點,這個sort有啥用呢?這個sort意味著,等會對于每個點找共線線段時,是按照從左下往右上的順序來的,看圖說話:
排過序的點是按照1,2,3,4,5的順序來遍歷的。當選取點1時,找到共線的5個點{1,2,3,4,5},因此我們選擇最大最小作為線段(1,5)。
當選取點2時,找到了共線的5個點{1,2,3,4,5},這里也這里發現2<1,由于有序性,我們可以確認在此之前就有(1,5)被選取過了,所以這次的選擇可以直接跳過。
這意味著,只要是當前遍歷的點P不是共線點集中最小的點,我們都可以直接拋棄。這樣就完美避免了重復的線段。
至此,根據這個方案,這個assignment圓滿解決了。
一下是fast方法的實現,其他的代碼就不貼了。


package collinear;import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet;import edu.princeton.cs.algs4.In; import edu.princeton.cs.algs4.StdDraw; import edu.princeton.cs.algs4.StdOut;public class FastCollinearPoints {private int N;private ArrayList<LineSegment> segment = new ArrayList<LineSegment>();private Point[] points;public FastCollinearPoints(Point[] points) {if (points == null)throw new java.lang.NullPointerException();this.points = new Point[points.length];for (int i = 0; i < points.length; i++) {this.points[i] = points[i];}N = 0;Arrays.sort(this.points);// remove repeat pointsfor (int i = 0; i < this.points.length - 1; i++) {if (this.points[i].compareTo(this.points[i + 1]) == 0) {throw new java.lang.IllegalArgumentException();}}Point[] temp = new Point[this.points.length];Point[] copy = new Point[this.points.length];ArrayList<ArrayList<Point>> end_sets = new ArrayList<>();// use a copy to sortfor (int i = 0; i < this.points.length; i++) {temp[i] = copy[i] = this.points[i];end_sets.add(new ArrayList<Point>());}Arrays.sort(copy);// to sort by slopefor (int i = 0; i < copy.length - 1; i++) {Arrays.sort(temp, copy[i].slopeOrder());// find same slope copy then save them as segment in segment// ArrayListint count = 1;double slope0 = copy[i].slopeTo(temp[0]);ArrayList<Point> set = new ArrayList<>();for (int j = 0; j < temp.length; j++) {// record max pointdouble slope1 = copy[i].slopeTo(temp[j]);if (slope1 == slope0) {set.add(temp[j]);count++;if (count > 2 && j == temp.length - 1) {set.add(copy[i]);Collections.sort(set);// System.out.println(set);if (set.get(0).compareTo(copy[i]) < 0) {} else {// no key, no setsegment.add(new LineSegment(set.get(0), set.get(set.size() - 1)));N++;} count = 1;}} else {if (count > 2) {set.add(copy[i]);Collections.sort(set);// System.out.println(set);if (set.get(0).compareTo(copy[i]) < 0) {} else {// no key, no setsegment.add(new LineSegment(set.get(0), set.get(set.size() - 1)));N++;}}set = new ArrayList<>();set.add(temp[j]);count = 1;}slope0 = slope1;}}}// the number of line segmentspublic int numberOfSegments() {return N;}// the line segmentspublic LineSegment[] segments() {LineSegment[] results = new LineSegment[N];for (int i = 0; i < N; i++) {results[i] = segment.get(i);}return results;}public static void main(String[] args) {// read the N points from a file// In in = new In(args[0]);In in = new In("./collinear/rs1423.txt");int N = in.readInt();// System.out.println(N);Point[] points = new Point[N];for (int i = 0; i < N; i++) {int x = in.readInt();int y = in.readInt();// System.out.println("x:" + x + " y:" + y);points[i] = new Point(x, y);}// draw the pointsStdDraw.show(0);StdDraw.setXscale(0, 32768);StdDraw.setYscale(0, 32768);for (Point p : points) {p.draw();}StdDraw.show();// // print and draw the line segmentsFastCollinearPoints collinear = new FastCollinearPoints(points);for (LineSegment segment : collinear.segments()) {StdOut.println(segment);segment.draw();}} }
老規矩,亮下分數:
?
?