8.3.6 内存使用
符号表的内存使用
方法 | N个元素所需的内存(引用类型) |
---|---|
基于拉链法的散列表 | 48N+32M |
基于线性探测的散列表 | 在32N和128N之间 |
各种二叉查找树 | 56N |
自计算机发展的伊始,研究人员就研究了(并且现在仍在继续研究)散列表并找到了很多方法来改进我们所讨论过的几种基本算法。你能找到大量关于这个主题的文献。大多数改进都能降低时间.空间的曲线:在查找耗时相同的情况下使用更少的空间,或使在使用相同空间的情况下进行更快的查找。其他方法包括提供更好的性能保证,如最坏情况下的查找成本;改进散列函数的设计等。
拉链法和线性探测法的详细比较取决于实现的细节和用例对空间和时间的要求。即使基于性能考虑,选择拉链法而非线性探测法也不一定是合理的 。在实践中,两种方法的性能差别主要是因为拉链法为每个键值对都分配了一小块内存而线性探测则为整张表使用了两个很大的数组。对于非常大的散列表,这些做法对内存管理系统的要求也很不相同。在现代系统中,在性能优先的情景下,最好由专家去把握这种平衡。
有了这些假设,期望散列表能够支持和数组大小无关的常数级别的查找和插人操作是可能的。对于任意的符号表实现,这个期望都是理论上的最优性能。但散列表并非包治百病的灵丹妙药,因为口每种类型的键都需要一个优秀的散列函数;
1.性能保证来自于散列函数的质量:
2.散列函数的计算可能复杂而且昂贵;
3.难以支持有序性相关的符号表操作。
8.4 应用
在计算机发展的早期,符号表帮助程序员从使用机器语言的数字地址进化到在汇编语言中使用符号名称;在现代应用程序中,符号名称的含义能够通行于跨越全球的计算机网络。快速查找算法曾经并继续在计算机领域中扮演着重要角色。符号表的现代应用包括科学数据的组织,例如在基因组数据中寻找分子标记或模式从而绘制全基因组图谱;网络信息的组织,从搜索在线贸易到数字图书馆;以及互联网基础构架的实现,例如包在网络结点中的路由、共享文件系统和流媒体等。高效的查找算法确保了这些以及无数其他重要的应用程序成为可能。
1.能够快速并灵活地从文件中提取由逗号分隔的信息的一个字典程序和一个索引程序。逗号分隔的格式(及类似格式)常用于存储网络信息。
2.为一组文件构建逆向索引的一个程序。
3.一个表示稀疏矩阵的数据类型。它用符号表处理的问题规模能够远远大于这种数据类型的标准实现。在第6章中,我们会学习一种适合于数据库或者文件系统的符号表,它能够保存的数据量超过你的想象
8.4.1 我应该使用符号表的哪种实现
下表总结了由本章中多个命题和性质得到的各种符号表算法的性能特点(散列表的最坏情况除外,它的结果来自于研究文献并且也不太可能在实际应用中遇到)。从表中显然可以知道,对于典型的应用程序,应该在散列表和二叉查找树之间进行选择。
相对二叉查找树,散列表的优点在于代码更简单,且查找时间最优(常数级别,只要键的数据类型是标准的或者简单到我们可以为它写出满足(或者近似满足)均匀性假设的高效散列函数即可)。二叉查找树相对于散列表的优点在于抽象结构更简单(不需要设计散列函数),红黑树可以保证最坏情况下的性能且它能够支持的操作更多(如排名、选择、排序和范围查找)。大多数程序员的第一选择都是散列表, 在其他因素更重要时才会选择红黑树。我们有时候会遇到这个“第选择”的例外:当键都是长字符串时,我们可以构造出比红黑树更灵活而又比散列表更高效的数据结构。
各种符号表实现的渐进性能的总结
算法(数据结构) | 最坏情况下的运行时间的增长数量级(N次插入之后) 查找命中 | 平均情况下的运行时间的增长数量级(N次插入之后) 查找命中 | 关键接口 | 内存使用(字节) |
---|---|---|---|---|
顺序查询(无序链表) | N | N/2 | equals() | 48N |
二分查找 (有序数组) | lgN | lgN | compareTo() | 16N |
二叉树查找(二叉树查找) | N | 1.39lgN | compareTo() | 64N |
2.3树查找(红黑树) | 2lgN | 1.00lgN | compareTo() | 64N |
拉链法(链表数组) | <lgN | N/(2M) | equals() | 48N+64N |
线性探测法( 并行数组) | clgN | <1.5 | hashCode() | 32N ~128N |
我们的符号表实现已经可以广泛应用于各种应用程序,但经过简单的修改后这些算法还可以适应并支持其他一些使用广泛的场景,有必要在这里提下。
8.4.2 原始数据类型
假设我们有一张符号表,其中整型的键对应着浮点型的标准实现值。如果使用我们的标准实现,键和值会被储存在Integer和Double类中,因此我们需要两个额外的引用来访问每个键值对。如果应用程序只会使用几千个键进行几千次查找,那么这些引用可能设什么问题。但如果是对几十亿个键进行几十亿次查找,已吧 空那么这些引用就会造成巨大的额外开销。使用原始数据类型代替Key类型可以为每个键值对节省一个引用。当键的值也是原始数据类型时我们又可以节约另外一个引用。显示了在原始数据类型的实现拉链法中使用原始数据类型的情况,这种交换也适用于符号表的其他实现。
8.4.3 重复键
符号表的实现有时需要专门考虑重复键的可能性。许多应用都希望能够为同一个键绑定多个值。例如在一- 个交易处理系统中,多笔交易的客户属性都是相同的。符号表不允许重复键,因此用例只能自己管理重复键。本节稍后我们会遇到一个这样的示例程序。我们可以考虑在实现中允许数据结构保存重复的键值对,并在查找时返回给定的键所对应的任意值之一。我们也可以加人一个方法来返回给定的键对应的所有值。修改我们实现的二叉查找树和散列表来在数据结构中保存重复的键并不困难。
8.4.5 Java 标准库
Java 的jva.til.TreeMap和java. uil.HashMap分别是基于红黑树和拉链法的散列表的符号表实现。TreeMap没有直接支持rank()、select()和我们的有序符号表API中的一些其他方法, 但它支持一些能够高效实现这些方法的操作。HashMap 和我们的LinearProbingHashST的实现基本相同——它也会动态调整数组的大小来保持使用率大约不超过75%。
为了保持前后一致,我们在本书中一般会使用基于红黑树的符号表或是基于线性探测法的符号表。为了节省篇幅并保证符号表的用例和具体实现的独立性,我们在调用代码中将使用ST来代替有序符号表RedBlackBST,用HashST来代替有序性操作无关紧要且拥有散列函数的LinearProbingHashST.尽管我们知道某些应用可能需要改变或者扩展这些算法和数据结构,我们仍然要这样约定。你应该使用哪种符号表?随便,只要记得测试你的选择是否能够提供所需要的性能就好。