您当前的位置:首页 > 电脑百科 > 程序开发 > 算法

GeoHash 算法

时间:2021-08-30 11:53:41  来源:微信公众号  作者:听枫逐日

对于很多初学者来说,“附近的人”或者类似功能,在技术实现上还有点摸不着头脑。本文将简要的为你讲解“附近的人”的基本理论原理,并以redis的GEO系列地理位置操作指令为例,理论联系实际地为你讲解它们是如何被高效实现的。

 

阅读提示:本文适合有一定Redis使用经验和经纬度知识的服务器后端开发人员阅读。

 

经纬度常识

 

  • 经线是纵的,经度是横的,用于表示不同的经线,纬线是横的,纬度是纵的,用于表示不同的纬线,如下图

 

GeoHash 算法

 


GeoHash 算法

 

  • 纬线:地球仪上的横线,lat,赤道是最大的纬线,从赤道开始分为北纬和南纬,都是0-90°,纬线是角度数值,并不是米;
  • 经线:地球仪上的竖线,lng,子午线为0°,分为西经和东经,都是0-180°,经线也是角度数值;
  • 经纬线和米的换算:经度或者纬度0.00001度,约等于1米,这个在GPS测算距离的时候可以体会到,GPS只要精确到小数点后五位,就是10米范围内的精度
  • 经度0度的位置为本初子午线,在180度的位置转为西经,数字由大到小依次经过北美洲到达西欧.纬度0度的位置为赤道
  • 为了便于理解,将地球看成一个基于经纬度线的坐标系。纬线就是平行于赤道平面的那些平面的周线,经线就是连接南北两极的大圆线的半圆弧。纬度分为北纬(正),南纬(负),赤道所在的纬度值为0。经度以本初子午线界(本初子午线经度为0),分为东经(正),西经(负)。故纬度范围可表示为[-90o, 0o),(0o, 90o],经度范围可表示为[-180o, 0o),(0o, 180o]

 

什么是geohash

 


 

  • GeoHash将二维的经纬度转换成字符串,比如下图展示了北京9个区域的GeoHash字符串,分别是WX4ER,WX4G2、WX4G3等等,每一个字符串代表了某一矩形区域。也就是说,这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串,这样既可以保护隐私(只表示大概区域位置而不是具体的点),又比较容易做缓存。
GeoHash 算法

 

 

GeoHash 算法

 

  • 不同的编码长度,表示不同的范围区间,字符串越长,表示的范围越精确
  • 字符串相似的表示距离相近(特殊情况后文阐述),这样可以利用字符串的前缀匹配来查询附近的POI信息。如下两个图所示,一个在城区,一个在郊区,城区的GeoHash字符串之间比较相似,郊区的字符串之间也比较相似,而城区和郊区的GeoHash字符串相似程度要低些

GeoHash 算法

 

GeoHash 算法

 

  • 总结:GeoHash就是一种将经纬度转换成字符串的方法,并且使得在大部分情况下,字符串前缀匹配越多的距离越近

 

GeoHash 算法

 


 

以经纬度值:(116.389550, 39.928167)进行算法说明,对纬度39.928167进行逼近编码 (地球纬度区间是[-90,90])

 

  1. 区间[-90,90]进行二分为[-90,0),[0,90],称为左右区间,可以确定39.928167属于右区间[0,90],给标记为1
  2. 接着将区间[0,90]进行二分为 [0,45),[45,90],可以确定39.928167属于左区间 [0,45),给标记为0
  3. 递归上述过程39.928167总是属于某个区间[a,b]。随着每次迭代区间[a,b]总在缩小,并越来越逼近39.928167
  4. 如果给定的纬度x(39.928167)属于左区间,则记录0,如果属于右区间则记录1,序列的长度跟给定的区间划分次数有关,如下图

GeoHash 算法

 

