【问题标题】:LINQ into SortedListLINQ 到 SortedList
【发布时间】:2010-05-24 14:36:17
【问题描述】:

我是一个完整的 LINQ 新手,所以我不知道我的 LINQ 是否不适合我需要做的事情,或者我对性能的期望是否太高。

我有一个对象的 SortedList,以 int 为键; SortedList 而不是 SortedDictionary,因为我将使用预先排序的数据填充集合。我的任务是找到确切的密钥,或者如果没有确切的密钥,则找到具有下一个更高值的密钥。如果搜索的列表太高(例如最高键是 100,但搜索 105),返回 null。

// The structure of this class is unimportant.  Just using
// it as an illustration.
public class CX
{
    public int KEY;
    public DateTime DT;
}

static CX getItem(int i, SortedList<int, CX> list)
{
    var items =
    (from kv in list
     where kv.Key >= i
     select kv.Key);

    if (items.Any())
    {
        return list[items.Min()];
    }

    return null;
}

给定一个包含 50,000 条记录的列表,调用 getItem 500 次大约需要一秒半。调用它 50,000 次需要 2 分钟以上。这个性能似乎很差。我的 LINQ 不好吗?我期待太多了吗?我应该滚动自己的二进制搜索功能吗?

【问题讨论】:

  • 标准的List类其实内置了BinarySearch方法,可以使用;请参阅下面的答案。
  • tzaman 建议使用内置的BinarySearch 方法是正确的;只是使用List&lt;T&gt;.BinarySearch 不是可行的方法,因为它需要将您的密钥放在List&lt;T&gt; 中。我已经发布了一个答案,其中包括任何IList&lt;T&gt;(这包括SortedList&lt;TKey, TValue&gt;.Keys 属性)上的扩展方法的代码,直接取自微软的二进制搜索实现。您可能会发现它很有用。

标签: c# linq sortedlist sorteddictionary


【解决方案1】:

首先,您的查询将被评估两次(一次用于Any,一次用于Min)。其次,Min 要求它遍历整个列表,即使它已排序这一事实意味着第一项将是最小值。你应该可以改变这个:

if (items.Any())
{
    return list[items.Min()];
}

到这里:

var default = 
    (from kv in list
     where kv.Key >= i
     select (int?)kv.Key).FirstOrDefault();

if(default != null) return list[default.Value];

return null;

更新

由于您选择的是值类型,FirstOrDefault 不会返回可为空的对象。我已更改您的查询以将所选值转换为int?,从而允许检查结果值是否为null。我会提倡使用ContainsKey,因为如果您的列表包含0 的值,那将返回true。例如,假设您有以下值

0 2 4 6 8

如果你要传入任何小于或等于 8 的值,那么你会得到正确的值。但是,如果您要传入 9,您将得到 0 (default(int)),它 在列表中,但 不是 有效结果。

【讨论】:

  • 哇,是的。这从 2 分钟变为每秒 1/2。
  • 由于 FirstOrDefault 将在这里返回 int 键,我需要将 default.Value 更改为默认值。不过,有几件事:这里的“默认”是什么?我似乎无法在 MSDN 中找到它的含义。因为我不知道,所以我需要在返回 list[default] 之前检查 list.ContainsKey(default);幸运的是,这不会增加执行时间。
  • 默认值是否只是 int 的默认值,0?
  • n/m 关于默认问题。我看到这意味着值类型为 0。
  • @Chris: 有关默认值的更多信息,请查看 c# 中的 default() 关键字。
【解决方案2】:

自己编写二进制搜索可能很困难。

幸运的是,微软已经写了一个非常强大的:Array.BinarySearch&lt;T&gt;This is, in fact, the method that SortedList&lt;TKey, TValue&gt;.IndexOfKey uses internally。唯一的问题是,它需要一个T[] 参数,而不是任何IList&lt;T&gt;(如SortedList&lt;TKey, TValue&gt;.Keys)。

你知道吗?有一个很棒的工具叫做Reflector,可以让你查看.NET 源代码...

检查一下:IList&lt;T&gt; 上的通用 BinarySearch 扩展方法,直接取自 Microsoft 的 Array.BinarySearch&lt;T&gt; 实现的反射代码。

public static int BinarySearch<T>(this IList<T> list, int index, int length, T value, IComparer<T> comparer) {
    if (list == null)
        throw new ArgumentNullException("list");
    else if (index < 0 || length < 0)
        throw new ArgumentOutOfRangeException((index < 0) ? "index" : "length");
    else if (list.Count - index < length)
        throw new ArgumentException();

    int lower = index;
    int upper = (index + length) - 1;

    while (lower <= upper) {
        int adjustedIndex = lower + ((upper - lower) >> 1);
        int comparison = comparer.Compare(list[adjustedIndex], value);
        if (comparison == 0)
            return adjustedIndex;
        else if (comparison < 0)
            lower = adjustedIndex + 1;
        else
            upper = adjustedIndex - 1;
    }

    return ~lower;
}

