2012-11-20 91 views
2

我正在開發一個項目,給定一組座標(經度和緯度)需要找到比給定距離(Km)更近的所有點。計算比給定距離更近的點的數量

我有工作實現遍歷每個點,併爲每個點迭代所有其他點。這是O(n^2)。我想知道我可以採取什麼樣的方法來改善這種情況,考慮到我不想要最近的點,而是比x Km更近的點(我也希望能夠做到相反,找出距離x Km更遠的所有點)。

如果你可以提供一些想法和算法,它會很好。另外代碼示例會很好,特別是在Scala中(我正在開發此項目Scala)。

+5

Quadtrees可以是一個很好的起點。你想要什麼樣的表面?平面,球面還是需要完整的地球模型? – biziclop

+3

它可以是球形的。 除了四叉樹之外,我還想到了kd-trees和R trees。但我發現的所有例子都是爲了找到最近的點。 – petersaints

+2

我認爲基於樹的想法應該適應這個問題。將其分成兩部分:1.找到可能位於目標x內的所有點。 2.檢查每個點的實際距離。 –

回答

1

遲了幾個月回答我自己。那麼我最終使用KD-Tree來實現這一點。我以自己的Java實現爲基礎:http://people.cs.vt.edu/~shaffer/Book/JAVA/progs/KDtree/

但是,我對它進行了一些修改(包括將其移植到Scala),並將其適用於我在Geocaches周圍的特定問題。由此產生的代碼如下:

/* 
* Based on 
* http://people.cs.vt.edu/~shaffer/Book/JAVA/progs/KDtree/ 
* 
* That code is made available as part of this textbook on Data Structures: 
* http://people.cs.vt.edu/~shaffer/Book/ 
*/ 
//A node for a KDTree 
class KDNode[E] { 
    private var key_value : Array[Double] = null //key for this node 
    def key = key_value      
    def key_=(k : Array[Double]) { key_value = k } 

    private var element_value: E = _    //Element for this node 
    def element = element_value 
    def element_=(e : E) {element_value = e} 

    private var left_value : KDNode[E] = null  //Pointer to left child 
    def left = left_value 
    def left_=(l : KDNode[E]) {left_value = l} 

    private var right_value : KDNode[E] = null  //Pointer to right child 
    def right = right_value 
    def right_=(r : KDNode[E]) {right_value = r} 
    /** Constructors */ 
    def this(k : Array[Double], value : E) = 
    { 
    this() 
    key = k 
    element = value 
    } 
    def this(k : Array[Double], value : E, l : KDNode[E], r : KDNode[E]) = 
    { 
    this(k,value) 
    left = l 
    right = r  
    } 
    //Checks if it's a leaf 
    def isLeaf : Boolean = 
    { 
    return (left == null) && (right == null) 
    } 
} 

//Trait for a basic Dictionary that will be used as basis for our KDTree 
trait Dictionary[K,V] { 
    def size: Int 
    def insert(key: K, value: V) 
    def remove(key: K): V 
    def find(key: K): V 
    def clear 
    def isEmpty: Boolean 
} 

/** 
* A extended trait that defines that the key of the dictionary is an array of 
* doubles and defines the need of a method called regionSearchGeo (very 
* important for All Years 5 & 6 stats) 
*/ 
trait GeoDictionary[E] extends Dictionary[Array[Double], E] { 
    def regionSearchGeo(k: Array[Double], radius: Double) : List[E] 
} 

/** 
* Our implementation of a KDTree. It's based on the code provided in the 
* reference above, but had a few key areas modified for geographic searching. 
* For example, this KDtree is a 3D Tree. The keys are Latitude, Longitude and 
* the Year of the cache (that was also included to speed thins even further) 
*/ 
class K3DGeoTree[E] extends GeoDictionary[E]{ 

    private var root : KDNode[E] = null 
    private val D : Int = 3 // Only supporting 2D points in this implementation 
    private var nodecount : Int = 0 // Number of nodes in the KD tree 

    /** 
    * Implementing everything that is required by the Dictionary trait 
    * Some private auxiliary methods are also present 
    */ 
    def clear() = { root = null } 
    def isEmpty() : Boolean = { root == null } 
    def size() : Int = { return nodecount } 

