我有一个Zip
项目列表(ZipCodes
),其中包含每个ZCTA(邮政编码列表区域)的Location
,目前列表中有~33,000个项目
我可以包含我的Zip
类,但我认为唯一需要注意的是它包含一个名为LatLong
的{{1}}项,它包含纬度和经度坐标。 Location
接受Haversine()
个项目并做一些神奇的事情来返回双倍。
我正在尝试将5个最接近的邮政编码(带距离)拉到我提供的邮政编码。这是我目前的解决方案(不介意我手动添加了5个空KVP):
LatLong
但是,我想把它写成尽可能高效(我实际上并不是100%确定应用程序将会是什么),所以(我相信)我想使用LINQ。< / p>
我将我的代码更改为只抓取最近的邮政编码,ReSharper建议我能够使用的LINQ查询。我对LINQ并不十分熟悉,但我能够重组它以适应我想要的任务:
//don't judge me... I'm still working on a better solution here
private static readonly KeyValuePair<Zip, double> init = new KeyValuePair<Zip, double>(null, 9999);
private static readonly List<KeyValuePair<Zip, double>> workstack = new List<KeyValuePair<Zip, double>>
{
init, init, init, init, init
};
private static KeyValuePair<Zip, double>[] FindClosest(Zip myZip)
{
var closestList = workstack.ToArray(); //I said don't judge me :(
//fwiw ^ is actually faster than initializing a new array each cycle
foreach (var zip in ZipCodes.Where(x => x != myZip))
{
//Haversine magic returns distance (double) in km
var dist = Haversine(myZip.Location, zip.Location);
//If everything else is smaller, just skip it
if (closestList.All(x => x.Value < dist)) continue;
closestList = closestList.OrderByDescending(x => x.Value).ToArray();
closestList[0] = new KeyValuePair<Zip, double>(zip, dist);
}
return closestList;
}
然后我使用//the Skip(1) is to skip the first element, which would be the distance between the zipcode and itself
var closest = ZipCodes.Select(x => new KeyValuePair<Zip, double>
(x, Haversine(myZip.Location, x.Location))).OrderBy
(x => x.Value).Skip(1).Take(5).ToArray();
计算两个函数来处理500个Stopwatch
项,并发现使用LINQ方法平均花费 11.25s ,而我原来的foreach方法平均只有 8s (LINQ 慢每500件物品3.25秒)。
同样,我对LINQ知之甚少,但总是让我相信它更快。在这种情况下,我可以看出它为什么不是 - 我正在尝试对33,000个项目的完整列表进行排序。
我如何编写查询以提高效率?或者,一般来说,我如何根据它们与给定项目和列表其余部分的关系,编写一个更有效的查询来从列表中提取指定数量的项目?
答案 0 :(得分:2)
对于你想要做的事情,LINQ可能不是最好的解决方案。但是,我认为使用SortedList而不是数组可以改善你的foreach:
private static SortedList<double, Zip> FindClosest(Zip myZip)
{
var closestZips = new SortedList<double, Zip>();
List<Zip> ZipCodes = new List<Zip>();
foreach (var zip in ZipCodes.Where(x => x != myZip))
{
//Haversine magic returns distance (double) in km
double dist = Haversine(myZip.Location, zip.Location);
//If everything else is smaller, just skip it
if (closestZips.Count < 5)
{
closestZips.Add(dist, zip);
}
else if (dist < closestZips.Keys[4])
{
closestZips.RemoveAt(4);
closestZips.Add(dist, zip);
}
}
return closestZips;
}
意识到有一个错误。得到它修复,但不得不扭转键,价值观。所以现在每个距离都是关键。
LINQ并不适合短路。由于你只需要一小部分,LINQ通常效率很低,因为它必须首先创建整个集合,然后对其进行排序,然后选择你想要的数量。我认为LINQ在这里的主要优点是它简洁易读。而且,我认为,通过更有效的foreach循环,foreach仍然会非常有利。
编辑:进一步优化
您可以尝试使用Parallel库。结果并非100%一致,但在10-30%左右有明显的速度增益。
using System.Threading;
using System.Threading.Tasks;
private static Object thisLock = new Object();
private static SortedList<double, Zip> FindClosest2(Zip myZip)
{
var closestZips = new SortedList<double, Zip>();
Parallel.ForEach(ZipCodes, (zip) =>
{
//Haversine magic returns distance (double) in km
double dist = Haversine(myZip.Location, zip.Location);
if (closestZips.Count() < 6)
{
lock(thisLock)
{
closestZips.Add(dist, zip);
}
}
else if (dist < closestZips.Keys[4])
{
lock(thisLock)
{
closestZips.RemoveAt(4);
closestZips.Add(dist, zip);
}
}
});
return closestZips;
}
这是我使用过的Haversine:
public static class Haversine
{
public static double calculate(double lat1, double lon1, double lat2, double lon2)
{
var R = 6372.8; // In kilometers
var dLat = toRadians(lat2 - lat1);
var dLon = toRadians(lon2 - lon1);
lat1 = toRadians(lat1);
lat2 = toRadians(lat2);
var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + Math.Sin(dLon / 2) * Math.Sin(dLon / 2) * Math.Cos(lat1) * Math.Cos(lat2);
var c = 2 * Math.Asin(Math.Sqrt(a));
return R * 2 * Math.Asin(Math.Sqrt(a));
}
public static double calculate(Coords a, Coords b)
{
return calculate(a.lat, a.lng, b.lat, b.lng);
}
public static double toRadians(double angle)
{
return Math.PI * angle / 180.0;
}
}
答案 1 :(得分:1)
正如您所注意到的,HaverSine()是一项昂贵的功能。
在任何已发布的解决方案中,您都可以用更便宜的东西替换HaverSine()。您无需准确的英里/公里即可找到最近的公里。即使对于大型邮政区域,地球应该足够平坦,以便在坐标上使用简单的毕达哥拉斯距离。因为我们只需要比较你甚至不必采取根。然后你必须将HaverSine()应用到前5名的确切距离。
...
var dist = SimpleDistance(myZip.Location, zip.Location);
...
double SimpleDistance(Zip a, Zip b)
{
double dLat = a.Lat - b.Lat;
double dLon = a.Lon - b.Lon;
dLon = dLon / 2; // Lat Lon use different degrees
return dLon * dLon + dLat * dLat;
}
但邮政区域的集合应该有其他限制搜索的方法,例如逻辑或区域编号系统。
答案 2 :(得分:0)
完全是LINQed版本。它可能会慢一些。
class ZipDist
{
public Zip Zip;
public double Distance;
}
ZipDist[] FindCLosest2(Zip myZip)
{
return (from zip in ZipCodes
where zip != myZip
let dist = Haversine(myZip.Location, zip.Location)
orderby dist ascending
select new ZipDist { Zip = zip, Distance =dist}).Take(5).ToArray();
}
答案 3 :(得分:0)
这应该比你的方法快一点(排序算法效率更高一些)。 (我不能肯定地说,因为我没有你使用的数据结构)
public class FixedSortedArray<T>
where T : new()
{
T[] _array;
IComparer<T> _comparer;
int _unused;
public FixedSortedArray(int size, IComparer<T> cmp = null)
{
_array = new T[size];
_comparer = cmp ?? Comparer<T>.Default;
_unused = size;
}
public bool Add(T item)
{
if (_unused > 0)
{
var pos = _unused-1;
for (int i = _unused; i < _array.Length; ++i)
{
var cmp = _comparer.Compare(item, _array[i]);
if (cmp < 0)
{
_array[i-1] = _array[i];
}
else
{
pos = i-1;
break;
}
}
_array[pos] = item;
--_unused;
}
else
{
var cmp = _comparer.Compare(item, _array[0]);
if (cmp < 0)
{
int pos = 0;
for (int i = 1; i < _array.Length; ++i)
{
cmp = _comparer.Compare(item, _array[i]);
if (cmp < 0)
{
_array[i - 1] = _array[i];
pos = i;
}
else
{
break;
}
}
if (pos >= 0)
_array[pos] = item;
}
}
return true;
}
public T[] GetArray()
{
return _array;
}
}
class ZipDist
{
public Zip Zip;
public double Distance;
}
class ZipDistCOmparer : IComparer<ZipDist>
{
public int Compare(ZipDist lhs, ZipDist rhs)
{
return lhs.Distance.CompareTo(rhs.Distance);
}
}
private static ZipDist[] FindClosest(Zip myZip)
{
var closestList = new FixedSortedArray<ZipDist>(5, new ZipDistCOmparer());
foreach (var zip in ZipCodes.Where(x => x != myZip))
{
//Haversine magic returns distance (double) in km
var dist = Haversine(myZip.Location, zip.Location);
closestList.Add(new ZipDist { Zip = zip, Distance = dist});
}
return closestList.GetArray();
}
// Stuff I needed to add to get it to compile
public class Zip
{
public string Location;
}
static public Zip[] ZipCodes;
static double Haversine(string lhs, string rhs) { return 0.0; }