【问题标题】:What is the fastest way to get all the keys between 2 keys in a SortedList?获取 SortedList 中两个键之间的所有键的最快方法是什么?
【发布时间】:2014-07-11 11:01:57
【问题描述】:

给定一个已填充的SortedList<DateTime, double>。我想获取给定的低和高 DateTime 间隔的所有 (或它们的索引范围,什么应该是封闭的 int 间隔(错过了一些东西?))。

注意:低值和高值不是必需的,实际上在 SortedList 中。


如果有人对如何在没有 SortedList 的情况下执行此操作有更好的了解,这里是我想做的更广泛的范围,我发现 SortedList 可能是合适的:

  • 我想使用 DateTime 键缓存双精度。
  • 对具有给定键的双精度的访问性能优于添加键和删除键的性能
  • 事情是这样的:我必须“使”给定键范围的缓存“无效”(删除键)同样不能保证在缓存中准确找到范围 min 和 max。

【问题讨论】:

  • 所以你的问题基本上是得到一系列键?
  • 你喜欢使用SortedList<DateTime, double>吗?您是否可以使用SortedSet<Tuple<DateTime, double>> 和通过DateTime 进行比较的比较器?然后你可以使用GetViewBetween
  • @Skeet SortedSet 不是问题,只是我希望可以使用像 SortedList 这样的“已排序”的东西来完成简单的任务。恐怕我错过了有关 SortedList 的一些东西(从未使用过)
  • 您还可以对Keys 进行二分搜索,以便在搜索未命中时将“较低”值传回给您。 Keys 支持高效的索引访问,并且不需要复制来实现,所以一切都应该很顺利。

标签: c# .net


【解决方案1】:

列表排序后,您可以使用binary search 来定位区间的端点。最坏情况下的性能将是 O(log n)。

【讨论】:

  • 端点在列表中不是必需的。它不会起作用。
  • 最坏的情况是列表中的所有键都是相等的,这将退化为 O(n),除非二进制搜索经过非常仔细的优化以在对数时间内找到范围的末端。
  • SortedList.Keys 是一个IList<DateTime> 既不是List<DateTime> 也不是DateTime[],那么你如何使用BinarySearch
  • @g.pickardou:它当然可以工作——当二分搜索失败时,它已经有效地找到了搜索目标如果存在的位置。考虑一下,在 C++ 中,equal_range 完全符合您的要求,它是对数的。
  • 顺便说一句,不要将此视为抱怨,但我不敢相信我必须在 2014 中对已排序的事物进行二分搜索,才能得到一个项目是什么=
【解决方案2】:

您可以通过对Keys 运行两次经过调整的二分搜索来解决该问题,以查找限制Keys 集合中感兴趣范围的索引。

由于IList<T> 不提供二进制搜索功能,您需要自己编写。幸运的是,还有从How to perform a binary search on IList 窃取现成实现的选项。

这是一个找到下限的改编版本:

public static int LowerBound<T>(this IList<T> list, T value, IComparer<T> comparer = null)
{
    if (list == null)
        throw new ArgumentNullException("list");

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

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

    while (lower <= upper)
    {
        int middle = lower + (upper - lower) / 2;
        int comparisonResult = comparer.Compare(value, list[middle]);

        // slightly adapted here
        if (comparisonResult <= 0)
            upper = middle - 1;
        else
            lower = middle + 1;
    }

    return lower;
}

要实现UpperBound,只需更改

if (comparisonResult <= 0)

if (comparisonResult < 0)

现在这样做很简单:

var low = set.Keys.LowerBound(value);
var high = set.Keys.UpperBound(value);

// These extra comparisons are required because the adapted binary search
// does not tell us if it actually found the needle. They could be rolled
// into the methods themselves, but this would require another out parameter.
if (set.Keys[low] != value) ++low;
if (set.Keys[high] != value) --high;

if (low <= high) /* remove keys in the range [low, high] */