    def insert(pt : Array[Double], value : E) = { 
    root = inserthelp(root, pt, value, 0) 
    nodecount=nodecount+1 
    } 
    def inserthelp(rt : KDNode[E], key : Array[Double], value : E, level : Int) 
           : KDNode[E] = { 
    if (rt == null) return new KDNode[E](key, value) 
    val rtkey : Array[Double] = rt.key 
    if (rtkey(level) > key(level)) 
     rt.left = inserthelp(rt.left, key, value, (level+1)%D) 
    else 
     rt.right = inserthelp(rt.right, key, value, (level+1)%D) 
    return rt 
    } 

    private def findmin(rt : KDNode[E], descrim : Int, level : Int): KDNode[E]= { 
    var temp1 : KDNode[E] = null 
    var temp2 : KDNode[E] = null 
    var key1 : Array[Double] = null 
    var key2 : Array[Double] = null 
    if (rt == null) return null 
    temp1 = findmin(rt.left, descrim, (level+1)%D) 
    if (temp1 != null) key1 = temp1.key 
    if (descrim != level) { 
    temp2 = findmin(rt.right, descrim, (level+1)%D) 
    if (temp2 != null) key2 = temp2.key 
    if ((temp1 == null) || ((temp2 != null) && (key1(descrim) > key2(descrim)))) 
    temp1 = temp2 
    key1 = key2 
    } // Now, temp1 has the smaller value 
    var rtkey : Array[Double] = rt.key 
    if ((temp1 == null) || (key1(descrim) > rtkey(descrim))) 
    return rt 
    else 
    return temp1 
} 

    def find(key : Array[Double]) : E = { return findhelp(root, key, 0) } 

    private def findhelp(rt : KDNode[E], key : Array[Double], level : Int) : E ={ 
    if (rt == null) return null.asInstanceOf[E] 
    val it : E = rt.element 
    val itkey : Array[Double]= rt.key 
    if ((itkey(0) == key(0)) && (itkey(1) == key(1))) 
    return rt.element 
    if (itkey(level) > key(level)) 
    return findhelp(rt.left, key, (level+1)%D) 
    else 
    return findhelp(rt.right, key, (level+1)%D) 
} 

    def remove(key : Array[Double]) : E = { 
    val temp : E = findhelp(root, key, 0) // First find it 
    if (temp != null) { 
      root = removehelp(root, key, 0) // Now remove it 
      nodecount=nodecount-1 
    } 
    return temp 
    } 

    private def removehelp(rt : KDNode[E], key : Array[Double], level : Int) 
              : KDNode[E] = { 
     if (rt == null) return null 
     val rtkey : Array[Double] = rt.key 
     if (key(level) < rtkey(level)) 
     rt.left = removehelp(rt.left, key, (level+1)%D) 
     else if (key(level) > rtkey(level)) 
     rt.right = removehelp(rt.right, key, (level+1)%D) 
     else { // Found it 
     if (rt.right == null) 
     if (rt.left == null) // Just drop element 
     return null 
     else { // Switch subtree to right 
      rt.right = rt.left 
      rt.left = null 
     } 
     val temp : KDNode[E] = findmin(rt.right, level, (level+1)%D) 
     rt.right = removehelp(rt.right, temp.key, (level+1)%D) 
     rt.element = temp.element 
    } 
    return rt 
    } 