GeoHash 算法

 

 

  • 同理,地球经度区间是[-180,180],可以对经度116.389550进行编码
  • 通过上述计算,纬度产生的编码为1 1 0 1 0 0 1 0 1 1 0 0 0 1 0,经度产生的编码为1 0 1 1 1 0 0 0 1 1 0 0 0 1 1
  • 合并:偶数位放经度,奇数位放纬度,把2串编码组合生成新串如下图:
  • 首先将11100 11101 00100 01111 0000 01101转成十进制,对应着28、29、4、15,0,13 十进制对应的base32编码就是wx4g0e,如下图

GeoHash 算法

 

GeoHash 算法

 

  • Ø同理,将编码转换成经纬度的解码算法与之相反

 

GeoHash 原理

 


 

  • Geohash其实就是将整个地图或者某个分割所得的区域进行一次划分,由于采用的是base32编码方式,即Geohash中的每一个字母或者数字(如wx4g0e中的w)都是由5bits组成(2^5 = 32,base32),这5bits可以有32中不同的组合(0~31),这样我们可以将整个地图区域分为32个区域,通过00000 ~ 11111来标识这32个区域。第一次对地图划分后的情况如下图所示(每个区域中的编号对应于该区域所对应的编码):

GeoHash 算法

 

​​

  • Geohash的0、1串序列是经度0、1序列和纬度0、1序列中的数字交替进行排列的,偶数位对应的序列为经度序列,奇数位对应的序列为纬度序列,在进行第一次划分时,Geohash0、1序列中的前5个bits(11100),那么这5bits中有3bits是表示经度,2bits表示纬度,所以第一次划分时,是将经度划分成8个区段(2^3 = 8),将纬度划分为4个区段(2^2 = 4),这样就形成了32个区域。如下图

GeoHash 算法

 

  • 同理,可以按照第一次划分所采用的方式对第一次划分所得的32个区域各自再次划分.

 

对照

 

GeoHash 算法

 

GeoHash 算法

 


GeoHash 算法

 


GeoHash 算法

 

附近的人,附近的加油站如何实现

 

它需要做以下两件事情:

 

1)在使用“附近的人”功能前提交自己的地理位置;

2)根据“我”的地理位置,计算出别人跟我的距离;

3)将第2步中计算出的距离由近及远,进行排序。

 

具体在产品技术上的实现原理和技术难点

 

1)现在移动端(IOSAndroid等),通过系统的API很容易抓到用户当前的位置(即经纬度数据);

2)根据第1步中的经纬度数据,很容易计算出两个点之间的距离

3)对第2步中的计算结果排序就更简单了。

 

技术难点

 

1)如何高效地进行两点距离的计算