【讨论】:

    【解决方案3】:

    我想知道为什么 SortedList&lt;TKey, TValue&gt; 在已经按键排序时不提供 BinarySearch。它也使用方法本身(例如IndexOf),但使用的数组是私有字段。所以我试图为此创建一个扩展方法。看看:

    public static class SortedListExtensions
    {
        public static int BinarySearch<TKey, TValue>(this SortedList<TKey, TValue> sortedList, TKey keyToFind, IComparer<TKey> comparer = null)
        {
            TKey[] keyArray = sortedList.GetKeyArray();
            if (comparer == null) comparer = Comparer<TKey>.Default;
            int index = Array.BinarySearch<TKey>(keyArray, keyToFind, comparer);
            return index;
        }
    
        public static IEnumerable<TKey> GetKeyRangeBetween<TKey, TValue>(this SortedList<TKey, TValue> sortedList, TKey low, TKey high, IComparer<TKey> comparer = null)
        {
            int lowIndex = sortedList.BinarySearch(low, comparer);
            if (lowIndex < 0)
            {
                // list doesn't contain the key, find nearest behind
                // If not found, BinarySearch returns the complement of the index
                lowIndex = ~lowIndex;
            }
    
            int highIndex = sortedList.BinarySearch(high, comparer);
            if (highIndex < 0)
            {
                // list doesn't contain the key, find nearest before
                // If not found, BinarySearch returns the complement of the index
                highIndex = ~highIndex - 1;
            }
    
            IList<TKey> keys = sortedList.Keys;
            for (int i = lowIndex; i < highIndex; i++)
            {
                yield return keys[i];
            }
        }
        
        private static TKey[] GetKeyArray<TKey, TValue>(this SortedList<TKey, TValue> sortedList)
        {
            // trying to resolve array with reflection because SortedList.keys is a private array
            Type type = typeof(SortedList<TKey, TValue>);
            FieldInfo keyField = type.GetField("keys", BindingFlags.NonPublic | BindingFlags.Instance);
            if(keyField != null && keyField.GetValue(sortedList) is TKey[] keyArrayFromReflection)
            {
                return keyArrayFromReflection;      
            }
            
            // fallback: fill a new array from the public Keys property, you might want to log this since you should change the reflection implementation
            IList<TKey> keyList = sortedList.Keys;
            TKey[] keyArray = new TKey[keyList.Count];
            for (int i = 0; i < keyArray.Length; i++)
                keyArray[i] = keyList[i];
            return keyArray;
        }
    }
    

    创建示例SortedList

    DateTime start = DateTime.Today.AddDays(-50);
    var sortedList = new SortedList<DateTime, string>();
    for(int i = 0; i < 50; i+=2)
    {
        var dt = start.AddDays(i);
        sortedList.Add(dt, string.Format("Date #{0}: {1}", i, dt.ToShortDateString()));
    }
    
    DateTime low = start.AddDays(1);   // is not in the SortedList which contains only every second day
    DateTime high = start.AddDays(10);
    

    现在您可以使用扩展方法来获取低键和高键之间的键范围:

    IEnumerable<DateTime> dateRange = sortedList.GetKeyRangeBetween(low, high).ToList();
    

    结果:

    04/04/2014
    04/06/2014
    04/08/2014
    04/10/2014
    

    请注意,这是从头开始构建的,并未经过真正的测试。

    【讨论】:

    • 嗨@Tim,在您添加for循环以将内容复制到新数组的那一刻,它变成了O(n)而不是O(log n)进行二进制搜索。有什么办法可以避免这个副本?
    • @Kiran:迟到总比没有好。稍微更改了扩展名以尝试使用反射解决 TKey[] 并仅填充该新数组作为后备。
    猜你喜欢
    • 2018-05-22
    • 2021-04-02
    • 1970-01-01
    • 2011-10-08
    • 2019-11-23
    • 2020-06-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多