    /** 
    * Implementing the GeoDictionary trait 
    */ 
    def regionSearchGeo(point: Array[Double], radius: Double) : List[E] = 
    { 
    val pointGeo : GeoLocation = GeoLocation.fromDegrees(point(0), point(1)) 
    /** 
    * Calculates a bounding rectangle that contains the circle with the given 
    * radius. This will be explained later in the corresponding class 
    */ 
    val boundingRect = pointGeo.boundingCoordinates(radius) 
    //Return the caches found 
    return rsGeoHelp(root, point, radius, boundingRect, 0) 
    } 
    /** 
    * Auxiliary region search function that does all the heavy work 
    */ 
    private def rsGeoHelp(rt : KDNode[E], point : Array[Double], radius : Double, 
               boundingRect : Tuple2[GeoLocation,GeoLocation], 
               lev : Int): List[E] = { 
    if (rt == null) return Nil 
    val rtkey : Array[Double] = rt.key 
    var found : List[E] = Nil 
//Checks if the current node is in the desired radius (and also the year) 
    if (InCircleGeo(point, radius, rtkey)) 
      found = List(rt.element) 
//First Dimension is latitude 
    if(lev % D == 0){ 
     if (rtkey(lev) >= boundingRect._1.degLat) 
     found = found:::rsGeoHelp(rt.left, point, radius, boundingRect, (lev+1)%D) 
     if (rtkey(lev) <= boundingRect._2.degLat) 
     found = found:::rsGeoHelp(rt.right, point, radius, boundingRect, (lev+1)%D) 
    } 
//Second Dimension is Longitude 
    else if(lev % D == 1){ 
     if (rtkey(lev) >= boundingRect._1.degLon) 
     found = found:::rsGeoHelp(rt.left, point, radius, boundingRect, (lev+1)%D) 
     if (rtkey(lev) <= boundingRect._2.degLon) 
    found = found:::rsGeoHelp(rt.right, point, radius, boundingRect, (lev+1)%D) 
    } 
//Third and last dimension is the year 
    else{ 
    found = found:::rsGeoHelp(rt.left, point, radius, boundingRect, (lev+1)%D) 
     if (rtkey(lev) <= point(lev)) 
     found = found:::rsGeoHelp(rt.right, point, radius, boundingRect, (lev+1)%D) 
    } 
    //Return the found nodes (in our case it will be caches) 
    return found 
    } 
    private def InCircleGeo(point : Array[Double], radius : Double, 
                coord : Array[Double]) : Boolean = { 
    //Creates a GeoLocation object for each point 
    val pointGeo : GeoLocation = GeoLocation.fromDegrees(point(0), point(1)) 
    val coordGeo : GeoLocation = GeoLocation.fromDegrees(coord(0), coord(1)) 
    /** 
    * If the year is smaller than the query point and the distance is within 
    * radius return true. Else it's false. 
    */ 
    return (coord(0) != point(0) && coord(1) != point(1) && coord(2) <= point(2) 
     && pointGeo.distanceTo(coordGeo) < radius) 
    } 
} 

/** 
* This class encapsulates a series of utility methods to deal with geographic 
* coordinates. It was based on the information in the link below that gives 
* a very good insight about how to do math with geographic coordinates and 
* also provides some Java samples that we used as an inspiration for this 
* class. 
* Link: http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates 
*/ 
//Companion object of Class GeoLocation to define static methods and variables 
object GeoLocation { 
    //Min maxs in terms of Latitude and Longitude accross the globe 
    private val MIN_LAT : Double = math.toRadians(-90) // -PI/2 
    private val MAX_LAT : Double = math.toRadians(90) // PI/2 
    private val MIN_LON : Double = math.toRadians(-180) // -PI 
    private val MAX_LON : Double = math.toRadians(180) // PI 
    /** 
    * Earth radius. This value is the most used but there are others that may 
    * give slightly different results. 
    */ 
    private val RADIUS : Double = 6372.8 

    /** 
    * A factory method that creates a GeoLocation object from given latitude and 
    * longitude in degrees 
    */ 
    def fromDegrees(latitude : Double, longitude : Double) : GeoLocation = { 
    val result : GeoLocation = new GeoLocation() 
     result.radLat = math.toRadians(latitude) 
     result.radLon = math.toRadians(longitude) 
     result.degLat = latitude 
     result.degLon = longitude 
     result.checkBounds 
     return result 
    } 

    /** 
    * A factory method that creates a GeoLocation object from given latitude and 
    * longitude in radians 
    */ 
    def fromRadians(latitude : Double, longitude : Double) : GeoLocation = { 
     val result : GeoLocation = new GeoLocation() 
     result.radLat = latitude 
     result.radLon = longitude 
     result.degLat = math.toDegrees(latitude) 
     result.degLon = math.toDegrees(longitude) 
     result.checkBounds 
     return result 
    } 
} 
/** 
* The GeoLocation class itself. The constructor is private use the factory 
* methods above. 
*/ 
class GeoLocation private{ 

