一、問題引入
網絡上經常會遇到判斷圖形個數的題目,如下例:
如果我們要把圖中所有三角形一個一個選出來,在已知每個交點的前提下,該如何用代碼判斷我們選的圖形是否是三角形呢。如下圖,如何把圖3篩選出來呢?
這里需要用到兩步:
1.得到所選圖形(陰影部分)所包含的所有小圖形的頂點集合,求集合的凸包,根據凸包頂點個數判定凸包圍成的圖形是否是三角形,若頂點個數不為3則不是三角形,如圖(1)。
.2.若凸包圍成的圖形是三角形,判斷凸包的面積與所選圖形(所有選中的小圖形面積之和)是否相等,若相等則所選圖形是三角形,若不相等則不是三角形,如上圖(2)。
二、求點的凸包——Graham's Scan法介紹
(1) 什么是凸包
在二維歐幾里得空間中,凸包可想象為一條剛好包着所有點的橡皮圈。
用不嚴謹的話來講,給定二維平面上的點集,凸包就是將最外層的點連接起來構成的凸多邊型,它能包含點集中所有的點。如下圖紅色部分:
(2) Graham's Scan法求凸包
這個算法是由數學大師葛立恆(Graham)發明的,算法思路如下:
1.找出所有點中y坐標最小的點,若y坐標最小的點有兩個以上,則選擇其中x坐標最小的點,將此點記為H;
2.設除H之外所有點的坐標集合為N{p1,p2,p3,p4,p5,p6,…},分別計算向量< H,p1>,< H,p2>,< H,p3>極坐標的角度,對集合N按極坐標角度的大小排序,即以H為圓心,順時針掃描各點,將掃描到的點依次加入集合中;如下圖,排好序的坐標集合為N{a1,a2,a3,a4,a5,a6},其中θ為向量< H,a1>極坐標的角度。
3.線段< H,a1>一定在凸包上,現在加入a2,假設a2也在凸包上,接下來加入a3,如果a3在向量<a1,a2>的左側則判斷a2不在凸包上,需將a2從凸包中移除,在此例中a3在向量<a1,a2>的右側;然后加入a4,如果a4在向量<a2,a3>的左側則判斷a3不在凸包上,此例中a4在向量<a2,a3>的左側,所以需將a3從凸包中移除,接下來需回溯判斷a4在向量<a1,a2>的左側還是右側,決定是否要將a2移除…,即每加入一點時,必須考慮到前面的線段是否在凸包上。從基點開始,凸包上每條相臨的線段的旋轉方向應該一致,並與掃描方向相同。如果發現新加的點使得新線段與上線段的旋轉方向發生變化,則可判定上一點必然不在凸包上。如下圖:
當加入d點時,發現<c,d>和<b,c>的旋轉方向不一致(d在<b,c>左側),則說明c點不在凸包上。
可用叉積來判斷一個點在一個向量的左側還是右側,如上圖,若<b,c>與<c,d>的叉積為正則d在<b,c>的右側,若為負則在<b,c>的右側,若為0,在d在<b,c>直線上。
復雜度
這個算法可以直接在原數據上進行運算,因此空間復雜度為O⑴。但如果將凸包的結果存儲到另一數組中,則可能在代碼級別進行優化。由於在掃描凸包前要進行排序,因此時間復雜度至少為快速排序的O(nlgn)。后面的掃描過程復雜度為O(n),因此整個算法的復雜度為O(nlgn)。
三、求解凸包的javascript代碼
設數組points是存儲所選圖形的所有頂點的集合
1 var convexPoints=[];//用來存儲凸包 2 var startPoint=getStartPoint(points);//得到y坐標最小的點 3 points.splice(points.indexOf(startPoint),1); 4 points.sort(compare);//按極坐標排序 5 convexPoints.push(startPoint); 6 convexPoints.push(points[0]); 7 8 for(i=1;i<points.length;i++){ 9 var vector1={ x:convexPoints[convexPoints.length-1].x-convexPoints[convexPoints.length-2].x, 10 y:convexPoints[convexPoints.length-1].y-convexPoints[convexPoints.length-2].y}; 11 12 var vector2={ x:points[i].x-convexPoints[convexPoints.length-1].x, 13 y:points[i].y-convexPoints[convexPoints.length-1].y}; 14 //若兩個向量叉積小於0,則需將上一個點移除 15 while(getCross(vector1,vector2)<0){ 16 convexPoints.pop(); 17 vector1={ x:convexPoints[convexPoints.length-1].x-convexPoints[convexPoints.length-2].x, 18 y:convexPoints[convexPoints.length-1].y-convexPoints[convexPoints.length-2].y}; 19 vector2={ x:points[i].x-convexPoints[convexPoints.length-1].x, 20 y:points[i].y-convexPoints[convexPoints.length-1].y}; 21 } 22 convexPoints.push(points[i]); 23 }
求points集合中y坐標最小的點,y坐標相同的情況下,取x坐標最小的點
1 //選出y軸最小的點startPoint 2 function getStartPoint(points){ 3 var startPoint=points[0]; 4 for(var i=1;i<points.length;i++){ 5 if(points[i].y<startPoint.y){ 6 startPoint=points[i]; 7 }else if(points[i].y==startPoint.y){ 8 if(points[i].x<startPoint.x){ 9 startPoint=points[i]; 10 } 11 } 12 } 13 return startPoint 14 }
按極坐標角度排序的compare函數
1 //各點按極坐標的角度排序 2 function compare(value1,value2){ 3 var value1Angle=getPolarAngle(startPoint,value1); 4 var value2Angle=getPolarAngle(startPoint,value2); 5 if(value1Angle<value2Angle){ 6 return -1; 7 }else if (value1Angle>value2Angle){ 8 return 1; 9 }else{ 10 return 0; 11 } 12 }
用此凸包判斷所選圖形是否是三角形時遇到的一個問題:
如下圖所示,我們對陰影部分包含的點求凸包時,希望得到的是{A,B,C}三個點,這樣我們通過判斷凸包的大小即可知凸包的形狀,而事實上我們通過以上算法求得的凸包可能會包含D,E,F,是否包含取決於對D,E,F坐標的采樣精度,那么該如何從凸包中把這些點刪掉呢?
刪除凸包中兩個頂點組成的線段中間的點:如下圖,當一條線段中間的點的采樣坐標凹向圖形內部的時候會被凸包算法排除掉(如D點),當采樣坐標凸出來的時候就會被凸包算法計算在內(如E,F點),我們可以計算出<A,E>和<E,B>向量的夾角,當這個夾角小於某一范圍時(取決於對采樣誤差的估算),我們就可認定E點在AB上,即可將E點從凸包中刪除。按此方法依次檢測凸包中所有的點,最后會得到我們選中的圖形的各頂點。
代碼如下:
1 //去掉一條線上中間的點,留下頂點 2 convexPoints.push(startPoint);//為了判定凸包最后一個點,需將起始點加在末尾形成一個環 3 deletePointIndexs=[];//存儲要刪除的點的索引 4 for(i=0;i<convexPoints.length-2;i++){ 5 var vector1={x:convexPoints[i+1].x-convexPoints[i].x,y:convexPoints[i+1].y-convexPoints[i].y} 6 var vector2={x:convexPoints[i+2].x-convexPoints[i+1].x,y:convexPoints[i+2].y-convexPoints[i+1].y} 7 var angle=getAngle(vector1,vector2); 8 if(Math.abs(angle)<Math.PI/90){//誤差范圍設置為2度 9 deletePointIndexs.push(i+1); 10 }; 11 } 12 if(deletePointIndexs.length>0){ 13 for(i=deletePointIndexs.length-1;i>=0;i--){ 14 convexPoints.splice(deletePointIndexs[i],1); 15 } 16 } 17 convexPoints.pop();//將起始點移除
當convexPoints的長度為3時,我們就可判定凸包所圍成的圖形是三角形。
四、通過面積的比較確定所選圖形形狀
當我們所選形狀是如下圖形時,通過上面的凸包判斷出來是三角形(紅色部分),而實際不是。這種情況下我們可以通過比較所選圖形的面積(陰影部分)與凸包所形成的圖形的面積是否相等來最終確定圖形形狀。

