数据结构,C#:~O(1)使用范围键查找?

时间:2010-10-11 22:22:27

标签: c# data-structures hash

我有一个数据集。此数据集将提供查找表。给定一个数字,我应该能够查找该数字的相应值。

数据集(比如说它的CSV)有一些警告。而不是:

1,ABC
2,XYZ
3,LMN

数字是范围( - 是“通过”,而不是减号):

1-3,ABC     // 1, 2, and 3 = ABC
4-8,XYZ     // 4, 5, 6, 7, 8 = XYZ
11-11,LMN   // 11 = LMN

所有数字都是签名的。没有范围与其他范围重叠。有一些差距;有一些未在数据集中定义的范围(如上面最后一个片段中的9和10)。 `

我如何在C#中对此数据集进行建模,以便在保持内存占用率较低的同时获得最高性能的查找?

我想出的唯一选择就是过度消耗内存。假设我的数据集是:

1-2,ABC
4-6,XYZ

然后我创建一个Dictionary<int,string>(),其键/值为:

1/ABC
2/ABC
4/XYZ
5/XYZ
6/XYZ

现在我有哈希性能查找,但哈希表中浪费了大量空间。

有什么想法吗?也许只是使用PLINQ而希望获得良好的性能? ;)

5 个答案:

答案 0 :(得分:4)

您可以创建双向间接查找:

Dictionary<int, int> keys;
Dictionary<int, string> values;

然后存储如下数据:

keys.Add(1, 1);
keys.Add(2, 1);
keys.Add(3, 1);
//...
keys.Add(11, 3);

values.Add(1, "ABC");
//...
values.Add(3, "LMN");

然后查看数据:

return values[keys[3]];  //returns "ABC"

我不确定用琐碎的字符串可以节省多少内存,但是一旦超出“ABC”就应该有所帮助。

修改

丹涛在下面的评论之后,我回去检查了他的问题。以下代码:

var abc = "ABC";
var def = "ABC";
Console.WriteLine(ReferenceEquals(abc, def));

将“True”写入控制台。这意味着编译器或运行时(澄清?)是保持对“ABC”的引用,并将其指定为两个变量的值。

Intern ed字符串上阅读更多内容之后,如果您使用字符串文字来填充字典,或者Intern计算字符串,它实际上会占用更多空间来实现我的建议比原来的字典还要多。如果您没有使用Intern ed字符串,那么我的解决方案应占用更少的空间。

最终编辑

如果您正确处理字符串,原始Dictionary<int, string>应该没有多余的内存使用量,因为您可以将它们分配给变量然后将该引用指定为值(或者,如果您需要,因为你可以Intern他们)

只需确保您的作业代码包含一个中间变量作业:

while (thereAreStringsLeftToAssign)
{
    var theString = theStringToAssign;
    foreach (var i in range)
    {
        strings.Add(i, theString);
    }
}

答案 1 :(得分:4)

如果你的词典真正存储了大量的键值,那么将所有可能的范围扩展为显式键的方法将迅速消耗比你可能的更多的内存。

您最好的选择是使用支持二进制搜索(或其他O(log N)查找技术)的一些变体的数据结构。这是一个内部使用OrderedList的link to a generic RangeDictionary for .NET,具有O(log N)性能。

实现恒定时间O(1)查找需要将所有范围扩展为显式键。这需要大量内存,并且当您需要拆分或插入新范围时,实际上会降低性能。这可能不是你想要的。

答案 2 :(得分:1)

作为arootbeer has mentioned in his answer,以下代码不会创建字符串“ABC”的多个实例;相反,它实例化一个实例并将该实例的引用分配给KeyValuePair<int, string>中的每个dictionary

var dictionary = new Dictionary<int, string>();
dictionary[0] = "ABC";
dictionary[1] = "ABC";
dictionary[2] = "ABC";

// etc.

好的,所以在字符串文字的情况下,每个键范围只使用一个string个实例。是否存在不会出现这种情况的情况 - 也就是说,您将为范围内的每个键使用单独的string实例(这是我假设您在谈话时所关注的“过度消耗记忆”)?

老实说,我不这么认为。有些情况下可能会创建多个等效的字符串实例而没有实习的好处,是的。但我无法想象这些情况会影响你在这里做的事情。

我的理由是:您想要将某些值分配给不同的范围键,对吧?因此,每当您定义此类的键范围值配对时,您都有 几个 键< / strong>即可。 部分让我怀疑你将拥有相同字符串的多个实例,除非它被定义为多个范围的值。

为了说明:是的,以下代码将实例化两个相同的字符串:

string x = "ABC";

Console.Write("Type 'ABC' and press Enter: ");
string y = Console.ReadLine();

Console.WriteLine(Equals(x, y));
Console.WriteLine(ReferenceEquals(x, y));

以上程序,假设用户遵循说明并输入“ABC”,输出True,然后False。所以你可能会想,“啊,所以当一个字符串只在运行时提供时,它就不会被实习!所以这可能是我的值可以复制的地方!”

但是......再次:我不这么认为。这一切都回到了这样一个事实,即您将为一系列键分配单个值。所以我们说你的价值来自用户输入;然后你的代码看起来像这样:

var dictionary = new Dictionary<int, string>();

int start, count;
GetRange(out start, out count);
string value = GetValue();

foreach (int key in Enumerable.Range(start, count))
{
    // Look, you're using the same string instance to assign
    // to each key... how could it be otherwise?
    dictionary[key] = value;
}

现在,如果您实际上更多地考虑LBushkin mentions in his answer - 您可能具有巨大范围的行,那么为该范围内的每个键定义KeyValuePair<int, string>是不切实际的(例如, ,如果你的范围是1-1000000) - 那么我同意你最好使用某种基于二进制搜索查找的数据结构。如果那更像你的情景,那就这么说,我很乐意在这方面提供更多的想法。 (或者你可以看看LBushkin已发布的链接。)

答案 3 :(得分:0)

arootbeer有一个很好的解决方案,但你可能会觉得很难解决这个问题。

另一种选择是使用引用类型而不是字符串,以便指向相同的引用

class StringContainer { 
    public string Value { get; set; }
}

Dictionary<int, StringContainer> values;

var value1 = new StringContainer { Value = "ABC" };
values.Add(1, value1);
values.Add(2, value1);

它们都指向StringContainer的相同实例

编辑:感谢大家的评论。此方法处理除string之外的值类型,因此它可能比给定示例更有用。另外,我的理解是字符串并不总是以您期望的参考值的方式运行,但我可能是错的。

答案 4 :(得分:0)

使用平衡有序树(或类似的)将范围开始映射到范围结束和数据。对于不重叠的范围,这很容易实现。