    /** 
    * Getters and Setters implemented as properties with syntactic sugar 
    * This properties contain the latitude and longitude in degrees and radians 
    */ 
    private var radLat_value : Double = _ 
    def radLat = radLat_value      
    private def radLat_=(k : Double) { radLat_value = k } 

    private var radLon_value : Double = _ 
    def radLon = radLon_value      
    private def radLon_=(k : Double) { radLon_value = k } 

    private var degLat_value : Double = _ 
    def degLat = degLat_value      
    private def degLat_=(k : Double) { degLat_value = k } 

    private var degLon_value : Double = _ 
    def degLon = degLon_value      
    private def degLon_=(k : Double) { degLon_value = k } 

    /** 
    * Check if the vales are valid considering the MIN and MAX for latitude and 
    * longitude. 
    */ 
    private def checkBounds = { 
     if (radLat < GeoLocation.MIN_LAT || radLat > GeoLocation.MAX_LAT || 
       radLon < GeoLocation.MIN_LON || radLon > GeoLocation.MAX_LON) 
      throw new IllegalArgumentException() 
    } 

    /** 
    * Function to calculate the distance between this GeoLocation and the given 
    * GeoLocation. 
    * 
    * Check the reference above and 
    * http://en.wikipedia.org/wiki/Haversine_formula 
    * for more information. 
    */ 
    def distanceTo(location : GeoLocation) : Double = { 
     return math.acos(math.sin(radLat) * math.sin(location.radLat) + 
        math.cos(radLat) * math.cos(location.radLat) * 
        math.cos(radLon - location.radLon)) * GeoLocation.RADIUS 
    } 

    /** 
    * This method is very important for the search made in the K3DTree. 
    * It allows us to make a bouding rectangle with the given distance/radius 
    * that is geometrically correct. Check the reference above to learn more 
    * about the math involved. 
    */ 
    def boundingCoordinates(distance : Double) 
     : Tuple2[GeoLocation, GeoLocation] = { 
    if (distance < 0d) throw new IllegalArgumentException() 
     // Angular distance in radians on a great circle 
     val radDist : Double = distance/GeoLocation.RADIUS 

     //Initialize local variables to check for poles 
     var minLat : Double = radLat - radDist 
     var maxLat : Double = radLat + radDist 

     var minLon : Double = 0 
     var maxLon : Double = 0 

     //Normal case 
     if (minLat > GeoLocation.MIN_LAT && maxLat < GeoLocation.MAX_LAT) { 
       val deltaLon : Double = math.asin(math.sin(radDist)/math.cos(radLat)) 
         minLon = radLon - deltaLon 
       if (minLon < GeoLocation.MIN_LON) minLon += 2d * math.Pi 
         maxLon = radLon + deltaLon 
       if (maxLon > GeoLocation.MAX_LON) maxLon -= 2d * math.Pi 
     } 
    //Special case in which a pole is within the distance 
     else{ 
         minLat = math.max(minLat, GeoLocation.MIN_LAT) 
         maxLat = math.min(maxLat, GeoLocation.MAX_LAT) 
         minLon = GeoLocation.MIN_LON 
         maxLon = GeoLocation.MAX_LON 
       }  
    /** 
    * Each of the bounding points (one in the south-west, bottom-left, 
    * and other in the north-east, top-right) 
    */ 
    val swPoint : GeoLocation = GeoLocation.fromRadians(minLat, minLon) 
    val nePoint : GeoLocation = GeoLocation.fromRadians(maxLat, maxLon) 
    //Return the tuple with the two points 
    return (swPoint, nePoint) 
    } 
} 

整個代碼被記錄,所以我希望這可以幫助有類似問題的人。在這個特定的問題中,除了緯度和經度之外,我還要處理幾年,所以我增加了一個額外的維度。但是對於一個更普遍的地理問題,只用兩個維度(一個用於緯度,一個用於經度)更容易。

3

對於這個問題,我會使用一個java庫(這可能在斯卡拉)。 計算地球上的距離比您想象的要困難得多。例如,地球不是一個完美的球體。 在java中做'geo stuff'的主要庫是JTS:http://tsusiatsoftware.net/jts/main.html 你也可以看一下Geotools或甚至像PostGIS(建立在Postgresql上)的GIS數據庫,但這可能對你的項目來說過於矯枉過正