为啥字典“未排序”?

Posted

技术标签:

【中文标题】为啥字典“未排序”?【英文标题】:Why is a Dictionary "not ordered"?为什么字典“未排序”? 【发布时间】:2011-09-17 02:27:09 【问题描述】:

我已阅读此内容以回答此处的许多问题。但这究竟是什么意思?

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");

上面的代码似乎按预期工作。那么字典以什么方式被认为是无序的呢?上面的代码在什么情况下会失败?

【问题讨论】:

即使一个特定的测试成功,字典的顺序也不能保证,因此一般不能依赖。 @Sorpigal:是的,但是为什么?怎么做? 字典不是无序的,只是不一定是有序的,这不一样。 ***.com/questions/4634223/… 中介绍了原因和方法 @Jon Skeets 的回答很好——但值得注意的是,如果您 需要 订单,您可以改用Ordered Dictionary 【参考方案1】:

嗯,一方面不清楚您希望这是 insertion-order 还是 key-order。例如,如果您这样写,您期望的结果是什么:

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);

您会期待“三”还是“零”?

碰巧,我认为只要您从不删除任何内容,当前的实现就会保留插入顺序 - 但您不能依赖此。这是一个实现细节,将来可能会改变。

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

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 之前使用的空出条目。但这也不能保证。

重新散列(当字典的底层存储需要扩展时)可能会影响事情......各种事情都会发生。

只是不要将其视为有序集合。它不是为此而设计的。即使它现在恰好可以工作,您也依赖于违反课程目的的无证行为。

【讨论】:

@fearofawhackplanet:好的,所以您期待插入订单。在我的第二个示例中,您期望什么? 感谢您的更新。这是一个很好的例子,实际上我猜对了结果顺序,但我明白你的意思。字典 is 已排序,但不能保证 如何 它是排序的。当人们谈论“没有订单”时,我在想象一种情况,比如随后调用 ElementAt 返回不同的结果。 字典通常以最有效的获取值的顺序排序。它们是查找表。看起来在 C# 中插入顺序保持不变,除非字典被修改,但在例如 Python 中,它是按键值的哈希排序的,因此可以进行快速读取。无论如何,乔恩所说的:永远不要相信字典的顺序;它可以在运行、实现和架构之间完全不同。 @Dov:我不同意。假设它是按哈希码排序的,Foo 中没有任何内容覆盖GetHashCode...,那么添加Foo 的新实例的连续运行很容易显示不同的顺序。当然,这取决于您所说的“相同的插入序列”是什么意思——但我没有看到任何东西试图保证顺序“最好是相同的”——我也不想依赖它。 这里有一篇文章描述了如何在不改变内容的情况下改变字典顺序:blogs.msdn.com/b/ericlippert/archive/2011/05/23/…【参考方案2】:

Dictionary&lt;TKey, TValue&gt; 代表Hash Table,在哈希表中没有顺序的概念。

documentation 解释得很好:

出于枚举的目的,每个项目 在字典中被视为 KeyValuePair 结构 表示一个值及其键。这 退回物品的顺序 未定义。

【讨论】:

哈希表针对随机而非顺序访问进行了优化。他们为了更快的访问而牺牲了排序。 +1 考虑它有 undefined order 而不是 unordered 对我来说更有意义。恕我直言,这些语言术语的含义并不完全相同。【参考方案3】:

这里有很多好主意,但很分散,所以我会尝试创建一个更好的答案,即使问题已经得到解答。

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

如果您想要订购,则使用 OrderedDictionary,但代价是查找速度较慢,因此如果您不需要订购,请不要要求。

字典(和 Java 中的 HashMap)使用散列。无论您的表格大小如何,这都是 O(1) 时间。有序字典通常使用某种平衡树,即 O(log2(n)),因此随着数据的增长,访问速度会变慢。比较一下,对于 100 万个元素,大约是 2^20,所以你必须对一棵树进行大约 20 次查找,但对于哈希映射需要 1 次。这要快很多。

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

人们的意思是,如果您将内容添加到字典中,则顺序很复杂,并且在您添加(或可能删除)元素时随时更改。例如,假设哈希表中有 50 万个元素,而您有 40 万个值。当您再添加一个时,您将达到临界阈值,因为它需要大约 20% 的空白空间才能有效,因此它分配了一个更大的表(例如,100 万个条目)并重新散列所有值。现在他们都在与以前不同的位置。

如果您两次构建相同的字典(仔细阅读我的声明,THE SAME),您将获得相同的顺序。但正如乔恩正确所说,不要指望它。太多的东西可以使它不一样,即使是最初分配的大小。

