尝试:把数据(int)当作目录使用
- 创建一个布尔类型的 ArrayList,大小为 20 亿。默认情况下,所有内容都是 false。
- add(int x)方法将ArrayList第x位置设为true。这花费Θ(1)时间
- contains(int x)方法返回ArrayList第x位是真还是假。也花费Θ(1)时间。
我们的 DataIndexedIntegerSet 只允许插入整数,但现在我们想插入字符串 "cat"。我们将把可以插入字符串的数据结构称为 DataIntexedEnglishWordSet。
我们可以将小写英文字符串看成26进制的整数,并把它转化为十进制int,这样我们的ArrayList就可以存储小写英文字符串了。
进一步拓展,我们使用ASCII码,每一个字符有一个在0(含)和127(含)之间的值。即相当于128进制整数。如果要包括中文等字符,就要用相应规则拓展,如unicode。unicode里中文最大值为40959。但这时要想表示中文字符串,数值就会膨胀到非常非常大,最终达到integer 溢出(2 147 483 647)。
所以,由鸽巢原理可知只要数据量够大,int就一定会溢出。
避免溢出造成的歧义:HashTable
在ArrayList里不存储真假,而是将真正的字符串(s)都存入ArrayLIst,如果想知道是否存了某个字符串,先找到字符串对应数字的那项,再在这一项内找是否有这个字符串。
下面是一种实现方式:单独链接数据索引数组:数组的每一栏都是个单独的链表,存储了若干个数据。
列表里每个框初始都是空的,当元素x被添加到第h项时:
1. 若框h是空的,我们就创建一个包含x的新列表并将它存在第h项内。
2. 若框h不是空的,如果x不在表中,我们把x存到这个列表内。
这个实现的性能:
1. contains(x) theta(Q)
2. insert(x) theta(Q)
Q为最长的小列表的长度。
每个栏内部的实现方法:可以使用链表LinkedList、数组列表ArrayList、数组集合ArraySet都可以。
改进:节约空间
改进一:取模量。每个字符串按照取100的模量 对应的位置存入。
请注意,我们在数组中的 LinkedList 现在会变长,因为我们要把分布在 4 个十亿索引中的所有项目压缩到 100 个索引中。
注:如果我们主列表只取长度为5,即取5的模,结果操作会相当耗时。
改进二:假设我们有:
1. 增长的栏数M
2. 增长的元素数N
只要M=Θ(N), 那么O(N/M)=O(1)。
改进如下:
- 我们通常把增加M的过程叫做“resizing”。
- N/M通常叫做“load factor", 它代表hash table有多满。如果太满了就要增加栏数M。
当N/M>=1.5 , 就将M乘2。
M乘2以后,所有已存储的元素都要取新M的模并重新存储。
Resizing花费时间Θ(N)。
HashTable在Java里的实现
注:Python里的字典的实质就是HashTable。
在Java里,实现为java.util.HashMap和java.util.HashSet。
注:java对负数的取模运算规则:先忽略负号,按照正数运算之后,被取模的数是正数结果就取正,反之取负。(注:(-2)%5中被取模数是-2)。
如果想得到真正数学意义上的取模,应该使用floorMod(x,y)的函数。
警告:1. 不要往HashSet或HashMap里存可变量。
2. 不要再没重写hashCode之前重写equals。
使用素数进位(如31)能比用126产生更好的随机性,降低重复几率。