关于C#:为什么字典”未排序”?

Why is a Dictionary “not ordered”?

我读过这篇文章是为了回答很多问题。但这到底是什么意思?

1
2
3
4
5
6
7
var test = new Dictionary<int, string>();
test.Add(0,"zero");
test.Add(1,"one");
test.Add(2,"two");
test.Add(3,"three");

Assert(test.ElementAt(2).Value =="two");

上述代码似乎按预期工作。那么,什么样的词典被认为是无序的呢?在什么情况下,上述代码会失效?


好吧,首先,还不清楚您希望它是插入顺序还是键顺序。例如,如果您写下:

1
2
3
4
5
6
7
var test = new Dictionary<int, string>();
test.Add(3,"three");
test.Add(2,"two");
test.Add(1,"one");
test.Add(0,"zero");

Console.WriteLine(test.ElementAt(0).Value);

你期望"三"还是"零"?

事实上,我认为当前的实现保留了插入顺序,只要您从不删除任何内容——但是您不能依赖于这一点。这是一个实现细节,将来可能会有所改变。

删除也会影响这一点。例如,您希望这个程序的结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using System.Collections.Generic;

class Test
{
    static void Main()
    {
        var test = new Dictionary<int, string>();
        test.Add(3,"three");
        test.Add(2,"two");
        test.Add(1,"one");
        test.Add(0,"zero");

        test.Remove(2);
        test.Add(5,"five");

        foreach (var pair in test)
        {
            Console.WriteLine(pair.Key);
        }
    }    
}

实际上(在我的盒子上)是3,5,1,0。5的新条目使用了2以前使用的空条目。但这也不能保证。

重新刷新(当字典的底层存储需要扩展时)可能会影响事物…各种各样的事情都有。

只是不要把它当作一个有序的集合。它不是为这个设计的。即使它恰好在现在起作用,你也依赖于无证行为,这违背了课堂的目的。


Dictionary表示哈希表,在哈希表中没有顺序的概念。

文档解释得很好:

For purposes of enumeration, each item
in the dictionary is treated as a
KeyValuePair structure
representing a value and its key. The
order in which the items are returned
is undefined.


这里有很多好的想法,但都是零散的,所以我将尝试创建一个更好的解决方案,尽管问题已经得到了解决。

首先,字典没有保证的顺序,因此您只能使用它快速查找键并找到相应的值,或者枚举所有键-值对,而不关心顺序是什么。

如果你想要订单,你可以使用一个orderedDictionary,但代价是查找速度较慢,所以如果你不需要订单,就不要要求它。

字典(和爪哇中的hash映射)使用散列。这是O(1)次,不考虑您的桌子大小。有序字典通常使用某种平衡树,即O(log2(n)),因此随着数据的增长,访问速度会变慢。要比较,对于100万个元素,这是2^20的顺序,因此您必须按照20个查找树的顺序进行查找,而1个查找哈希图。速度快得多。

哈希是确定性的。不确定性意味着当你第一次散列(5),下一次散列(5)时,你会得到一个不同的地方。那将是完全无用的。

人们想说的是,如果您向字典中添加内容,那么顺序是复杂的,并且在您添加(或可能删除)元素时可能会发生更改。例如,假设散列表中有500k个元素,而您有400k个值。当您再添加一个时,您会达到临界阈值,因为它需要大约20%的空白空间来提高效率,所以它会分配一个更大的表(比如100万个条目)并重新散列所有值。现在他们都在不同的地方比以前。

如果你两次编同一本字典(仔细阅读我的陈述,同样),你会得到同样的顺序。但正如乔恩正确地说的,不要指望它。太多的事情会使它不一样,甚至最初分配的大小。

这提出了一个很好的观点。调整哈希图的大小确实非常昂贵。这意味着您必须分配一个更大的表,并重新插入每个键值对。因此,分配所需内存的10倍是值得的,而不是必须进行单个增长。知道你的hashmap大小,如果可能的话,预先分配足够多,这是一个巨大的性能胜利。如果有一个不好的实现不能调整大小,那么如果选择太小的大小,它可能是一个灾难。

