搜索附近

最近开发中用到位置搜索附近功能,看到网上有流行的Geohash实现,也有KD树实现。

分别写了实现算法进行验证,感觉既复杂又低效(尤其是难以理解)。

于是想能不能更直观高效的算法实现呢。

 

假设地球表面展开是一个正方形 -180~180,-90~90,(可以修正)

           把这个正方向分割成许许多多的小正方型(一个正方形认为是一个点,不考虑立体经纬度重合问题)

          当计算附近的时候,首先计算中心点所在的正方形,再根据距离计算出要找的周围正方形的范围(绿色区域),

                                           然后遍历计算一个正方形区域(蓝色区域),获得距离内的待搜索目标(红色区域内)

               

                                                           图一

           地球周长: 6371*2*PI*1000 (m)

           用30bit存贮: 2^30=1342177279

           计算 6371*2*PI*1000/2^30=0.030(m)

           共需要2^30*2^30=2^60= 1152921504606846975 个正方形

这个原理简单明了,但是有两个致命缺陷:

     1 正方形太多,没有哪台计算机能存的下这么多正方形

     2 确定中心后,周围待查询的小正方形太多,效率太低。

 针对问题1 ,可以通过稀疏矩阵来解决

         

                                                                                              图二

 针对问题2,暂时没有办法。

      

改进一下:

    

    1 30bit分两级存贮,0~14一级,15~29二级     (这里要根据情况需要调整)

    2 用正方形左下角的点表示这个正方形。(由于所有正方形的宽、高都是相同的)

    3 只存贮有对象的正方形,没对象的不存

                                                                                图三

    如上图 一级正方行里面保存二级正方形,二级正方形保存实际的对象(m,n可以根据实际情况选合适的值)。

    1 一级,二级均通过稀疏矩阵存贮。

    2 一级矩阵由于命中概率较高,采用for循环行列(如图一的蓝色区域)

    3 二级矩阵命中率较低,采用稀疏矩阵遍历。

    4 把稀疏矩阵的行列拼接成一个整数(long)。

 

算法实现:

    假定 m=15,n=15;

    计算 6371*2*PI/2^15=1.22(km)(一级表示的精度是1.22km)

            6371*2*PI*1000/2^30=0.030(m) (二级表示的精度是0.030m)

 

把所有待搜索目标计算经纬度,放置到这些正方形里面。(计算方法同Geohash)

          地球纬度区间是[-90,90],中关村大厦的纬度是39.97694,可以通过下面算法对纬度39.97694进行逼近编码:
         1)区间[-90,90]进行二分为[-90,0),[0,90],称为左右区间,可以确定39.97694属于右区间[0,90],给标记为1;
         2)接着将区间[0,90]进行二分为 [0,45),[45,90],可以确定39.97694属于左区间 [0,45),给标记为0;
         3)递归上述过程39.928167总是属于某个区间[a,b]。随着每次迭代区间[a,b]总在缩小,并越来越逼近39.97694;
         4)如果给定的纬度x(39.97694)属于左区间,则记录0,如果属于右区间则记录1,这样随着算法的进行会产生一个序列101110001101101,记作:lat。

           int  lat=getLatCode(39.97694)=775342647;   (101110001101101100101000110111)

         地球经度区间是[-90,90],中关村大厦的经度是116.31,同样的原理计算出编码为:110100101011010,记作:lng

           int  lng=getLngCode(116.31)=883778999;  (110100101011010110010110110111)

       int MAXIMIZE=0x40000000;
       int DATA_BITS=30;

       int DATA_BITS_FIRST=15;

       LONGITUDE_RATIO=MAXIMIZE/360.0;
       double LATITUDE_RATIO=MAXIMIZE/180.0;

       //Geohash 经度编码

      int getLngCode(double lng){

        int result=0;

       double start=-180,end=180,midd;

       for(int i=DATA_BITS;i>0;i--){
            result<<=1;
            midd=(start+end)/2;
            if(lng>midd){
                 result|=1;
                 start=midd;
            }
           else{
               end=midd;
            }
       }
       return result;

        }

       

      //Geohash 纬度编码

      int getLatCode(double lat){

       int result=0;
       double start=-90,end=90,midd;
       for(int i=DATA_BITS-1;i>0;i--){
        result<<=1;
           midd=(start+end)/2;
           if(lat>midd){
                result|=1;
                start=midd;
           }
          else{
              end=midd;
           }
       }
       return result;
        }

分别取经度、纬度的高15位作为行、列构造一级稀疏矩阵,然后将行列合成。

经纬度合成:   把经度高15bit左移16bit  | 纬度高15bit ,记作:key1 (一级正方形左下角的点)

       int key1=(((lng>>15)&0x7fff)<<16)|((lat>>15)&0x7fff);       (一级KEY)

                       把经度左移32bit  | 纬度 ,记作:key 2(二级正方形左下角的点)

      long key2=(lng<<32)|lat;                                                    (二级KEY)

 这样稀疏矩阵就可以通过Hash表来存贮:

  Hashtable<key1,二级稀疏矩阵>

      二级稀疏矩阵:     Hashtable<Object,key2>

 详细算法实现。

import java.util.HashMap;
import java.util.Map;


