为啥 python 的 dict 实现为哈希表,而 std::map 是基于树的?

Posted

技术标签:

【中文标题】为啥 python 的 dict 实现为哈希表,而 std::map 是基于树的?【英文标题】:Why is python's dict implemented as hash table whereas std::map is tree-based?为什么 python 的 dict 实现为哈希表,而 std::map 是基于树的? 【发布时间】:2012-01-06 03:03:29 【问题描述】:

为什么一种语言使用树而另一种语言使用哈希表来表示看似相似的数据结构?

c++的map vs python的dict

一个相关的问题是关于哈希表的性能。 请在下面评论我对哈希表的理解。

一棵树保证有 O(log n)。 而哈希表没有保证,除非输入是先前已知的,因为可能发生冲突。 我倾向于认为哈希表的性能会随着问题规模的增大而接近 O(n)。 因为我还没有听说过随着问题大小的增长动态调整其表大小的哈希函数。

因此,哈希表只对特定范围的问题大小有用,这就是为什么大多数数据库使用树而不是哈希表。

【问题讨论】:

和基于大小平衡二叉树的 Haskell Data.Map 通常你使用的哈希值比表的大小大很多倍,所以你可以调整表的大小并使用不同的模数。数据库使用树(也有不同的树)的原因是尽量减少磁盘访问并将大部分实际结构保留在内存中,这样您就可以直接获取数据而无需太多浪费的磁头移动(与 SSD 有争议,但仍然可以减少磁盘访问是一个主要因素)。 @Joey:所以,哈希表在大小很大的情况下很难在内存中操作。另一方面,tree 在这方面自然是合适的。我对你的理解正确吗? 在不平衡的树变成列表的特殊情况下,树可以在 O(n) 上工作。为了使树平衡和工作 O(lon n) 必须花一些时间来平衡它。 【参考方案1】:

新的 C++ 标准具有 std::unordered_map 类型 which is a hash table。 IIRC 他们也希望它进入以前的标准,但是在讨论期间没有足够的时间,所以它被遗漏了。但是,大多数流行的编译器多年来都以一种或另一种方式提供它。

换句话说,不要太担心它。为手头的任务使用正确的数据结构。


至于你对哈希表的理解不准确:

我还没有听说过动态调整其表的哈希函数 随着问题规模的增长而规模扩大

所有重要的哈希表实现都会动态调整自身以适应不断增长的输入,方法是分配更大的数组并重新散列所有键。虽然这个操作很昂贵,但如果设计得当(很少做),性能仍然可以摊销 O(1)。

【讨论】:

【参考方案2】:

Python 哈希表永远不会超过 2/3。随着它们的增长而调整大小(从大小 8 开始,然后大小翻两番直到 50000,然后翻倍)。这给了他们摊销的 O(1) 插入、删除和查找。过度碰撞是可能的,但很少见。

【讨论】:

【参考方案3】:

您对哈希表(以及谁使用它们)的理解存在缺陷。

问题是,哈希表是一个相当模糊的术语。在底层有很多实现......但首先让我们谈谈 BST(二叉搜索树)的使用。


为什么 C++ 使用二叉搜索树?

C++ 是由委员会设计的,有许多可能的哈希表实现导致大不相同的特性,而最流行的 BST(红黑树和 AVL 树)实现具有几乎相同的特性。因此,他们并没有彻底拒绝哈希表,他们只是无法确定要选择的特征和向用户公开的细节。

请参阅 James Kanze 的评论,该提案来得太晚了,James 提出了一个有趣的问题,即为什么 Stepanov 没有首先提出该提案。我仍然怀疑选择的数​​量是罪魁祸首。

为什么数据库使用搜索树?

首先,让我们选择一个数据库软件。我会选择 Oracle,因为它既有广泛的文档记录,又是 SQL 数据库的典型代表。 Oracle 提供两种类型的索引:位图和搜索树。

注意:它们不使用 BINARY Search Trees,而是使用对 IO 和缓存更友好的 B+Trees

哈希表和搜索树之间有一个根本区别:后者是排序的。许多数据库操作都意味着排序:

获取第n个元素 获取前 n 个元素 获取[a,b]中的元素

在所有这些情况下,哈希表都是无用的。

