【问题标题】:What .NET dictionary supports a "find nearest key" operation?什么 .NET 字典支持“查找最近的键”操作?
【发布时间】:2010-12-14 00:55:46
【问题描述】:

我正在将一些 C++ 代码转换为 C#,它调用 std::map::lower_bound(k) 以在映射中查找其键等于或大于 k 的条目。但是,我看不出有任何方法可以用 .NET 的 SortedDictionary 做同样的事情。我怀疑我可以使用 SortedList 实现一种解决方法,但不幸的是,SortedList 太慢(O(n) 无法插入和删除键)。我能做什么?

注意:我找到了一种解决方法,它利用了我的特定场景...具体来说,我的键是从 0 开始的密集整数,所以我使用 List 作为我的字典和列表索引作为键,搜索等于或大于 k 的键只需几次循环迭代即可完成。但是很高兴看到原始问题得到回答。

【问题讨论】:

  • question,但对SortedList<K, V>没有限制。

标签: .net dictionary key lower-bound


【解决方案1】:

我开发了几个支持“查找下一个较高键”和“查找下一个较低键”操作的集合类。

首先我制作了一组Compact Patricia Trie 集合。这些是旨在最小化内存使用的集合/字典;它们对于大量 URL 和某些其他类型的数据特别有效。它们只能部分解决问题,因为只支持某些类型的键,即byte[]string,以及所有原始整数类型 (Int8..UInt64)。此外,字符串排序区分大小写。 NuGet 包:Loyc.Utilities

发布此答案后,我制作了更多可解决此问题的排序数据结构:BList<T>BDictionary<K,V>BMultiMap<K,V>SparseAList<T>。有关详细信息,请参阅我的第二个答案。

【讨论】:

  • 我同意。还有其他更好的解决方案,例如C5 Collections 中的 TreeDictionary<K,V> 是一个红黑树实现,并且已经具有 WeakSuccessor/TryWeakSuccessor 方法。
【解决方案2】:

问题在于字典/哈希表旨在根据输入值到达唯一的内存位置,因此您需要一个数据结构,旨在容纳与您存储的每个值相关的范围,并且同时正确更新每个区间

我认为skip lists(或平衡二叉树)可以帮助您。尽管它们无法在 O(n) 中执行查找,但它们可以以对数方式执行,并且仍然比树快。

我知道这不是一个正确的答案,因为我不能说 .NET BCL 已经包含这样一个类,很遗憾您必须自己实现一个,或者找到一个支持它的第 3 方程序集。不过,The CodeProject here 似乎有一个很好的例子。

【讨论】:

  • SortedDictionary 似乎是用红黑树实现的;太糟糕了,它的所有功能都没有公开。
【解决方案3】:

你可以试试我下面写的代码。它使用二进制搜索,因此假设列表/数组是预先排序的。

public static class ListExtensions
{
    public static int GetAtMostIndex<TItem, TValue>(/*this*/ IList<TItem> list, TValue value, Func<TItem, TValue, int> comparer)
    {
        return GetAtMostIndex(list, value, comparer, 0, list.Count);
    }

    public static int GetAtLeastIndex<TItem, TValue>(/*this*/ IList<TItem> list, TValue value, Func<TItem, TValue, int> comparer)
    {
        return GetAtLeastIndex(list, value, comparer, 0, list.Count);
    }

    public static int GetAtMostIndex<TItem, TValue>(/*this*/ IList<TItem> list, TValue value, Func<TItem, TValue, int> comparer, int index, int count)
    {
        if (count == 0)
        {
            return -1;
        }

        int startIndex = index;
        int endIndex = index + count - 1;
        int middleIndex = 0;
        int compareResult = -1;

        while (startIndex < endIndex)
        {
            middleIndex = (startIndex + endIndex) >> 1; //  / 2
            compareResult = comparer.Invoke(list[middleIndex], value);

            if (compareResult > 0)
            {
                endIndex = middleIndex - 1;
            }
            else if (compareResult < 0)
            {
                startIndex = middleIndex + 1;
            }
            else
            {
                return middleIndex;
            }
        }

        if (startIndex == endIndex)
        {
            compareResult = comparer.Invoke(list[startIndex], value);

            if (compareResult <= 0)
            {
                return startIndex;
            }
            else
            {
                int returnIndex = startIndex - 1;

                if (returnIndex < index)
                {
                    return -1;
                }
                else
                {
                    return returnIndex;
                }
            }
        }
        else
        {
            //todo: test
            return startIndex - 1;
        }
    }

    public static int GetAtLeastIndex<TItem, TValue>(/*this*/ IList<TItem> list, TValue value, Func<TItem, TValue, int> comparer, int index, int count)
    {
        if (count == 0)
        {
            return -1;
        }

        int startIndex = index;
        int endIndex = index + count - 1;
        int middleIndex = 0;
        int compareResult = -1;

        while (startIndex < endIndex)
        {
            middleIndex = (startIndex + endIndex) >> 1; //  / 2
            compareResult = comparer.Invoke(list[middleIndex], value);

            if (compareResult > 0)
            {
                endIndex = middleIndex - 1;
            }
            else if (compareResult < 0)
            {
                startIndex = middleIndex + 1;
            }
            else
            {
                return middleIndex;
            }
        }

        if (startIndex == endIndex)
        {
            compareResult = comparer.Invoke(list[startIndex], value);

            if (compareResult >= 0)
            {
                return startIndex;
            }
            else
            {
                int returnIndex = startIndex + 1;

                if (returnIndex >= index + count)
                {
                    return -1;
                }
                else
                {
                    return returnIndex;
                }
            }
        }
        else
        {
            return endIndex + 1;
        }
    }
}