在已知一個多邊形各頂點的情況下,可以通過將多邊形分解為多個三角形進行計算,如下圖。三角形面積可通過兩條邊向量的叉積求得;
計算多邊形面積代碼如下
1 //根據圖形各頂點坐標求圖形面積,思路:將多邊形分解為多個三角形,利用叉積計算三角形面積 2 function getPolygonArea(pointData){ 3 var area=0; 4 var startPoint=pointData[0]; 5 for(var i=1;i<pointData.length-1;i++){ 6 var vector1={x:pointData[i].x-startPoint.x,y:pointData[i].y-startPoint.y}; 7 var vector2={x:pointData[i+1].x-pointData[i].x,y:pointData[i+1].y-pointData[i].y}; 8 area+=Math.abs(getCross(vector1,vector2))/2; 9 } 10 return area; 11 }
五、讓程序自動選出所有三角形
我們可以遍歷圖形中小圖形各種可能的排列組合,通過上述方式計算每一種組合拼成的是否是三角形,從而讓程序幫我們判斷圖形共有多少三角形,並生成每一種三角形,具體實現不再贅述。
備注:用到的一些計算幾何方面的工具
1 //得到向量極坐標的角度,p1和p2為向量的起點和終點 2 function getPolarAngle(p1,p2){ 3 return Math.atan2(p2.y-p1.y,p2.x-p1.x); 4 } 5 6 //求兩個向量的叉積 7 function getCross(vector1,vector2){ 8 return vector1.x*vector2.y-vector2.x*vector1.y; 9 } 10 11 //求兩向量的夾角 12 function getAngle(vector1,vector2){ 13 return Math.acos(getDot(vector1,vector2)/(getLengthOfVector(vector1)*getLengthOfVector(vector2))); 14 } 15 16 //求向量的長度 17 function getLengthOfVector(vector){ 18 return Math.sqrt(vector.x*vector.x+vector.y*vector.y); 19 } 20 21 //求兩向量的點積 22 function getDot(vector1,vector2){ 23 return vector1.x*vector2.x+vector1.y*vector2.y; 24 }