如何对 IList<T> 执行二进制搜索?

Posted

技术标签:

【中文标题】如何对 IList<T> 执行二进制搜索?【英文标题】:How to perform a binary search on IList<T>? 【发布时间】:2010-11-01 07:25:48 【问题描述】:

简单的问题 - 给定一个IList&lt;T&gt;,您如何在不自己编写方法且不将数据复制到具有内置二分搜索支持的类型的情况下执行二分搜索。我目前的状态如下。

List&lt;T&gt;.BinarySearch() 不是IList&lt;T&gt; 的成员 List&lt;T&gt; 没有等效的 ArrayList.Adapter() 方法 IList&lt;T&gt; 不继承自 IList,因此无法使用 ArrayList.Adapter()

我倾向于认为使用内置方法是不可能的,但我无法相信 BCL/FCL 中缺少这种基本方法。

如果不可能,谁能给IList&lt;T&gt;提供最短、最快、最聪明或最漂亮的二分搜索实现?

更新

我们都知道在使用二分搜索之前必须对列表进行排序,因此您可以假设它是。但我认为(但没有验证)排序是同样的问题 - 你如何对IList&lt;T&gt; 进行排序?

结论

IList&lt;T&gt; 似乎没有内置二进制搜索。可以使用First()OrderBy() LINQ 方法进行搜索和排序,但它可能会影响性能。自己实现它(作为扩展方法)似乎是你能做的最好的。

【问题讨论】:

您不能对任何旧数据执行二分搜索 - 它必须经过适当排序并且首先没有重复 您可以假设列表已排序。 但是你刚才说我们可以假设它是排序的。因此,您不想对其进行假设,除非它已排序并支持二分查找? 是的,它是一个排序的 IList,我想搜索它。我可以在一两分钟内自己编写一个二分搜索,但我真的很想看到一个内置方法。 这看起来像是一个疏忽,可惜 .NET 让你重新发明***。我们应该向微软报告这个错误。 【参考方案1】:

一段时间以来,我一直在努力解决这个问题。特别是 MSDN 中指定的边缘情况的返回值:http://msdn.microsoft.com/en-us/library/w4e7fxsh.aspx

我现在已经从 .NET 4.0 复制了 ArraySortHelper.InternalBinarySearch() 并出于各种原因制作了自己的风格。

用法:

var numbers = new List<int>()  ... ;
var items = new List<FooInt>()  ... ;

int result1 = numbers.BinarySearchIndexOf(5);
int result2 = items.BinarySearchIndexOfBy(foo => foo.bar, 5);

这应该适用于所有 .NET 类型。到目前为止,我已经尝试过 int、long 和 double。

实施:

public static class BinarySearchUtils

    public static int BinarySearchIndexOf<TItem>(this IList<TItem> list,
        TItem targetValue, IComparer<TItem> comparer = null)
    
        Func<TItem, TItem, int> compareFunc =
            comparer != null ? comparer.Compare :
            (Func<TItem, TItem, int>) Comparer<TItem>.Default.Compare;
        int index = BinarySearchIndexOfBy(list, compareFunc, targetValue);
        return index;
    

    public static int BinarySearchIndexOfBy<TItem, TValue>(this IList<TItem> list,
        Func<TItem, TValue, int> comparer, TValue value)
    
        if (list == null)
            throw new ArgumentNullException("list");

        if (comparer == null)
            throw new ArgumentNullException("comparer");

        if (list.Count == 0)
            return -1;

        // Implementation below copied largely from .NET4
        // ArraySortHelper.InternalBinarySearch()
        int lo = 0;
        int hi = list.Count - 1;
        while (lo <= hi)
        
            int i = lo + ((hi - lo) >> 1);
            int order = comparer(list[i], value);

            if (order == 0)
                return i;
            if (order < 0)
            
                lo = i + 1;
            
            else
            
                hi = i - 1;
            
        

        return ~lo;
    

单元测试:

[TestFixture]
public class BinarySearchUtilsTest

    [Test]
    public void BinarySearchReturnValueByMsdnSpecification()
    
        var numbers = new List<int>()  1, 3 ;

        // Following the MSDN documentation for List<T>.BinarySearch:
        // http://msdn.microsoft.com/en-us/library/w4e7fxsh.aspx

        // The zero-based index of item in the sorted List(Of T), if item is found;
        int index = numbers.BinarySearchIndexOf(1);
        Assert.AreEqual(0, index);

        index = numbers.BinarySearchIndexOf(3);
        Assert.AreEqual(1, index);


        // otherwise, a negative number that is the bitwise complement of the
        // index of the next element that is larger than item
        index = numbers.BinarySearchIndexOf(0);
        Assert.AreEqual(~0, index);

        index = numbers.BinarySearchIndexOf(2);
        Assert.AreEqual(~1, index);


        // or, if there is no larger element, the bitwise complement of Count.
        index = numbers.BinarySearchIndexOf(4);
        Assert.AreEqual(~numbers.Count, index);
    

我只是从我自己的代码中剪掉了这个,所以如果它不能开箱即用,请评论。

希望这可以一劳永逸地解决问题,至少根据 MSDN 规范。

【讨论】:

您似乎错过了Antoine's answer。它自 2010 年以来一直存在,使您的答案变得多余。 而你在他的回答中错过了我的 cmets。我相信我在实现中发现了一个缺陷,至少根据 MSDN 规范。如果我错了,请纠正我。 你是对的。好吧,现在它是多余的,因为 Antoine 在您的 cmets 之后(实际上是在您的回答之后)修复了它。我只是赞成你的回答。但是,您可能会考虑添加一条评论,说明您看似多余的答案为何存在作为它的序言(目前仅针对极端案例的一般性评论,没有提及之前提示您的 Antoine 答案的问题)。【参考方案2】:

这是我的 Lasse 代码版本。我发现能够使用 lambda 表达式来执行搜索很有用。在对象列表中搜索时,它只允许传递用于排序的键。使用 IComparer 的实现是从这个简单派生的。

我也喜欢在找不到匹配项时返回 ~lower。 Array.BinarySearch 可以做到这一点,它可以让您知道应将搜索的项目插入到何处以保持排序。

/// <summary>
/// Performs a binary search on the specified collection.
/// </summary>
/// <typeparam name="TItem">The type of the item.</typeparam>
/// <typeparam name="TSearch">The type of the searched item.</typeparam>
/// <param name="list">The list to be searched.</param>
/// <param name="value">The value to search for.</param>
/// <param name="comparer">The comparer that is used to compare the value
/// with the list items.</param>
/// <returns></returns>
public static int BinarySearch<TItem, TSearch>(this IList<TItem> list,
    TSearch value, Func<TSearch, TItem, int> comparer)

    if (list == null)
    
        throw new ArgumentNullException("list");
    
    if (comparer == null)
    
        throw new ArgumentNullException("comparer");
    

    int lower = 0;
    int upper = list.Count - 1;

    while (lower <= upper)
    
        int middle = lower + (upper - lower) / 2;
        int comparisonResult = comparer(value, list[middle]);
        if (comparisonResult < 0)
        
            upper = middle - 1;
        
        else if (comparisonResult > 0)
        
            lower = middle + 1;
        
        else
        
            return middle;
        
    

    return ~lower;


/// <summary>
/// Performs a binary search on the specified collection.
/// </summary>
/// <typeparam name="TItem">The type of the item.</typeparam>
/// <param name="list">The list to be searched.</param>
/// <param name="value">The value to search for.</param>
/// <returns></returns>
public static int BinarySearch<TItem>(this IList<TItem> list, TItem value)

    return BinarySearch(list, value, Comparer<TItem>.Default);


/// <summary>
/// Performs a binary search on the specified collection.
/// </summary>
/// <typeparam name="TItem">The type of the item.</typeparam>
/// <param name="list">The list to be searched.</param>
/// <param name="value">The value to search for.</param>
/// <param name="comparer">The comparer that is used to compare the value
/// with the list items.</param>
/// <returns></returns>
public static int BinarySearch<TItem>(this IList<TItem> list, TItem value,
    IComparer<TItem> comparer)

    return list.BinarySearch(value, comparer.Compare);

【讨论】:

如果需要,可以使用SixPack library 中的BinarySearchExtensions 类。 Naaw,我只是为你保持它与 BCL 一致而鼓掌。老实说,这是正确的答案。 您确定应该返回~lower 而不是~upper?如果我没记错的话,在下方插入会破坏顺序。 msdn.microsoft.com/en-us/library/w4e7fxsh.aspx 我检查了 .NET 4.0 中的实现,发现它们确实使用了 ~lower。请参阅我的答案以获得有效的实施。 感谢这个可复制粘贴的解决方案。 ~lower(而不是常量 -1)在排序列表上实现许多有趣的功能时至关重要,例如 SortedList&lt;K, V&gt; 和 I have used your code for that 上的 TailMap 和 HeadMap 【参考方案3】:

我怀疑 .NET 中是否存在类似的通用二进制搜索方法,除了某些基类中存在的方法(但显然不在接口中),所以这是我的通用方法。

public static Int32 BinarySearchIndexOf<T>(this IList<T> list, T value, IComparer<T> comparer = null)

    if (list == null)
        throw new ArgumentNullException(nameof(list));

    comparer = comparer ?? Comparer<T>.Default;

    Int32 lower = 0;
    Int32 upper = list.Count - 1;

    while (lower <= upper)
    
        Int32 middle = lower + (upper - lower) / 2;
        Int32 comparisonResult = comparer.Compare(value, list[middle]);
        if (comparisonResult == 0)
            return middle;
        else if (comparisonResult < 0)
            upper = middle - 1;
        else
            lower = middle + 1;
    

    return ~lower;

这当然假设相关列表已经按照比较器将使用的相同规则进行了排序。

【讨论】:

IList 有一个 Count 属性 msdn.microsoft.com/en-us/library/s16t9z9d.aspx IList 本身没有 Count 属性,但它需要实现 ICollection 参数验证不需要调用ReferenceEquals。由于运算符重载是在编译时解决的,== 将忽略参数实际类型的任何重载,并且接口不能覆盖运算符。 迟到的评论,但无论如何。我知道我不必使用 ReferenceEquals,但那些行来自我使用的 sn-ps,所以我倾向于每次都使用 ReferenceEquals,即使我不需要。 通过在未找到项目时返回~lower而不是-1,行为将等同于Array.BinarySearch,另见***.com/a/2948872/167251。【参考方案4】:

您可以使用List&lt;T&gt;.BinarySearch(T item)。如果您想使用自定义比较器,请使用List&lt;T&gt;.BinarySearch(T item, IComparer&lt;T&gt; comparer)。请查看此 MSDN link 了解更多详情。

【讨论】:

【参考方案5】:

如果您可以使用 .NET 3.5,则可以使用构建在 Linq 扩展方法中:

using System.Linq;

IList<string> ls = ...;
var orderedList = ls.OrderBy(x => x).ToList();
orderedList.BinarySearch(...);

但是,这实际上只是 Andrew Hare 解决方案的一种稍微不同的方式,并且只有在您从同一个有序列表中搜索多次时才会真正有用。

【讨论】:

ToList() 会将所有项目复制到一个新的 List 中。然后您只需使用 List.BinarySearch()。因为复制是 O(n),所以不能与二分搜索结合使用。 如果这只是一次性电话,那么是的,这是真的。但是,如果您存储有序列表,并对其进行多次二进制搜索,则 O(n) 是一次性命中。这与您使用***.com/a/967081/24954 获得的性能完全相同,编辑答案以使这一点更清楚。【参考方案6】:

请注意,Antoine 在下面提供的实现中存在一个错误:当搜索大于列表中任何一个的项目时。返回值应该是 ~lower 而不是 ~middle。反编译方法ArraySortHelper.InternalBinarySearch(mscorlib)查看框架实现。

【讨论】:

感谢您发现此问题。二进制搜索绝对很难正确!您应该对答案发表评论,而不是回复问题,这样可以更轻松地找到相关帖子。【参考方案7】:

如果您需要在IList&lt;T&gt;s、Wintellect's Power Collectionshas one(在Algorithms.cs)上进行二分搜索的现成实现:

/// <summary>
/// Searches a sorted list for an item via binary search. The list must be sorted
/// by the natural ordering of the type (it's implementation of IComparable&lt;T&gt;).
/// </summary>
/// <param name="list">The sorted list to search.</param>
/// <param name="item">The item to search for.</param>
/// <param name="index">Returns the first index at which the item can be found. If the return
/// value is zero, indicating that <paramref name="item"/> was not present in the list, then this
/// returns the index at which <paramref name="item"/> could be inserted to maintain the sorted
/// order of the list.</param>
/// <returns>The number of items equal to <paramref name="item"/> that appear in the list.</returns>
public static int BinarySearch<T>(IList<T> list, T item, out int index)
        where T: IComparable<T>

    // ...

