哈希表作为数据结构课程中的核心内容,本质上是一种通过哈希函数将键映射到存储位置的快速查找结构。在解决洛谷P11615这道模板题时,我们需要实现一个支持插入、查询和删除三大基础操作的哈希表。
这道题的数据规模要求我们处理高达10^6次操作,这意味着普通的线性查找(O(n))或二叉搜索树(O(logn))都无法满足性能需求。哈希表在这种场景下的优势就凸显出来了——在合理设计的情况下,它能提供平均O(1)时间复杂度的操作性能。
题目给出的约束条件中特别强调"所有操作均为合法操作",这个提示非常重要。它意味着:
选择哈希函数是构建哈希表的第一步。本题采用了最简单的取模哈希:
code复制h(x) = x mod N
其中N是哈希表的大小。这里有几个关键考量:
N必须是一个素数,这能有效减少因数的共性导致的哈希冲突。比如选择2000003这个素数,它比题目要求的1e6数据量大一倍左右,为冲突留出了缓冲空间。
对负数取模的处理:
cpp复制int t = (x % N + N) % N;
这个技巧确保无论x是正是负,最终结果都在[0, N-1]范围内。C++的%运算符对负数取模会得到负结果,所以需要这样的调整。
开放寻址法处理冲突的核心思想是:当理想位置被占用时,按照预定规则寻找下一个可用位置。本题采用最简单的线性探测:
code复制h'(x) = (h(x) + k) mod N
其中k从1开始递增。这种方法的优点是实现简单,缓存友好(访问连续内存地址)。但需要注意:
探测步长通常为1,但也可以选择其他固定步长(与N互质更好)
当到达数组末尾时需要回绕到开头:
cpp复制if(t == N) t = 0;
删除操作在开放寻址法中是个棘手问题。直接清空位置会导致后续探测链断裂,因此我们采用惰性删除策略:
这种实现需要注意:
cpp复制const int null = 0x3f3f3f3f;
memset(h, 0x3f, sizeof(h));
memset按字节设置内存,而0x3f3f3f3f正好是每个字节都是0x3f的int值。
find函数是整套实现的核心,它统一处理了插入、查询和删除的位置查找:
cpp复制int find(int x){
int t = (x % N + N) % N; // 处理负数的取模
while(h[t] != null && h[t] != x){ // 冲突处理
t++;
if(t == N) t = 0; // 循环查找
}
return t;
}
这个函数的精妙之处在于:
主函数通过简单的分支处理三种操作:
cpp复制int pos = find(x);
if(op == 'I') h[pos] = x;
else if(op == 'Q') puts(h[pos] == x ? "Yes" : "No");
else if(op == 'D')
if(h[pos] == x)
h[pos] = null;
这种结构保证了:
选择N=2000003(大于2×10^6的素数)的考虑:
实际工程中,可以根据预期数据量动态调整表大小,但本题固定大小已足够。
探测循环必须满足两个终止条件之一:
这保证了算法不会无限循环。在本题中,由于题目保证操作合法,所以不需要额外检查表是否已满。
使用0x3f3f3f3f作为空标记有几个好处:
虽然取模哈希简单高效,但在实际应用中可能需要更复杂的哈希函数:
除了线性探测,还有:
开放寻址法虽然实现简单,但在实际工程中需要考虑更多因素:
在算法竞赛中,哈希表常用于:
我个人的经验是,对于1e6量级的数据,精心实现的哈希表比平衡树快3-5倍。但要注意,哈希表无法维护有序性,这是它的主要局限。