public class Position {

//2^30
 protected static final int MAXIMIZE=0x40000000;
 protected static final int DATA_BITS=30;
 
 
 public static final int LONGITUDE_MAXIMIZE=MAXIMIZE;
 public static final int LATITUDE_MAXIMIZE=(MAXIMIZE>>1);

 
 protected static final double EARTH_PERIMETER=6378.137*2000*Math.PI;
 protected static final double EARTH_DISTANCE=EARTH_PERIMETER/360;
 
 protected static final double DATA_RATIO=MAXIMIZE/360.0;
 protected static final double EARTH_RATIO=EARTH_PERIMETER/MAXIMIZE;
 

 protected static final int DATA_MASK=0x3FFFFFFF;
 protected static final int HIGH_MASK=0xFFFF;
 protected static final int HIGH_BITS=16;
 protected static final int LOW_BITS=14;

 

 //浮点型经纬度转换为整型
 public static int getLngCode(double pos){
  return (int)(DATA_RATIO*(pos+180));
 }
 //整型经纬度转换浮点型
 public static double getLongitude(int code){
  return code/DATA_RATIO-180;
 }
 //根据浮点纬度计算编码
 public static int getLatCode(double lat){
  return (int)(DATA_RATIO*(lat+90));
 }
 //根据编码计算浮点纬度
 public static double getLatitude(int code){
  return code/DATA_RATIO-90;
 }
 //计算两点间距离(单位:米,勾股定理)
 public static int getDistance(double lng1,double lat1,double lng2,double lat2){
  lng1-=lng2;
  //修正曲面引起的误差 r=R*cos(纬度)
  lng1*=Math.cos((lat1+lat2)*Math.PI/360);
  lat1-=lat2;
  return (int)(Math.sqrt(lng1*lng1+lat1*lat1)*EARTH_DISTANCE);
 }
 //计算两点间距离(单位:米,勾股定理)
 public static int getDistance(int lng1,int lat1,int lng2,int lat2){
  long lg=lng1-lng2;
  //修正曲面引起的误差 r=R*cos(纬度-90)=R*sin(纬度)
  lg*=Math.sin(((lat1+lat2)*Math.PI/LATITUDE_MAXIMIZE/2));
  long lt=lat1-lat2;
  return (int)(Math.sqrt((lg*lg)+((lt*lt)))*EARTH_RATIO);
 }

 private Map<Integer,Map<Object,Long>> table=new HashMap<Integer,Map<Object,Long>>();
 //插入对象
 public void insert(double lng,double lat,Object code){
  insert(getLngCode(lng),getLatCode(lat),code);
 }
 
 public void insert(int lg,int lt,Object code){
  int key=((lt>>LOW_BITS)<<HIGH_BITS)|(lg>>LOW_BITS);
  
  Map<Object,Long> value=table.get(key);
  if(value==null){
   value=new HashMap<Object,Long>();
   table.put(key, value);
  }
  long l=lt;
  //这里value是  ((纬度<<32)|经度)
  value.put(code,((l<<32)|lg));
 }
 //删除对象
 public void remove(double lng,double lat,Object code){
  remove(getLngCode(lng),getLatCode(lat),code);
 }
 public void remove(int lg,int lt,Object code){
  int key=((lt>>LOW_BITS)<<HIGH_BITS)|(lg>>LOW_BITS);
  
  Map<Object,Long> value=table.get(key);
  if(value!=null){
   value.remove(code);
   //不再有任何对象,直接删除一级点
   if(value.size()==0){
    table.remove(key);
   }
  }
 }
 //查询对象
 public Map<Object,Long> search(double lng,double lat,int distance){
  return search(getLngCode(lng),getLatCode(lat),distance);
 }
 
 public Map<Object,Long> search(int cntX,int cntY,int distance){
  int dist=(int)(distance/EARTH_RATIO);
  
  //计算纬度开始,结束值
  int startY=((cntY-dist)>>LOW_BITS),endY=((cntY+dist)>>LOW_BITS);
  
  dist/=Math.sin((cntY*Math.PI/LATITUDE_MAXIMIZE));  
  //计算经度开始,结束值
  int startX=((cntX-dist)>>LOW_BITS),endX=((cntX+dist)>>LOW_BITS);
  
  Map<Object,Long> map=new HashMap<Object,Long>();
  //循环遍历蓝色格子
  for(int lt=startY;lt<=endY;lt++){
   for(int lg=startX;lg<=endX;lg++){
    int key=((lt<<HIGH_BITS)|lg);
    
    Map<Object,Long> value=table.get(key);
    if(value!=null){
     //遍历二级点
     for(Map.Entry<Object, Long> it:value.entrySet()){
      long t=it.getValue();
      
      if(getDistance((int)(t&DATA_MASK),(int)(t>>32),cntX,cntY)<distance){
       map.put(it.getKey(), t);
      }
     }
    }
   }
  }
  return map;
 }
}

注意事项:

   1 一级、二级的划分,根据实际情况合理划分一二级,比如二级的对象太多(一般几十个点比较合适),就应该放大一级的位数,反之缩小一级的位数

 

posted @ 2018-07-31 14:24  clima  阅读(359)  评论(0)    收藏  举报