python检查字典中是否有这元素 python查询字典里的值_c#如何取字典表的值


1前言

在微信轻触【发现】->【附近的人】,便可以查看距离自己较近的人,这个神奇的功能让陌生人能成为朋友、饭友、聊友、约友等。但是这个功能的技术实现原理又是什么呢?

最简单的想法是计算自己和每个好友间的距离,然后按距离排序并输出。这种想法对于用户数较少的应用尚可,但对于微信这种有上亿用户数的应用,显然是不可取的。事实上,不管用户数多少,只要点击【附近的人】基本上几秒钟之内,就能把附近的用户筛选出来。这么快的速度是用了什么奇淫巧技呢?本文我们一起来探究下。

2原理

首先需要获取自己和好友的位置信息,这涉及到LBS(Location Based Service,基于位置的服务),就是通过移动终端获取到用户或者物体的经纬度坐标,通过这些位置信息来提供服务。

其次需要对这些位置信息进行网格化处理。把整个地球想象成经纬度构成的网络,每一个网格包含一定的区域,只要这个网格划分足够精细,便可以用来逼近我们所处的位置了。当某个用户都被划分在某个网格中时,通过查询自己所处网格或周边网格内的用户,便可以找到自己附近的人。

所以需要记录下网格的名称,以便于查询用户附近的人。记录网格名称的过程涉及到GeoHash算法。

当网格划分结束(即每个网格都确定了名称),那么接下来便是根据网格名称来查询用户自己的附近的人。本文用到了字典树来实现这个查询功能。

3实现流程

本文主要介绍如何在Python中实现模拟微信《附近的人》这个功能。实现流程如下所示。


python检查字典中是否有这元素 python查询字典里的值_字符串_02

微信《附近的人》模拟实现流程


4 Geohash法

利用GeoHash算法,可以将经纬度数据转化为字符串格式,这样便将二维的数据转化为了一维,存储就方便了,搜索效率也会高很多。GeoHash算法分为编码和组码。

第一步是编码。经纬度的编码通过折半比较法实现,当大于中值时编码为1,下次新的区间为中值到最大值;当小于中值时编码为0,下次新的区间为最小值到中值。这样一直比较下去,直到到达要求的精度,经度和纬度的方法是一样的,只是纬度的原始区间是[-90,90],经度的原始区间[-180,180]。如下表所示对纬度42.61233和经度-5.61234进行编码:


python检查字典中是否有这元素 python查询字典里的值_字符串_03


这样便可将经纬度(42.61233, -5.61234)转换成二进制码(10111 10010, 01111 10000),二进制码的位数即为编码精度。

第二步是组码(或者合并)。将第一步产生的二进制码组合起来产生Base32的字符串。组码的方式是奇数位放经度、偶数位放纬度(也可互换),如下图所示:


python检查字典中是否有这元素 python查询字典里的值_字典树_04


合并后的二进制码为:01101, 11111, 11000, 00100

将二进制码转换成十进制数,得到:13, 31, 24, 4

再按照如下图所示的Base32编码转换关系(用0-9、b-z(去掉a, i, l, o)这32个字母表示),将十进制数转换为Base32的字符串编码,得到ezs4


python检查字典中是否有这元素 python查询字典里的值_字典树_05


本例中编码精度设置为10位,当继续增大精度位数时,可得到更为精确的Base32字符串。

给定如下图所示的8个用户位置信息,


python检查字典中是否有这元素 python查询字典里的值_python检查字典中是否有这元素_06

用户的所有好友的位置信息

可以利用Geohash算法将这些好友的经纬度信息转化为Base32的字符串编码。如下表所示:


python检查字典中是否有这元素 python查询字典里的值_字符串_07


其中,转换后的后4位为_ID,是为了区别同一位置的不同用户。

用Python实现经纬度的编码和组码,代码见附录一。

5 字典树查询法

计算出这些经纬度的字符串编码值,那么如何存放到数据库中,能够快速检索一个用户附近的好友呢?

本节使用字典树来完成字符串编码值的存储与查询。

字典树(也叫Trie树),是一种 N 叉树,也是一种特殊的前缀树结构。通常来说,一个前缀树是用来存储字符串的。前缀树的每一个节点代表一个字符。每一个节点会有多个子节点,通往不同子节点的路径上有着不同的字符。子节点代表的字符是由节点本身的原始字符,以及通往该子节点路径上所有的字符组成的。

前缀树的一个重要的特性是,节点所有的后代都与该节点相关的字符串有着共同的前缀。

将上述各个用户转换后的字符串编码值,存入到字典树中,如下图所示:


python检查字典中是否有这元素 python查询字典里的值_字符串_08


图中,每一条从根节点(root)到叶子结点的路径都表示一个用户的位置信息(经纬度)。

当构建完成字典树后,便可以查询离自己距离较近的用户是哪些。如当自己(ID记为109)的位置信息表示为:(42.61236, -5.61234)时,通过Geohash算法可以计算出字符串编码值为 ‘ezs42m34yzx_109’。

通过字典树查询(深度优先搜索),可以获取到离该用户最近的好友有100和107,他们的信息分别如下:


python检查字典中是否有这元素 python查询字典里的值_python检查字典中是否有这元素_09


用Python实现字典树的插入与查询,代码见附录二。

6 距离计算法

根据2个经纬度点,可以利用Haversine公式计算这2个经纬度点之间的距离,计算公式如下:

其中

其中R为地球半径,可取平均值 6371km;

表示两点的纬度;

表示两点的经度。

可以使用字典dict来存放用户自己到每个附近邻居的距离。每个用户都可以通过ID检索,于是可以把ID作为字典的键,把距离作为字典的值。

然后通过快速排序方法对字典排序(如对上述ID=100和ID=107的用户按距离排序),便可以按照距离筛选出离自己较近的好友了。。。

7 参考

附录一:用Python实现经纬度的编码、组码和解码

class Geohash:
    def __init__(self):
        self._all_ = ['encode', 'decode', 'bbox', 'neighbors']
        _base32 = '0123456789bcdefghjkmnpqrstuvwxyz'
        # 10进制和32进制转换,32进制去掉了ailo
        self._decode_map = {}  # 解码表
        self._encode_map = {}  # 编码表
        for i in range(len(_base32)):
            self._decode_map[_base32[i]] = i  # _decode_map[字符]=数字
            self._encode_map[i] = _base32[i]  # _encode_map[数字]=字符

    # 编码函数
    # 将经纬度 编码 为字符串
    def encode(self, lat, lon, precision=12):
        """
        :param lat: 纬度
        :param lon: 经度
        :param precision: 精度
        :return:
        """
        lat_range, lon_range = [-90.0, 90.0], [-180.0, 180.0]
        geohash = []  # 地理位置哈希表
        code = []
        j = 0
        while len(geohash) < precision:
            # print(code, lat_range, lon_range, geohash)
            j += 1
            lat_mid = sum(lat_range) / 2
            lon_mid = sum(lon_range) / 2
            # 经度 -> 经度和纬度交叉放置
            if lon <= lon_mid:  # 交线位置给左下
                code.append(0)
                lon_range[1] = lon_mid
            else:
                code.append(1)
                lon_range[0] = lon_mid
            # 纬度
            if lat <= lat_mid:  # 交线位置给左下
                code.append(0)
                lat_range[1] = lat_mid
            else:
                code.append(1)
                lat_range[0] = lat_mid
            # encode  编码
            if len(code) >= 5:
                # 每5位2进制数 用1个字符表示
                geohash.append(self._encode_map[int(''.join(map(str, code[:5])), 2)])
                code = code[5:]
        return ''.join(geohash)

    # 解码函数
    # 将字符串 解码 为经纬度
    def decode(self, geohash):
        lat_range, lon_range = [-90.0, 90.0], [-180.0, 180.0]
        is_lon = True
        for letter in geohash:  # 取每个字符
            # 每个字符都对应着5位二进制数, 前面不够的用0补齐
            s = str(bin(self._decode_map[letter]))[2:]
            code = s.rjust(5, '0')  # 不够5位,前面用0补齐
            for bi in code:
                if is_lon and bi == '0':
                    lon_range[1] = sum(lon_range) / 2
                elif is_lon and bi == '1':
                    lon_range[0] = sum(lon_range) / 2
                elif (not is_lon) and bi == '0':
                    lat_range[1] = sum(lat_range) / 2
                elif (not is_lon) and bi == '1':
                    lat_range[0] = sum(lat_range) / 2
                is_lon = not is_lon  # 经度和纬度是交替出现的
        return sum(lat_range) / 2, sum(lon_range) / 2


附录二:用Python实现字典树的插入与查询

class Trie:
    def __init__(self):
        """
        Initialize your data structure here.
        """
        self.root = {}  # 字典
        self.end = -1  # 结束标志

    def insert(self, word):
        """  将单词插入到字典树中
        Inserts a word into the trie.
        :type word: str
        :rtype: void
        """
        curNode = self.root  # 从根结点开始遍历
        for c in word:  # 把每个字符都放入到 字典树 中
            if c not in curNode:  # 字符c不在当前结点中,则创建新分支。
                curNode[c] = {}
            curNode = curNode[c]  # 下一个结点
        curNode[self.end] = True  # 结束结点用True标记

    def startsWith(self, prefix):
        """  按照前缀开始搜索
        Returns if there is any word in the trie that starts with the given prefix.
        :type prefix: str
        :rtype: bool
        """
        curNode = self.root
        for c in prefix:
            if c not in curNode:
                return []
            curNode = curNode[c]
        # print(curNode)
        path = prefix
        paths = []  # 找到所有的相同前缀的路径,返回
        self.find_path_all(curNode, path, paths)
        return paths

    # 从树中找出从起始顶点到终止顶点的所有路径
    def find_path_all(self, root, path, paths):
        """ 利用深度优先搜索
        :param root: 当前顶点
        :param path: 当前路径
        :param paths: 所有路径
        :return:
        """
        if self.end in root:  # 判断是否搜索到末尾了。
            paths.append(path)
            return
        for v in root:
            # 构造下次递归的父路径
            path += v  # 加入顶点
            self.find_path_all(root[v], path, paths)  # 递归
            path = path[:-1]  # 回溯