哈希表是计算机科学中常用的数据结构之一,它提供了快速的查找、插入和删除操作。在本篇博客中,我们将探讨如何构造一个高效的哈希表,从最基本的思路逐步完善,直至最终实现。
1. 初始思路:使用布尔数组存储
我们最初的想法是使用一个布尔数组来存储哈希表的元素。具体来说,数组中的每个元素代表一个可能的键值,如果某个键存在于哈希表中,则将对应位置的布尔值设为true
,否则设为false
。这种方法的代码如下:
public class SimpleHashSet {
private boolean[] present;
public SimpleHashSet() {
present = new boolean[2000000000];
}
public void add(int key) {
present[key] = true;
}
public boolean contains(int key) {
return present[key];
}
}
这种方法的问题在于,它会浪费大量的内存空间,尤其是当哈希表中只有少量元素时。此外,它只能处理整数类型的键,无法处理其他类型的数据。
2. 使用哈希函数解决键的类型问题
为了解决只能处理整数类型键的问题,我们可以引入哈希函数将其他类型的键转换为整数。下面是一个将字符串转换为整数的示例:
public class DataIndexedEnglishWordSet {
private boolean[] present;
public DataIndexedEnglishWordSet() {
present = new boolean[2000000000];
}
public void add(String key) {
present[englishToInt(key)] = true;
}
public boolean contains(String key) {
return present[englishToInt(key)];
}
private static int letterNum(String s, int i) {
int ithChar = s.charAt(i);
if (ithChar < 'a' || ithChar > 'z') {
throw new IllegalArgumentException();
}
return ithChar - 'a' + 1;
}
private static int englishToInt(String s) {
int intRep = 0;
for (int i = 0; i < s.length(); i++) {
intRep = intRep * 26;
intRep += letterNum(s, i);
}
return intRep;
}
}
虽然现在我们可以处理字符串类型的键,但仍然存在两个主要问题:内存浪费和处理哈希冲突的困难。
3. 引入开放地址法解决冲突
为了解决哈希冲突的问题,我们引入了开放地址法中的线性探查。当插入的位置已被占用时,通过顺序检查数组的下一个位置来解决冲突。
public class DataIndexedEnglishWordSet {
private boolean[] present;
public DataIndexedEnglishWordSet() {
present = new boolean[2000000000];
}
public void add(String key) {
int index = englishToInt(key);
while (present[index] != false) {
index = (index + 1) % present.length;
}
present[index] = true;
}
public boolean contains(String key) {
int index = englishToInt(key);
while (present[index] != false) {
if (present[index]) {
return true;
}
index = (index + 1) % present.length;
}
return false;
}
private static int letterNum(String s, int i) {
int ithChar = s.charAt(i);
if (ithChar < 'a' || ithChar > 'z') {
throw new IllegalArgumentException();
}
return ithChar - 'a' + 1;
}
private static int englishToInt(String s) {
int intRep = 0;
for (int i = 0; i < s.length(); i++) {
intRep = intRep * 26;
intRep += letterNum(s, i);
}
return intRep;
}
}
通过引入线性探查,我们可以更有效地处理哈希冲突,提高了哈希表的性能。
4. 使用取模操作减少空间浪费
为了减少空间浪费,我们修改了哈希函数,使用取模操作将键映射到数组索引上。
public class DataIndexedEnglishWordSet {
private boolean[] present;
public DataIndexedEnglishWordSet() {
present = new boolean[101]; // 使用最小素数作为初始容量
}
public void add(String key) {
int index = englishToInt(key);
while (present[index] != false) {
index = (index + 1) % present.length;
}
present[index] = true;
}
public boolean contains(String key) {
int index = englishToInt(key);
while (present[index] != false) {
if (present[index]) {
return true;
}
index = (index + 1) % present.length;
}
return false;
}
private static int letterNum(String s, int i) {
int ithChar = s.charAt(i);
if (ithChar < 'a' || ithChar > 'z') {
throw new IllegalArgumentException();
}
return ithChar - 'a' + 1;
}
private int englishToInt(String s) {
int intRep = 0;
for (int i = 0; i < s.length(); i++) {
intRep = intRep * 26;
intRep += letterNum(s, i);
}
return intRep % present.length; // 修改为取模操作
}
}
通过取模操作,我们将键均匀地映射到数组索引上,减少了空间浪费。
5. 动态更改哈希表大小以优化性能
为了进一步优化性能,我们添加了动态调整哈希表大小的功能。当负载因子超过阈值时,我们将重构哈希表并扩大其容量。
public class ImprovedHashSet {
private boolean[] present;
private int size;
private static final float LOAD_FACTOR_THRESHOLD = 0.75f;
public ImprovedHashSet() {
present = new boolean[101];
size = 0;
}
public void add(String key) {
if (loadFactor() >= LOAD_FACTOR_THRESHOLD) {
resize();
}
int index = englishToInt(key);
while (present[index] != false) {
index = (index + 1) % present.length;
}
present[index] = true;
size++;
}
public boolean contains(String key) {
int index = englishToInt(key);
while (present[index] != false) {
if (present[index]) {
return true;
}
index = (index + 1) % present.length;
}
return false;
}
private float loadFactor() {
return (float) size / present.length;
}
private void resize() {
boolean[] oldPresent = present;
int newCapacity = nextPrime(oldPresent.length * 2);
present = new boolean[newCapacity];
size = 0;
for (boolean value : oldPresent) {
if (value) {
add(someKey); // 重新插入元素,假设someKey为原哈希表的所有键
}
}
}
private int nextPrime(int n) {
while (true) {
if (isPrime(n)) {
return n;
}
n++;
}
}
private boolean isPrime(int n) {
if (n <= 1) {
return false;
}
if (n <= 3) {
return true;
}
if (n % 2 == 0 || n % 3 == 0) {
return false;
}
int i = 5;
while (i * i <= n) {
if (n % i == 0 || n % (i + 2) == 0) {
return false;
}
i += 6;
}
return true;
}
private static int letterNum(String s, int i) {
int ithChar = s.charAt(i);
if (ithChar < 'a' || ithChar > 'z') {
throw new IllegalArgumentException();
}
return ithChar - 'a' + 1;
}
private int englishToInt(String s) {
int intRep = 0;
for (int i = 0; i < s.length(); i++) {
intRep = intRep * 26;
intRep += letterNum(s, i);
}
return intRep % present.length;
}
}
通过动态调整哈希表大小,我们可以根据负载因子的变化来合理分配内存,提高了哈希表的效率和性能。
总结
在本篇博客中,我们从最基本的思路出发,逐步完善了一个高效的哈希表的实现。我们通过引入哈希函数解决键的类型问题,使用开放地址法解决冲突,通过取模操作减少空间浪费,并最终实现了动态更改哈希表大小以
优化性能。这些优化措施使得我们的哈希表在处理不同类型的键和不同规模的数据时都能保持高效运行。
希望通过本文的介绍,你对构造高效的哈希表有了更深入的理解,也能在实际应用中更好地利用哈希表这一重要的数据结构。