这是一个挑战问题,而不是我迫切需要的东西,所以不要整天花在这些人身上。
我在2000年左右建立了一个约会网站(早已不复存在),其中一个挑战是计算用户之间的距离,以便我们可以在X英里范围内展示您的“匹配”。为了说明问题,给出以下数据库模式(粗略地):
USER TABLE 用户身份 用户名 邮编
ZIPCODE TABLE 邮政编码 纬度 经度
USER和ZIPCODE加入USER.ZipCode = ZIPCODE.ZipCode。
您会采取什么方法来回答以下问题:在给定用户的邮政编码X英里范围内的其他用户中存在哪些用户。
我们使用2000 census data,其中包含邮政编码表及其近似的纬度和经度。
我们还使用Haversine Formula来计算球体上任意两点之间的距离......非常简单的数学。
至少对我们来说,问题是我们19岁的大学生,真正成为如何有效地计算和/存储所有成员与所有其他成员的距离。一种方法(我们使用的方法)是导入所有数据并计算从每个邮政编码到每个其他邮政编码的距离。然后,您将存储并索引结果。类似的东西:
SELECT User.UserId
FROM ZipCode AS MyZipCode
INNER JOIN ZipDistance ON MyZipCode.ZipCode = ZipDistance.MyZipCode
INNER JOIN ZipCode AS TheirZipCode ON ZipDistance.OtherZipCode = TheirZipCode.ZipCode
INNER JOIN User AS User ON TheirZipCode.ZipCode = User.ZipCode
WHERE ( MyZipCode.ZipCode = 75044 )
AND ( ZipDistance.Distance < 50 )
问题当然是ZipDistance表中会有很多行。它并非完全不可行,但它确实很大。此外,它需要对整个数据集进行完整的预处理,这也不是无法管理,但不一定是可取的。
无论如何,我想知道你们中的一些大师会对这样的事情采取什么方法。此外,我认为这是程序员不时要解决的常见问题,特别是如果您考虑的算法类似的问题。我对一个彻底的解决方案很感兴趣,其中至少包括所有部分的HINTS,以便快速有效地完成此任务。谢谢!
答案 0 :(得分:33)
好的,对于初学者来说,你真的不需要在这里使用Haversine公式。对于距离较短的公式产生较大误差的较大距离,您的用户不关心匹配是加或减几英里,而对于更近的距离,误差非常小。 Geographical Distance维基百科文章中列出了更容易(计算)的公式。
由于邮政编码不是均匀分布的,因此任何将它们均匀分区的过程都会在它们紧密聚集的区域受到严重影响(DC附近的东海岸就是一个很好的例子)。如果您想进行视觉比较,请查看http://benfry.com/zipdecode并将邮政编码前缀89与07进行比较。
处理索引此空间的更好方法是使用Quadtree或R-tree之类的数据结构。此结构允许您对不均匀间隔的数据进行空间和距离搜索。
以下是四叉树的样子:
要搜索它,可以使用其中较小单元格的索引向下钻取每个较大的单元格。维基百科更彻底地解释了它。
当然,由于这是一件相当普遍的事情,其他人已经为你做了很多困难。由于您尚未指定要使用的数据库,因此PostgreSQL扩展PostGIS将作为示例。 PostGIS包括执行R树空间索引的功能,允许您进行有效的空间查询。
导入数据并构建空间索引后,查询距离就像是这样的查询:
SELECT zip
FROM zipcode
WHERE
geom && expand(transform(PointFromText('POINT(-116.768347 33.911404)', 4269),32661), 16093)
AND
distance(
transform(PointFromText('POINT(-116.768347 33.911404)', 4269),32661),
geom) < 16093
我会让你自己完成本教程的其余部分。
以下是一些其他参考资料,以帮助您入门。
答案 1 :(得分:14)
我只是创建一个zip_code_distances表并预先计算美国所有42K邮编码之间的距离,这些邮政编码彼此之间的距离在20-25英里之内。
create table zip_code_distances
(
from_zip_code mediumint not null,
to_zip_code mediumint not null,
distance decimal(6,2) default 0.0,
primary key (from_zip_code, to_zip_code),
key (to_zip_code)
)
engine=innodb;
只包括彼此半径20-25英里范围内的邮政编码,减少了您需要存储在距离表中的行数,从最大值17亿(42K ^ 2) - 42K到更易于管理的400万或如此。
我从网上下载了一个zipcode数据文件,其中包含csv格式的所有美国官方邮政编码的经度和纬度:
"00601","Adjuntas","Adjuntas","Puerto Rico","PR","787","Atlantic", 18.166, -66.7236
"00602","Aguada","Aguada","Puerto Rico","PR","787","Atlantic", 18.383, -67.1866
...
"91210","Glendale","Los Angeles","California","CA","818","Pacific", 34.1419, -118.261
"91214","La Crescenta","Los Angeles","California","CA","818","Pacific", 34.2325, -118.246
"91221","Glendale","Los Angeles","California","CA","818","Pacific", 34.1653, -118.289
...
我写了一个快速而又脏的C#程序来读取文件并计算每个邮政编码之间的距离,但只输出半径25英里范围内的输出邮政编码:
sw = new StreamWriter(path);
foreach (ZipCode fromZip in zips){
foreach (ZipCode toZip in zips)
{
if (toZip.ZipArea == fromZip.ZipArea) continue;
double dist = ZipCode.GetDistance(fromZip, toZip);
if (dist > 25) continue;
string s = string.Format("{0}|{1}|{2}", fromZip.ZipArea, toZip.ZipArea, dist);
sw.WriteLine(s);
}
}
结果输出文件如下所示:
from_zip_code|to_zip_code|distance
...
00601|00606|16.7042215574185
00601|00611|9.70353520976393
00601|00612|21.0815707704904
00601|00613|21.1780461311929
00601|00614|20.101431539283
...
91210|90001|11.6815708119899
91210|90002|13.3915723402714
91210|90003|12.371251171873
91210|90004|5.26634939906721
91210|90005|6.56649623829871
...
然后我会使用加载数据infile将此距离数据加载到我的zip_code_distances表中,然后使用它来限制我的应用程序的搜索空间。
例如,如果您的用户的邮政编码是91210并且他们想要找到距离他们不到10英里的人,那么您现在可以简单地执行以下操作:
select
p.*
from
people p
inner join
(
select
to_zip_code
from
zip_code_distances
where
from_zip_code = 91210 and distance <= 10
) search
on p.zip_code = search.to_zip_code
where
p.gender = 'F'....
希望这有帮助
编辑:将半径扩展到100英里,这使邮政编码距离增加到3250万行。
快速检查邮政编码91210运行时间为0.009秒。
select count(*) from zip_code_distances
count(*)
========
32589820
select
to_zip_code
from
zip_code_distances
where
from_zip_code = 91210 and distance <= 10;
0:00:00.009: Query OK
答案 2 :(得分:5)
您可以通过假设一个方框而不是圆形半径来简化计算。然后在搜索时,您只需计算给定点+“半径”的纬度/经度的下限/上限,只要您在纬度/经度列上有一个索引,就可以很容易地将所有落在盒子内的记录拉回来
答案 3 :(得分:1)
我会使用纬度和经度。例如,如果您的纬度为45且经度为45并且被要求在50英里范围内找到匹配项,那么您可以通过在纬度上移动50/69并在纬度下移动50/69来实现此目的(1度)纬度~69英里)。选择纬度在此范围内的邮政编码。经度有点不同,因为当你靠近两极时它们会变小。
但是在45度,1经度~49英里,所以你可以在纬度上向左移动50/49,在纬度向右移动50/49,并从经度设置的纬度中选择所有邮政编码。这将为您提供长度为一百英里的正方形内的所有邮政编码。如果你想要非常精确,你可以使用你提到的Haversine公式来清除盒子角落的拉链,为你提供一个球体。
答案 4 :(得分:1)
你可以将你的空间划分为大小相等的区域 - 例如,将地球近似为巴基球或二十面体。如果这更容易(例如使它们成圆形),这些区域甚至可以重叠一点。记录每个邮政编码所在的区域。然后,您可以预先计算每个区域对之间可能存在的最大距离,该区域对与计算所有邮政编码对时具有相同的 O(n ^ 2)问题,但对于较小的 n 。
现在,对于任何给定的邮政编码,您可以获得明确在您给定范围内的区域列表,以及跨越边界的区域列表。对于前者,只需获取所有邮政编码。对于后者,深入到每个边界区域并根据各个邮政编码进行计算。
它在数学上肯定更复杂,特别是必须选择区域的数量才能在表的大小与动态计算所花费的时间之间取得良好的平衡,但它会减小预先计算的表的大小很好的利润。
答案 5 :(得分:0)
并非所有可能的邮政编码都会被使用。我会建立zipdistance作为'缓存'表。对于每个请求,计算该对的距离并将其保存在缓存中。当对距离对的请求到来时,首先查看缓存,然后计算它是否不可用。
我不知道距离计算的复杂性,所以我还要检查运行中的计算是否比查找更便宜(也考虑到你需要计算的频率)。
答案 6 :(得分:0)
我的问题是运行得很好,几乎每个人的答案都得到了应用。我从旧的解决方案而不仅仅是“重新开始”考虑这个问题。 Babtek得到了最简单的说明。
我会跳过代码,因为我会提供推导所需公式的参考资料,并且在这里发布的内容太多了。
1)考虑球体上的点A,由纬度和经度表示。 Figure out North, South, East, and West edges of a box 2X miles across with Point A at the center
2)从ZipCode表中选择框内的所有点。这包括一个简单的WHERE子句,其中两个Between语句由Lat和Long限制。
3)使用半正公式确定A点与步骤2中返回的每个B点之间的球面距离。
4)丢弃距离A - >的所有点B. B> X
5)选择ZipCode在剩余的B点集中的用户。
这对于&gt;非常快100英里。计算匹配的最长结果是~0.014秒,运行select语句的结果很简单。
另外,作为旁注,有必要在几个函数中实现数学运算并在SQL中调用它们。一旦我超过一定距离,ZipCodes的匹配数量太大而无法传递回SQL并用作IN语句,因此我必须使用临时表并将生成的ZipCodes连接到ZipCode列上的User。
我怀疑使用ZipDistance表不会提供长期性能提升。行数刚刚变大。如果您计算从每个zip到每个其他邮政编码的距离(最终),则40,000个邮政编码的结果行数将为~1.6B。哇!
或者,我有兴趣使用SQL的内置地理类型来查看是否会使这更容易,但是好的旧int / float类型适用于此示例。
所以......我使用的最终在线资源列表,供您参考:
1)Maximum Difference, Latitude and Longitude。
3)Lengthy but complete discussion of the whole process,我在谷歌的答案中找到了这些东西。
答案 7 :(得分:0)
我知道这篇文章太旧了,但是为客户做了一些研究我发现了Google Maps API的一些有用的功能并且实现起来非常简单,你只需要传递到url的来源和目的地邮政编码,它计算距离流量的距离,您可以使用任何语言:
origins = 90210
destinations = 93030
mode = driving
在链接后面你可以看到它返回一个json。请记住,您需要一个API密钥才能在您自己的托管上使用它。