此外,数据库需要处理庞大的数据集(通常),这意味着它们需要组织数据以最小化 IO(磁盘读/写)。在这里,搜索树的排序特性意味着(在索引中)可能一起访问的元素(因为它们共享很多)也将被分组在一起,而不是分散到磁盘的四个角落。

最后,内部Oracle 可以在其执行计划中使用哈希表。当您执行需要将两组行相交的操作时,优化引擎可能会决定将(临时)组存储在哈希表中是最快的方法。


现在,关于性能。

确实,Search Trees 的性能一般是众所周知且易于理解的,O(log N) 很好,也很整洁。

另一方面,正如我所说,有许多不同的哈希表实现可能,以及处理增长和收缩的策略......肯定更复杂。

一个简单的结构示例,哈希表可以使用:

Open Addressing:哈希表是一个元素数组,哈希表示数组的槽位,如果槽位已满,则有策略定位另一个槽位。搜索使用相同的策略。 Buckets:哈希表是一个指向bucket的指针数组,hash表示bucket的槽位,其中放置元素。假设桶可以无限增长。

这两种策略有着截然不同的特点,而后者的特点也取决于桶的实现(简单的实现是使用简单的链表)。

但即使你选择一个实现,它的性能也是基于散列函数分布,这取决于输入序列本身!


我的个人建议?要在 C++ 中的 unordered_mapmap 之间进行选择,我只是问自己是否需要排序元素。如果我需要对它们进行排序,我使用map,否则我使用unordered_map。大多数时候,性能都一样好,所以只是语义

【讨论】:

不错的答案,但为什么最后一句?对于大型数据集中的典型查找需求(无需排序),我发现 unordered_mapmap 快得多 @EliBendersky:因为大多数初学者都被性能所吸引,所以他们专注于大 O,同时使用 10 个元素的序列……如果您需要性能,请测量(配置文件),否则就让您的生活通过选择支持手头任务所需语义的容器,更容易。 很公平,但我认为 SO 答案应该在一般意义上准确,而不是针对特定的用户群(不了解性能和大 O 东西的初学者)。如果您确实将其瞄准某个片段,请至少在括号或脚注中明确注明。否则,脱离其上下文,答案似乎不正确 一些历史点:大部分 C++ 不是由委员会设计的;在大多数情况下(有例外),委员会标准化了现有做法。大部分库来自 Alexander Stepanov 的 STL,所以问题是为什么 Stepanov 选择二叉树而不是哈希表。以及为什么没有人提出哈希表提案,直到为时已晚——当提案提出时,委员会非常看好它,但拒绝了它,因为在这个过程中将新的东西集成到这个过程中为时已晚标准。 @JamesKanze:重读我的回答,不清楚这是我的怀疑。我已经全部删除并重定向到您的评论。【参考方案4】:

这或多或少是语言设计者的任意选择。在里面 以 C++ 为例,我怀疑(但不确定)动机是 定义严格的复杂性上限的愿望:设计一个好的 散列函数不是微不足道的,散列函数很差的散列表 表现很差。另一个可能考虑过的问题是 有一个既定的订购操作员(<);那里 与散列没有什么相似之处。

在 Python(和许多其他语言)的情况下,很多时候, 键将是内置类型,例如 strstd::string 不是 在定义 STL 时可用),因此您可以确保足够的 哈希函数。当一切都是对象,并且继承自 通用基类,您可以轻松定义标准接口 hash,通过在通用基类中定义一个(虚拟)函数。

最后,C++ 解决方案依赖于单个函数/运算符;哈希 表需要两个(哈希函数和相等),必须 兼容,更容易出错。 Java 中的一个常见错误是 定义equals,但不定义hashCode;我怀疑有 犯同样错误的 Python 类(定义 __cmp____eq__,但不是__hash__)。当然,看次数 人们弄乱了 C++ 中的 < 运算符,我不确定它是否安全 要么:-)。

【讨论】:

以上是关于为啥 python 的 dict 实现为哈希表,而 std::map 是基于树的?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 Haskell Maps 实现为平衡二叉树而不是传统的哈希表?

Python哈希表的例子:dictset

python set dict实现原理

python set dict实现原理

python 字典为啥这么快

如何将 Python 字典转换为 JavaScript 哈希表?