现在乔恩在我的回答中和我争论的是,如果你在两次不同的运行中向字典添加对象,你会得到两个不同的顺序。是的,但那不是字典的错。

当你说:

1
new Foo();

您正在内存中的新位置创建新对象。

如果在没有其他信息的情况下,使用值foo作为字典中的键,那么它们唯一能做的就是使用对象的地址作为键。

那意味着

1
2
var f1 = new Foo(1);
var f2 = new Foo(1);

F1和F2不是同一个对象,即使它们具有相同的值。

所以如果你把它们放进字典里:

1
2
var test = new Dictionary<Foo, string>();
test.Add(f1,"zero");

不要期望它与以下内容相同:

1
2
var test = new Dictionary<Foo, string>();
test.Add(f2,"zero");

即使f1和f2的值相同。这与字典的确定性行为无关。

哈希是计算机科学中一个很棒的话题,我最喜欢教数据结构。

看看科尔曼和雷瑟森关于红黑树和哈辛的高端书籍。这个叫Bob的人有一个关于哈希和优化哈希的好网站:http://burtleburtle.net/bob


顺序是不确定的。

从这里

为了进行枚举,字典中的每个项都被视为表示值及其键的keyValuePair结构。返回项目的顺序未定义。

也许是为了满足你的需要,命令的是必需的。


dictionary,not sortedDictionary,默认为按插入顺序排序。奇怪的是,您需要专门声明一个sortedDictionary,以便有一个按关键字字符串顺序排序的字典:

1
public SortedDictionary<string, Row> forecastMTX = new SortedDictionary<string, Row>();


Dictionary是使用支持数组的索引链接列表实现的。如果从未移除任何项目,备份存储将按顺序保存项目。但是,当一个项被删除时,在扩展数组之前,空间将被标记为可重用。因此,如果在新字典中添加10个项目,删除第四个项目,添加新项目,并枚举字典,则新项目可能出现第四个而不是第十个,但不能保证不同版本的Dictionary将以相同的方式处理问题。

imho,如果微软记录一个从未从中删除过任何项目的字典将按原始顺序枚举项目,那将是很有帮助的,但是一旦删除了任何项目,以后对字典所做的任何更改都可能任意排列其中的项目。对于大多数合理的字典实现来说,只要不删除任何项就维护这样的保证相对便宜;在删除项之后继续维护该保证会更昂贵。

或者,有一个AddOnlyDictionary可能会很有帮助,它对一个编写器和任何数量的读卡器都是线程安全的,并保证按顺序保留项目(注意,如果只添加项目,而不删除或修改项目,则只需记下它当前包含的项目数,就可以获取"快照"。)做一个通用字典线程安全是昂贵的,但增加以上级别的线程安全将是便宜的。请注意,高效的多编写器多读卡器使用不需要使用读卡器编写器锁,但可以通过编写器锁和让读卡器不费心来处理。

当然,微软没有像上面所描述的那样实现AddOnlyDictionary,但值得注意的是,线程安全的ConditionalWeakTable具有只添加语义,这可能是因为(如前所述)添加并发性只添加集合要比添加允许删除的集合容易得多。


我不知道C或任何.NET,但字典的一般概念是它是键-值对的集合。您不能像迭代列表或数组那样按顺序访问字典。您可以通过拥有一个键来访问,然后查找字典上该键是否有值以及它是什么。在您的示例中,您发布了一个带有数字键的字典,这些数字键恰好是连续的,没有间隙,并且按照插入的升序排列。但无论按何种顺序插入键"2"的值,在查询键"2"时始终会得到相同的值。我不知道C是否允许,我想是的,使用除数字以外的密钥类型,但在这种情况下,它是一样的,密钥上没有明确的顺序。与现实生活中的字典进行类比可能会令人困惑,因为单词的键是按字母顺序排列的,所以我们可以更快地找到它们,但如果没有,字典无论如何都能工作,因为单词"aardvark"的定义将具有相同的含义,即使它出现在"zebra"之后。另一方面,想一部小说,改变书页的顺序没有任何意义,因为它们本质上是有序的集合。