基于功能结果的项目查询列表

时间:2017-01-06 23:24:49

标签: c# linq

我有一个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个项目的完整列表进行排序。

我如何编写查询以提高效率?或者,一般来说,我如何根据它们与给定项目和列表其余部分的关系,编写一个更有效的查询来从列表中提取指定数量的项目?

4 个答案:

答案 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; }