2)如何高效地进行地理围栏的圈定

 public class GeoHash {
public static final double MINLAT = -90;
public static final double MAXLAT = 90;
public static final double MINLNG = -180;
public static final double MAXLNG = 180;

private static int numbits = 3 * 5; //经纬度单独编码长度

private static double minLat;
private static double minLng;

private final static char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
        '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p',
        'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };

//定义编码映射关系
final static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();
//初始化编码映射内容
static {
    int i = 0;
    for (char c : digits)
        lookup.put(c, i++);
}

public GeoHash(){
    setMinLatLng();
}

public String encode(double lat, double lon) {
    BitSet latbits = getBits(lat, -90, 90);
    BitSet lonbits = getBits(lon, -180, 180);
    StringBuilder buffer = new StringBuilder();
    for (int i = 0; i < numbits; i++) {
        buffer.Append( (lonbits.get(i))?'1':'0');
        buffer.append( (latbits.get(i))?'1':'0');
    }
    String code = base32(Long.parseLong(buffer.toString(), 2));
    //Log.i("okunu", "encode  lat = " + lat + "  lng = " + lon + "  code = " + code);
    return code;
}

public ArrayList<String> getArroundGeoHash(double lat, double lon){
    //Log.i("okunu", "getArroundGeoHash  lat = " + lat + "  lng = " + lon);
    ArrayList<String> list = new ArrayList<>();
    double uplat = lat + minLat;
    double downLat = lat - minLat;

    double leftlng = lon - minLng;
    double rightLng = lon + minLng;

    String leftUp = encode(uplat, leftlng);
    list.add(leftUp);

    String leftMid = encode(lat, leftlng);
    list.add(leftMid);

    String leftDown = encode(downLat, leftlng);
    list.add(leftDown);

    String midUp = encode(uplat, lon);
    list.add(midUp);

    String midMid = encode(lat, lon);
    list.add(midMid);

    String midDown = encode(downLat, lon);
    list.add(midDown);

    String rightUp = encode(uplat, rightLng);
    list.add(rightUp);

    String rightMid = encode(lat, rightLng);
    list.add(rightMid);

    String rightDown = encode(downLat, rightLng);
    list.add(rightDown);

    //Log.i("okunu", "getArroundGeoHash list = " + list.toString());
    return list;
}

//根据经纬度和范围,获取对应的二进制
private BitSet getBits(double lat, double floor, double ceiling) {
    BitSet buffer = new BitSet(numbits);
    for (int i = 0; i < numbits; i++) {
        double mid = (floor + ceiling) / 2;
        if (lat >= mid) {
            buffer.set(i);
            floor = mid;
        } else {
            ceiling = mid;
        }
    }
    return buffer;
}

//将经纬度合并后的二进制进行指定的32位编码
private String base32(long i) {
    char[] buf = new char[65];
    int charPos = 64;
    boolean negative = (i < 0);
    if (!negative){
        i = -i;
    }
    while (i <= -32) {
        buf[charPos--] = digits[(int) (-(i % 32))];
        i /= 32;
    }
    buf[charPos] = digits[(int) (-i)];
    if (negative){
        buf[--charPos] = '-';
    }
    return new String(buf, charPos, (65 - charPos));
}

private void setMinLatLng() {
    minLat = MAXLAT - MINLAT;
    for (int i = 0; i < numbits; i++) {
        minLat /= 2.0;
    }
    minLng = MAXLNG - MINLNG;
    for (int i = 0; i < numbits; i++) {
        minLng /= 2.0;
    }
}

//根据二进制和范围解码
private double decode(BitSet bs, double floor, double ceiling) {
    double mid = 0;
    for (int i=0; i<bs.length(); i++) {
        mid = (floor + ceiling) / 2;
        if (bs.get(i))
            floor = mid;
        else
            ceiling = mid;
    }
    return mid;
}

//对编码后的字符串解码
public double[] decode(String geohash) {
    StringBuilder buffer = new StringBuilder();
    for (char c : geohash.toCharArray()) {
        int i = lookup.get(c) + 32;
        buffer.append( Integer.toString(i, 2).substring(1) );
    }

    BitSet lonset = new BitSet();
    BitSet latset = new BitSet();

    //偶数位,经度
    int j =0;
    for (int i=0; i< numbits*2;i+=2) {
        boolean isSet = false;
        if ( i < buffer.length() )
            isSet = buffer.charAt(i) == '1';
        lonset.set(j++, isSet);
    }

    //奇数位,纬度
    j=0;
    for (int i=1; i< numbits*2;i+=2) {
        boolean isSet = false;
        if ( i < buffer.length() )
            isSet = buffer.charAt(i) == '1';
        latset.set(j++, isSet);
    }

    double lon = decode(lonset, -180, 180);
    double lat = decode(latset, -90, 90);

    return new double[] {lat, lon};
}

public static void main(String[] args)  throws Exception{
    GeoHash geohash = new GeoHash();
//        String s = geohash.encode(40.222012, 116.248283);
//        System.out.println(s);
    geohash.getArroundGeoHash(40.222012, 116.248283);
//        double[] geo = geohash.decode(s);
//        System.out.println(geo[0]+" "+geo[1]);
}
}
xxxxxxxxxxbr  public class GeoHash {brpublic static final double MINLAT = -90;brpublic static final double MAXLAT = 90;brpublic static final double MINLNG = -180;brpublic static final double MAXLNG = 180;brbrprivate static int numbits = 3 * 5; //经纬度单独编码长度brbrprivate static double minLat;brprivate static double minLng;brbrprivate final static char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8',br        '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p',br        'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };brbr//定义编码映射关系brfinal static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();br//初始化编码映射内容brstatic {br    int i = 0;br    for (char c : digits)br        lookup.put(c, i++);br}brbrpublic GeoHash(){br    setMinLatLng();br}brbrpublic String encode(double lat, double lon) {br    BitSet latbits = getBits(lat, -90, 90);br    BitSet lonbits = getBits(lon, -180, 180);br    StringBuilder buffer = new StringBuilder();br    for (int i = 0; i < numbits; i++) {br        buffer.append( (lonbits.get(i))?'1':'0');br        buffer.append( (latbits.get(i))?'1':'0');br    }br    String code = base32(Long.parseLong(buffer.toString(), 2));br    //Log.i("okunu", "encode  lat = " + lat + "  lng = " + lon + "  code = " + code);br    return code;br}brbrpublic ArrayList<String> getArroundGeoHash(double lat, double lon){br    //Log.i("okunu", "getArroundGeoHash  lat = " + lat + "  lng = " + lon);br    ArrayList<String> list = new ArrayList<>();br    double uplat = lat + minLat;br    double downLat = lat - minLat;brbr    double leftlng = lon - minLng;br    double rightLng = lon + minLng;brbr    String leftUp = encode(uplat, leftlng);br    list.add(leftUp);brbr    String leftMid = encode(lat, leftlng);br    list.add(leftMid);brbr    String leftDown = encode(downLat, leftlng);br    list.add(leftDown);brbr    String midUp = encode(uplat, lon);br    list.add(midUp);brbr    String midMid = encode(lat, lon);br    list.add(midMid);brbr    String midDown = encode(downLat, lon);br    list.add(midDown);brbr    String rightUp = encode(uplat, rightLng);br    list.add(rightUp);brbr    String rightMid = encode(lat, rightLng);br    list.add(rightMid);brbr    String rightDown = encode(downLat, rightLng);br    list.add(rightDown);brbr    //Log.i("okunu", "getArroundGeoHash list = " + list.toString());br    return list;br}brbr//根据经纬度和范围,获取对应的二进制brprivate BitSet getBits(double lat, double floor, double ceiling) {br    BitSet buffer = new BitSet(numbits);br    for (int i = 0; i < numbits; i++) {br        double mid = (floor + ceiling) / 2;br        if (lat >= mid) {br            buffer.set(i);br            floor = mid;br        } else {br            ceiling = mid;br        }br    }br    return buffer;br}brbr//将经纬度合并后的二进制进行指定的32位编码brprivate String base32(long i) {br    char[] buf = new char[65];br    int charPos = 64;br    boolean negative = (i < 0);br    if (!negative){br        i = -i;br    }br    while (i <= -32) {br        buf[charPos--] = digits[(int) (-(i % 32))];br        i /= 32;br    }br    buf[charPos] = digits[(int) (-i)];br    if (negative){br        buf[--charPos] = '-';br    }br    return new String(buf, charPos, (65 - charPos));br}brbrprivate void setMinLatLng() {br    minLat = MAXLAT - MINLAT;br    for (int i = 0; i < numbits; i++) {br        minLat /= 2.0;br    }br    minLng = MAXLNG - MINLNG;br    for (int i = 0; i < numbits; i++) {br        minLng /= 2.0;br    }br}brbr//根据二进制和范围解码brprivate double decode(BitSet bs, double floor, double ceiling) {br    double mid = 0;br    for (int i=0; i<bs.length(); i++) {br        mid = (floor + ceiling) / 2;br        if (bs.get(i))br            floor = mid;br        elsebr            ceiling = mid;br    }br    return mid;br}brbr//对编码后的字符串解码brpublic double[] decode(String geohash) {br    StringBuilder buffer = new StringBuilder();br    for (char c : geohash.toCharArray()) {br        int i = lookup.get(c) + 32;br        buffer.append( Integer.toString(i, 2).substring(1) );br    }brbr    BitSet lonset = new BitSet();br    BitSet latset = new BitSet();brbr    //偶数位,经度br    int j =0;br    for (int i=0; i< numbits*2;i+=2) {br        boolean isSet = false;br        if ( i < buffer.length() )br            isSet = buffer.charAt(i) == '1';br        lonset.set(j++, isSet);br    }brbr    //奇数位,纬度br    j=0;br    for (int i=1; i< numbits*2;i+=2) {br        boolean isSet = false;br        if ( i < buffer.length() )br            isSet = buffer.charAt(i) == '1';br        latset.set(j++, isSet);br    }brbr    double lon = decode(lonset, -180, 180);br    double lat = decode(latset, -90, 90);brbr    return new double[] {lat, lon};br}brbrpublic static void main(String[] args)  throws Exception{br    GeoHash geohash = new GeoHash();br//        String s = geohash.encode(40.222012, 116.248283);br//        System.out.println(s);br    geohash.getArroundGeoHash(40.222012, 116.248283);br//        double[] geo = geohash.decode(s);br//        System.out.println(geo[0]+" "+geo[1]);br}b核心算法获取任意两点距离

核心算法获取任意两点距离

/**

 * 计算地球上任意两点(经纬度)距离    

 *     

 * @param long1 第一点经度    

 * @param lat1 第一点纬度    

 * @param long2 第二点经度    

 * @param lat2 第二点纬度    

 * @return 返回距离 单位:米

 */

public static double Distance(double long1, double lat1, double long2, double lat2)

{

    double a, b, R;

    R = 6378137; // 地球半径        

    lat1 = lat1 * Math.PI / 180.0;

    lat2 = lat2 * Math.PI / 180.0;

    a = lat1 - lat2;

    b = (long1 - long2) * Math.PI / 180.0;

    double d;

    double sa2, sb2;

    sa2 = Math.sin(a / 2.0);

    sb2 = Math.sin(b / 2.0);

    d = 2* R * Math.asin(Math.sqrt(sa2 * sa2 + Math.cos(lat1) * Math.cos(lat2) * sb2 * sb2));

    return d;

}
xxxxxxxxxxbr /**brbr * 计算地球上任意两点(经纬度)距离    brbr *     brbr * @param long1 第一点经度    brbr * @param lat1 第一点纬度    brbr * @param long2 第二点经度    brbr * @param lat2 第二点纬度    brbr * @return 返回距离 单位:米brbr */brbrpublic static double Distance(double long1, double lat1, double long2, double lat2)brbr{brbr    double a, b, R;brbr    R = 6378137; // 地球半径        brbr    lat1 = lat1 * Math.PI / 180.0;brbr    lat2 = lat2 * Math.PI / 180.0;brbr    a = lat1 - lat2;brbr    b = (long1 - long2) * Math.PI / 180.0;brbr    double d;brbr    double sa2, sb2;brbr    sa2 = Math.sin(a / 2.0);brbr    sb2 = Math.sin(b / 2.0);brbr    d = 2* R * Math.asin(Math.sqrt(sa2 * sa2 + Math.cos(lat1) * Math.cos(lat2) * sb2 * sb2));brbr    return d;brbr}

 

关注下我的公众号吧,只为开源精神



Tags:GeoHash 算法   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
对于很多初学者来说,“附近的人”或者类似功能,在技术实现上还有点摸不着头脑。本文将简要的为你讲解“附近的人”的基本理论原理,并以Redis的GEO系列地理位置操作指令为例,理论...【详细内容】
2021-08-30  Tags: GeoHash 算法  点击:(60)  评论:(0)  加入收藏
▌简易百科推荐
前言Kafka 中有很多延时操作,比如对于耗时的网络请求(比如 Produce 是等待 ISR 副本复制成功)会被封装成 DelayOperation 进行延迟处理操作,防止阻塞 Kafka请求处理线程。Kafka...【详细内容】
2021-12-27  Java技术那些事    Tags:时间轮   点击:(1)  评论:(0)  加入收藏
博雯 发自 凹非寺量子位 报道 | 公众号 QbitAI在炼丹过程中,为了减少训练所需资源,MLer有时会将大型复杂的大模型“蒸馏”为较小的模型,同时还要保证与压缩前相当的结果。这就...【详细内容】
2021-12-24  量子位    Tags:蒸馏法   点击:(9)  评论:(0)  加入收藏
分稀疏重建和稠密重建两类:稀疏重建:使用RGB相机SLAMOrb-slam,Orb-slam2,orb-slam3:工程地址在: http://webdiis.unizar.es/~raulmur/orbslam/ DSO(Direct Sparse Odometry)因为...【详细内容】
2021-12-23  老师明明可以靠颜值    Tags:算法   点击:(7)  评论:(0)  加入收藏
1. 基本概念希尔排序又叫递减增量排序算法,它是在直接插入排序算法的基础上进行改进而来的,综合来说它的效率肯定是要高于直接插入排序算法的;希尔排序是一种不稳定的排序算法...【详细内容】
2021-12-22  青石野草    Tags:希尔排序   点击:(6)  评论:(0)  加入收藏
ROP是一种技巧,我们对execve函数进行拼凑来进行system /bin/sh。栈迁移的特征是溢出0x10个字符,在本次getshell中,还碰到了如何利用printf函数来进行canary的泄露。ROP+栈迁移...【详细内容】
2021-12-15  星云博创    Tags:栈迁移   点击:(19)  评论:(0)  加入收藏
一、什么是冒泡排序1.1、文字描述冒泡排序是一种简单的排序算法。它重复地走访要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地...【详细内容】
2021-12-15    晓掌柜丶韶华  Tags:排序算法   点击:(16)  评论:(0)  加入收藏
在了解golang的map之前,我们需要了解哈希这个概念。哈希表,又称散列表(Hash table),是根据键(key)而直接访问在内存储存位置的数据结构。也就是说,它通过计算出一个键值的函数,将...【详细内容】
2021-12-07  一棵梧桐木    Tags:哈希表   点击:(13)  评论:(0)  加入收藏
前面文章在谈论分布式唯一ID生成的时候,有提到雪花算法,这一次,我们详细点讲解,只讲它。SnowFlake算法据国家大气研究中心的查尔斯&middot;奈特称,一般的雪花大约由10^19个水分子...【详细内容】
2021-11-17  小心程序猿QAQ    Tags:雪花算法   点击:(24)  评论:(0)  加入收藏
导读:在大数据时代,对复杂数据结构中的各数据项进行有效的排序和查找的能力非常重要,因为很多现代算法都需要用到它。在为数据恰当选择排序和查找策略时,需要根据数据的规模和类型进行判断。尽管不同策略最终得到的结果完...【详细内容】
2021-11-04  华章科技    Tags:排序算法   点击:(37)  评论:(0)  加入收藏
这是我在网上找的资源的一个总结,会先给出一个我看了觉得还行的关于算法的讲解,再配上实现的代码: Original author: Bill_Hoo Original Address: http://blog.sina.com.cn/s/bl...【详细内容】
2021-11-04  有AI野心的电工和码农    Tags: KMP算法   点击:(36)  评论:(0)  加入收藏
相关文章
最新更新
栏目热门
栏目头条