有效地找到最近的字典键
Posted
技术标签:
【中文标题】有效地找到最近的字典键【英文标题】:Efficiently find nearest dictionary key 【发布时间】:2012-09-06 22:20:40 【问题描述】:我在SortedDictionary<DateTime, decimal>
中有一组日期和货币值,对应于在合同定义的复利日期计算到未来的贷款余额。有没有一种有效的方法来找到最接近给定值的日期键? (具体来说,最近的键小于或等于目标)。关键是仅存储值更改时的数据,但有效地回答“x日期的余额是多少?”的问题。范围内的任何日期。
有人问了一个类似的问题 (What .NET dictionary supports a "find nearest key" operation?),当时的答案是“不”,至少在回答的人看来,但那是差不多 3 年前的事了。
问题How to find point between two keys in sorted dictionary 提出了天真地遍历所有键的明显解决方案。我想知道是否存在任何内置的框架函数来利用键已经在内存中索引和排序的事实 - 或者是一个内置的框架集合类,它更适合这种查询。
【问题讨论】:
但是字典的键没有在内存中排序。这就是为什么您不能直接在字典上执行“最接近”或“小于或等于”的原因。 链接的问题提到了SortedList<K, V>
结构。我会同意的。
【参考方案1】:
由于SortedDictionary
是按键排序的,因此您可以创建一个排序的键列表
var keys = new List<DateTime>(dictionary.Keys);
然后高效地对其执行binary search:
var index = keys.BinarySearch(key);
如文档所述,如果index
为正数或零,则密钥存在;如果它是负数,那么~index
是key
存在的索引。因此,“立即更小”的现有键的索引是~index - 1
。确保正确处理 key
小于任何现有键和 ~index - 1 == -1
的边缘情况。
当然,上述方法只有在keys
建立一次然后重复查询时才有意义;因为它涉及迭代整个键序列并在此之上进行二进制搜索,如果您只搜索一次,那么尝试这个是没有意义的。在那种情况下,即使是简单的迭代也会更好。
更新
正如 digEmAll 正确指出的那样,您还可以切换到 SortedList<DateTime, decimal>
,以便 Keys
集合实现 IList<T>
(SortedDictionary.Keys 没有)。该接口提供了足够的功能来手动对其执行二进制搜索,因此您可以采用例如this code 并使其成为IList<T>
的扩展方法。
您还应该记住,如果项目未按已排序的顺序插入,则SortedList
在构造期间的性能比SortedDictionary
差,尽管在这种特殊情况下,日期很可能按时间顺序插入(已排序) 完美的顺序。
【讨论】:
OP也可以切换到SortedList(避免转换成List)然后使用这个方法 --> LINK @digEmAll:这是一个非常好的建议,更重要的是因为该集合很可能是以非常SortedList
友好的方式构建的。我将其合并到答案中。
完美。您是正确的,列表将按顺序填充,但将被多次查询,而不是按顺序查询,因为计算了可能的贷款和合同的不同排列。【参考方案2】:
所以,这并不能直接回答您的问题,因为您专门要求 .NET 框架内置的东西,但面临类似的问题,我发现以下解决方案效果最好,我想发布它此处供其他搜索者使用。
我使用了C5 Collections (GitHub/NuGet) 中的TreeDictionary<K, V>
,这是一个红黑树的实现。
它具有Predecessor
/TryPredecessor
和WeakPredessor
/TryWeakPredecessor
方法(以及类似的后继方法),可以轻松找到离键最近的项目。
我认为,在您的情况下更有用的是 RangeFrom
/RangeTo
/RangeFromTo
方法,它允许您检索键之间的一系列键值对。
请注意,所有这些方法也可以应用于 TreeDictionary<K, V>.Keys
集合,这样您也可以只使用键。
它确实是一个非常简洁的实现,并且应该在 BCL 中包含类似的东西。
【讨论】:
Use => Predecessor , (diff1), (Value) , (diff2), Successor => if diff1 【参考方案3】:如果您需要将查询与插入相交错(除非您的数据已预先排序,或者集合总是很小)。
正如我在您引用的另一个问题中提到的,我创建了三个与 B+ 树相关的数据结构,它们为任何可排序的数据类型提供查找最近键功能:BList<T>
, BDictionary<K,V>
and BMultiMap<K,V>
。这些数据结构中的每一个都提供了FindLowerBound()
和FindUpperBound()
方法,其工作方式类似于C++ 的lower_bound
和upper_bound
。
这些在 Loyc.Collections NuGet 包中可用,BDictionary
通常使用的内存比SortedDictionary
少 44%。
【讨论】:
非常好的实现,我可以扩展 SparseAList 以支持 qword 索引大小吗?如果有long
这个版本就好了。
嗯,似乎所有的接口都是为 32 位设计的——但如果你想在 GitHub 上 fork 并制作一个 64 位版本,请随意。不幸的是,我认为一个版本还不够,因为 64 位索引会增加高达 50% 的内存使用率,而且并非所有客户都希望这样做。更大的问题是 all ALists 派生自 AListBase
,它使用包含 32 位索引的 AListInner
/AListInnerBase
。因此,64 位版本似乎必须消除基类并复制其所有代码。
注意:您可以使用LeMP 来避免物理复制代码(从而复制任何剩余的错误)。相反,您可以使用 using
为索引引入新名称 (using index = int; using uindex = uint
),然后使用 LeMP 对 32 位版本的代码执行查找和替换操作以生成 64 位版本。如果您创建issue,我们可以更多地讨论这种方法的工作原理。注意:LeMP 和 SparseAList 在同一个 repo 中定义。【参考方案4】:
public static DateTime RoundDown(DateTime dateTime)
long remainingTicks = dateTime.Ticks % PeriodLength.Ticks;
return dateTime - new TimeSpan(remainingTicks);
【讨论】:
你应该提供一些关于你的代码在做什么的解释。不要只是发布代码。 我认为这是不言而喻的。使用四舍五入的 DateTimes 作为您的键,并使用相同的方法进行查找。 PeriodLength 可以是一周、一天、一小时、5 分钟或您需要的任何常规长度。如果 PeriodLength 不是应用程序的常量,则更通用的解决方案将接受 periodLength 作为参数。以上是关于有效地找到最近的字典键的主要内容,如果未能解决你的问题,请参考以下文章
获取键及其值并有效地将其从 Dictionary<string,value> 中删除