【讨论】:

  • 感谢您提供这个二分搜索算法,但它不能解决我的问题,因为它需要一个排序数组。在我的场景中(抱歉,问题不清楚),键插入与键查询交错。在每次插入时维护数组的排序顺序(以便可以进行二进制搜索)需要 O(N) 时间。因此,按键排序的数组不会有很好的性能。现在,如果可以提前构建数组,然后进行一系列查询,则只需进行一次排序,这将是有效的。但这不是我的选择。
【解决方案4】:

我创建了几个为任何数据类型提供此功能的数据结构:BList&lt;T&gt;(排序列表)、BDictionary&lt;K,V&gt;(项目按键排序的字典)和BMultiMap&lt;K,V&gt;(包含更多信息的字典)一个值可以与一个键关联)。有关详细信息,请参阅this article。这些数据结构中的每一个都提供了FindLowerBound()FindUpperBound() 方法,它们的工作方式类似于C++ 的lower_boundupper_bound。在内部,这些集合类似于B+ trees,因此它们具有良好的性能和较低的内存使用率;假设 64 位密钥和 64 位值,BDictionary&lt;,&gt; 通常使用的内存比标准 SortedDictionary&lt;,&gt; 少 44%(而后者使用的内存平均比 Dictionary&lt;,&gt; 略少)。

我还做了一个“稀疏”集合,SparseAList&lt;T&gt;,它类似于BDictionary&lt;int,T&gt;,只是你可以在集合的任何地方插入和删除“空白空间”(空白空间不消耗任何内存)。详情请见this article

所有这些集合都在Loyc.Collections NuGet 包中。

【讨论】:

    【解决方案5】:

    找到离K最近的:

    dict.Keys.Where(i => i >= K).OrderBy(i => i).First();
    

    或者更快:

    public int? GetNearestKey(dict, K) 
    {
        int? lowerK = null;
        foreach (int key in dict.Keys)
        {
            if (key == K) 
            {
                lowerK = K;
                break; 
            }
            else if (key >= K && (!lowerK.HasValue || key < lowerK))
            {
                lowerK = key;
            }
        }
        return lowerK;
    }
    

    【讨论】:

    • 呃...现在从 O(n) 上升到 O(n log n)。
    • 我需要在 O(log n) 内完成。理论上 SortedDictionary 能够做到这一点,但我没有看到它的 API。
    【解决方案6】:

    基本框架中没有二叉搜索树集合实现,因此您必须构建一个或找到一个实现。如您所述,SortedList 在搜索方面最接近,但插入/删除速度较慢(由于其底层数组实现)。

    【讨论】:

    • SortedDictionary 是一个二叉搜索树。它的公共 API 只是省略了搜索功能。
    【解决方案7】:

    我认为关于SortedList 复杂性的问题有一个错误。

    SortedListinserting 新项目具有 O(log(n)) 分摊复杂度。如果您事先知道容量,在最坏的情况下可以在 O(Log(n)) 内完成。

    【讨论】:

    • 微软愚蠢地没有在文档中说明大 O 复杂性 (msdn.microsoft.com/en-us/library/…),但它似乎暗示 SortedList 将键和值存储在数组中。如果插入的键是随机的,则排序数组的插入复杂度为 O(N)。
    • 确实如此,在msdn.microsoft.com/en-us/library/… 中它说:“此方法是对未排序数据的 O(n) 操作,其中 n 是 Count。如果新元素是 O(log n)被添加到列表的末尾。如果插入导致调整大小,则操作为 O(n)。"
    • 未排序的数据是正常的。如果您的数据已经排序,那么您首先不需要 SortedList(List&lt;T&gt; 有一个 BinarySearch 方法。)所以您声称“SortedList 具有 O(log(n)) 摊销复杂性”是完全错误的.
    【解决方案8】:

    您可以使用以下扩展方法为SortedSet&lt;T&gt; 执行此操作:

    public static class SortedSetExtensions
    {
        public static bool FindLowerOrEqualThan<T>(this SortedSet<T> set, T value, out T first)
        {
            if(set.Count == 0)
            {
                first = default(T);
                return false;
            }
    
            var minimum = set.Min;
    
            if(set.Comparer.Compare(minimum, value) > 0)
            {
                first = default(T);
                return false;
            }
    
            first = set.GetViewBetween(minimum, value).Max;
            return true;
        }
    
        public static bool FindGreaterOrEqualThan<T>(this SortedSet<T> set, T value, out T first)
        {
            if (set.Count == 0)
            {
                first = default(T);
                return false;
            }
    
            var maximum = set.Max;
    
            if (set.Comparer.Compare(maximum, value) < 0)
            {
                first = default(T);
                return false;
            }
    
            first = set.GetViewBetween(value, maximum).Min;
            return true;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2016-02-20
      • 2015-05-16
      • 2012-09-06
      • 1970-01-01
      • 2011-03-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-23
      相关资源
      最近更新 更多