【讨论】:

【参考方案8】:

请记住,对于某些列表实现,二分查找可能效率很低。例如,对于链表,如果你正确实现它是 O(n),如果你天真地实现它是 O(n log n)。

【讨论】:

虽然在一般列表的上下文中是一个有用的声明,但我认为你已经被否决了,因为问题是关于 IList&lt;T&gt;,它应该提供 O(1) 的索引访问。至少 .Net 自己的 LinkedList&lt;T&gt; 没有实现 IList&lt;T&gt; - 我相信正是出于这个原因。当然,如果他们愿意,任何人都可以实现他们的 O(n) 版本的 IList&lt;T&gt;.Item[index]【参考方案9】:

我喜欢使用扩展方法的解决方案。但是,需要注意一点。

这实际上是 Jon Bentley 在他的《Programming Pearls》一书中的实现,它受到了一个 20 年左右未被发现的数字溢出错误的影响。如果 IList 中有大量项目,则 (upper+lower) 可能会溢出 Int32。对此的解决方案是使用减法来稍微不同地进行中间计算;中=下+(上-下)/2;

Bentley 还在 Programming Pearls 中警告说,虽然二分搜索算法于 1946 年发布,而第一个正确的实现直到 1962 年才发布。

http://en.wikipedia.org/wiki/Binary_search#Numerical_difficulties

【讨论】:

这正是我想使用内置实现的原因——要正确进行二分搜索非常困难。 +1 这很好!就个人而言,我认为我不会使用二进制搜索来查看大于 2^30 个项目的集合。当然,如果一个程序员的集合真的那么大,并且完全加载到 RAM 中,我觉得他已经没有注意到他要求他的计算机做什么。 真的很疯狂吗?你认为使用超过 4GB 的内存是荒谬的吗?你会在 30 年后告诉我们 4TB 的内存是荒谬的吗? @rocketsarefast:一般来说,将数据结构分为层是很有用的,其中没有特定的层太大。如果数据项中的字节数是项数的数百万倍,则表明细分该项可能会更好。如果项目的数量超过每个项目的大小数百万倍,则最好将项目聚合成更大的块。对于一个总共需要 1TB RAM 来拥有超过 20 亿个项目或每个项目大于两个 gig 的集合,需要 4,000,000:1 的项目/大小比率。【参考方案10】:

请注意,虽然 List 和 IList 没有 BinarySearch 方法,但 SortedList 有。

【讨论】:

List 有一个 BinarySearch() 方法。 而 sortedlist 没有。【参考方案11】:

IList&lt;T&gt; 进行二进制搜索会遇到一些问题,首先,就像你提到的,List&lt;T&gt; 上的BinarySearch 方法不是IList&lt;T&gt; 接口的成员。其次,您无法在搜索之前对列表进行排序(您必须这样做才能使二分搜索起作用)。

我认为最好的办法是创建一个新的List&lt;T&gt;,对其进行排序,然后进行搜索。它并不完美,但如果您有 IList&lt;T&gt;,则您不必有太多选择。

【讨论】:

您可以假设列表已排序。但我认为 sort 确实存在同样的问题 - 如何排序 IList? 复制是绝对没有选择的,因为它是 O(n),所以在此之后二进制搜索将没有多大意义...... ;) 为什么没有办法排序? IList 允许随机访问,所以只要你有能力比较 T 类型的对象,那么你就可以预先对其进行排序。 当然可以排序IList,不过好像也没有内置支持。 获得排序的 IList 的一种常见方法是使用 SortedList。然后你尝试对key进行二分查找,没有办法找到。我猜很多阅读这个帖子的人都会对 SortedList 上缺少二进制搜索感到沮丧。

以上是关于如何对 IList<T> 执行二进制搜索?的主要内容,如果未能解决你的问题,请参考以下文章

如何序列化 IList<T>?

如何转换通用IList IList?

C# 中的数组如何部分实现 IList<T>?

C#利用GridView对IList<T>或List<T>进行插入、删除和修改

将 IList<T> 转换为 BindingList<T>

为啥 `IList<T>` 不从 `IReadOnlyList<T>` 继承?