这提出了一个很好的观点。必须调整哈希图的大小真的非常昂贵。这意味着您必须分配一个更大的表,并重新插入每个键值对。因此,非常值得分配 10 倍所需的内存,而不是必须进行一次增长。了解您的 hashmap 大小,并尽可能预先分配足够的空间,这是一个巨大的性能优势。而且,如果您有一个无法调整大小的糟糕实现,那么如果您选择的尺寸太小,那将是一场灾难。

现在 Jon 在我的评论中与我争论的问题是,如果您在两次不同的运行中将对象添加到 Dictionary 中,您将获得两种不同的排序。没错,但这不是字典的错。

当你说:

new Foo();

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

如果你使用值 Foo 作为字典中的键,没有其他信息,他们唯一能做的就是使用对象的地址作为键。

也就是说

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

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

因此,如果您要将它们放入字典中:

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

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

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

即使 f1 和 f2 具有相同的值。这与 Dictionary 的确定性行为无关。

散列是计算机科学中一个很棒的主题,我最喜欢在数据结构中教授。

查看 Cormen 和 Leiserson,了解有关红黑树与散列的高端书籍 这个叫 Bob 的人有一个很棒的关于散列和最佳散列的网站:http://burtleburtle.net/bob

【讨论】:

【参考方案4】:

顺序是不确定的。

来自here

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

也许你的需要OrderedDictionary是必需的。

【讨论】:

顺序肯定是未定义的,但在大多数实现中它可能是确定性的。 顺序当然是确定的。您的意思是,如果插入或删除值,则顺序可以随时更改。这是一个非常不同的事情 如果将相同的值添加到字典中,它们将在字典中产生相同的顺序。那是确定性的。否则,你的哈希表会很糟糕。【参考方案5】:

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

【讨论】:

【参考方案6】:

Dictionary&lt;TKey,TValue&gt; 类是使用数组支持的索引链表实现的。如果没有项目被删除,后备存储将按顺序保存项目。但是,当一个项目被删除时,该空间将在数组展开之前被标记为重复使用。因此,如果例如新字典添加十项,删除第四项,添加新项,枚举字典,新项可能会出现第四而不是第十,但不能保证不同版本的Dictionary会以同样的方式处理事情。

恕我直言,如果 Microsoft 记录一个没有曾经删除任何项目的字典将按原始顺序枚举项目,但一旦删除任何项目,未来的任何更改都会很有帮助字典可以任意排列其中的项目。对于大多数合理的字典实现而言,只要不删除任何项目就坚持这样的保证相对便宜;在删除项目后继续维护保证会更加昂贵。

另外,拥有一个AddOnlyDictionary 可能会有所帮助,这对于单个写入器与任意数量的读取器同时是线程安全的,并保证按顺序保留项目(请注意,如果仅添加项目 -从未删除或以其他方式修改——一个人可以通过注意它目前包含多少项目来拍摄“快照”)。使通用字典线程安全是昂贵的,但添加上述线程安全级别会很便宜。请注意,高效的多写入器多读取器使用不需要使用读取器-写入器锁,而可以简单地通过让写入器锁定而让读取器不费心来处理。

当然,Microsoft 没有实现如上所述的 AddOnlyDictionary,但有趣的是,线程安全的 ConditionalWeakTable 具有仅添加语义,可能是因为 - 如前所述 - 它更容易将并发添加到仅添加集合而不是允许删除的集合。

【讨论】:

【参考方案7】:

Dictionary,不是SortedDictionary,默认按插入顺序排序。奇怪的是,您需要专门声明一个 SortedDictionary 才能拥有一个按字符串顺序排序的字典:

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

【讨论】:

我认为,当你对某人投反对票时,即使你不喜欢那个人,你也会给出解释,这是一种很好且专业的方式。请通过 *** 消息实用程序让我知道您为什么给我反对票。不要只给我投反对票,什么也不说,让我不知道我做错了什么,因为这是一个适合所有人的学习论坛。

以上是关于为啥字典“未排序”?的主要内容,如果未能解决你的问题,请参考以下文章

在模板 django 中打印(未排序的)字典

无法从字典创建 pd.Series | TypeError:“值”未排序

CoreData:从字典数组插入到SQLite数据库中发生未排序 - 因此无法设置外键

为啥这个循环比创建字典的字典理解更快?

当我将一个字典分配给另一个变量时,为啥 Python 会同时更新两个字典? [复制]

为啥字典大小比较被删除了?