public static int BinarySearch<T>(this IList<T> list, T value, IComparer<T> comparer) {
    return list.BinarySearch(0, list.Count, value, comparer);
}

public static int BinarySearch<T>(this IList<T> list, T value) where T : IComparable<T> {
    return list.BinarySearch(value, Comparer<T>.Default);
}

这将让您调用 list.Keys.BinarySearch 并获得所需索引的负位补码,以防找不到所需的键(以下内容基本上直接来自 tzaman 的回答):

int index = list.Keys.BinarySearch(i);
if (index < 0)
    index = ~index;
var item = index < list.Count ? list[list.Keys[index]] : null;
return item;

【讨论】:

  • 不错!我认为,这个扩展结合我的答案将是解决这个问题的最终方法。
  • 就是这个。谢谢你,也感谢其他回答的人。
【解决方案3】:

SortedList 上使用 LINQ 不会给您带来排序的好处。

为了获得最佳性能,您应该编写自己的二分搜索。

【讨论】:

  • 我不希望这样,但这正是我所担心的。谢谢!
  • 没有理由不能以尊重排序的方式在列表中使用 LINQ。您只是不应该期望像Min 这样的函数知道可以进行的优化(即,项目保证在列表中按顺序排列,所以Min 只是第一个,Max只是最后一个)。
  • @Adam Robinson:尽管如此,SLaks 似乎仍然是对的; Where 后跟 FirstOrDefault 不会本质上导致对第一个匹配值的线性搜索吗?是的,这比Min(必须枚举)要好;但它仍然没有可能那么快。我不认为 LINQ 扩展特别了解总是排序的集合,例如SortedList.Keys。我错了吗?还是我错过了什么?
  • @SLaks:很想知道:你为什么要取消使用二分搜索的界限?在我看来,这是一个准确的陈述;我错过了什么?
  • @Dan:不,他们不知道数据存储的内部结构(他们只知道它实现了IEnumerable&lt;T&gt;IQueryable&lt;T&gt;),但这并不意味着你无法编写利用列表排序特性的 LINQ 查询(不仅仅是简单的值搜索)。你是对的,它正在执行线性搜索,但这就是列表在内部所做的(这是SortedList,而不是SortedDictionary,它将使用哈希表)。此外,由于 OP 想要找到 该值或下一个最高值(如果它不存在),因此需要进行线性搜索。
【解决方案4】:

好的,只是为了让这个更可见 - 这是亚当罗宾逊答案的更简洁版本:

return list.FirstOrDefault(kv => kv.Key >= i).Value; 

FirstOrDefault 函数具有接受谓词的重载,该谓词选择满足条件的第一个元素 - 您可以使用它直接获取所需的元素,如果不存在则使用 null

【讨论】:

    【解决方案5】:

    为什么不使用List 类中内置的BinarySearch

    var keys = list.Keys.ToList();
    int index = keys.BinarySearch(i);
    if (index < 0)
        index = ~index;
    var item = index < keys.Count ? list[keys[index]] : null;
    return item;
    

    如果搜索目标不在列表中,BinarySearch 返回下一个较高项的按位补码;如果结果是否定的,我们可以使用它通过重新补充结果来直接获得您想要的结果。如果它等于Count,则您的搜索关键字比列表中的任何内容都大。

    这应该比执行 LINQ where 快得多,因为它已经排序... 正如 cmets 所指出的,ToList 调用将强制对整个列表进行评估,因此只有在不更改底层 SortedList 的情况下进行多次搜索并且单独保留 keys 列表时,这才有用。

    【讨论】:

    • 非常有趣。我试试看。
    • 哇,这绝对不会更快。 ToList 将填充一个全新的 List。这使得 O(n) 最小化,除了整个增加的内存成本。
    • 虽然这是一个有趣的想法,但以这种方式将SortedList 转换为List 无论如何都会单独评估底层SortedList 的每个元素。
    • 哦,当然。这就提出了一个问题,为什么SortedList 类本身没有BinarySearch 方法?这似乎很简单......另一方面,如果您在不更改 SortedList 的情况下连续进行多个搜索,则可以单独保留 Keys 列表,这将使这种方法值得。
    【解决方案6】:

    在 PowerCollections 中使用 OrderedDictionary,您可以获得一个枚举器,该枚举器从您要查找的键的位置开始......如果它不存在,您将获得下一个最近的节点,然后可以从 O 中的该节点向前/向后导航(log N) 每次导航调用的时间。

    这样做的好处是您不必编写自己的搜索,甚至不必在 SortedList 之上管理自己的搜索。

    【讨论】:

    猜你喜欢
    • 2011-07-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-18
    • 1970-01-01
    • 2011-07-07
    • 1970-01-01
    相关资源
    最近更新 更多