设计一个哈希集合(HashSet),不使用任何内建的哈希表库,实现以下操作:
add(key)
: 向哈希集合中插入值 keyremove(key)
: 将给定值 key 从哈希集合中删除contains(key)
: 返回哈希集合中是否存在这个值 key数据范围: 0 <= key <= 10^6,最多调用 10^4 次操作
哈希表是一种在时间和空间上做权衡的经典数据结构。其核心思想是:
直接使用一个足够大的数组,让每个key都有唯一的索引位置,完全避免冲突。
class MyHashSet {
private:
vector<bool> data;
public:
MyHashSet() {
// 10^6 + 1 大小的数组,key直接作为索引
data.resize(1000001, false);
}
void add(int key) {
data[key] = true;
}
void remove(int key) {
data[key] = false;
}
bool contains(int key) {
return data[key];
}
};
class MyHashSet:
def __init__(self):
# 使用列表模拟超大数组
self.data = [False] * 1000001
def add(self, key: int) -> None:
self.data[key] = True
def remove(self, key: int) -> None:
self.data[key] = False
def contains(self, key: int) -> bool:
return self.data[key]
优点:
缺点:
使用较小的数组作为桶,每个桶维护一个链表处理冲突。这是大多数编程语言哈希表的实现方式。
class MyHashSet {
private:
// 桶的数量,选择质数有助于均匀分布
static const int BASE = 1009;
vector<list<int>> buckets;
// 哈希函数:取模运算
int hash(int key) {
return key % BASE;
}
public:
MyHashSet() {
buckets.resize(BASE);
}
void add(int key) {
int index = hash(key);
// 检查是否已存在
for (auto it = buckets[index].begin(); it != buckets[index].end(); ++it) {
if (*it == key) {
return; // 已存在,直接返回
}
}
// 不存在则添加
buckets[index].push_back(key);
}
void remove(int key) {
int index = hash(key);
// 在链表中查找并删除
for (auto it = buckets[index].begin(); it != buckets[index].end(); ++it) {
if (*it == key) {
buckets[index].erase(it);
return;
}
}
}
bool contains(int key) {
int index = hash(key);
// 在链表中查找
for (auto it = buckets[index].begin(); it != buckets[index].end(); ++it) {
if (*it == key) {
return true;
}
}
return false;
}
};
class MyHashSet:
def __init__(self):
# 选择质数作为桶数,有助于均匀分布
self.buckets = 1009
self.table = [[] for _ in range(self.buckets)]
def hash(self, key: int) -> int:
return key % self.buckets
def add(self, key: int) -> None:
hash_key = self.hash(key)
# 检查是否已存在
if key not in self.table[hash_key]:
self.table[hash_key].append(key)
def remove(self, key: int) -> None:
hash_key = self.hash(key)
# 如果存在则删除
if key in self.table[hash_key]:
self.table[hash_key].remove(key)
def contains(self, key: int) -> bool:
hash_key = self.hash(key)
return key in self.table[hash_key]
优点:
缺点:
插播迭代器相关
迭代器(Iterator)是C++ STL中的一个重要概念,它提供了一种统一的方式来访问容器中的元素。可以把迭代器想象成一个"指针",它指向容器中的某个元素。
容器是存储数据的结构,比如:
vector
- 动态数组list
- 双向链表set
- 集合map
- 映射迭代器是访问容器元素的工具,类似于指针。
vector<int> vec = {1, 2, 3, 4, 5};
// 获取指向第一个元素的迭代器
auto begin_it = vec.begin();
// 获取指向最后一个元素之后位置的迭代器
auto end_it = vec.end();
vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin();
// 解引用:获取迭代器指向的值
int value = *it; // value = 1
// 移动迭代器
++it; // 移动到下一个元素
--it; // 移动到上一个元素(如果支持)
// 比较迭代器
if (it != vec.end()) {
// 迭代器还没有到达末尾
}
让我们逐步分析这段代码:
void add(int key) {
int index = hash(key);
// 检查是否已存在?
for (auto it = buckets[index].begin(); it != buckets[index].end(); ++it) {
if (*it == key) return;
}
// 不存在, 则直接添加
buckets[index].push_back(key);
}
auto
关键字auto it = buckets[index].begin();
auto
是C++11引入的类型推导关键字it
的类型list::iterator it = buckets[index].begin();
begin()
和 end()
方法buckets[index].begin() // 返回指向链表第一个元素的迭代器
buckets[index].end() // 返回指向链表末尾之后位置的迭代器
for (auto it = buckets[index].begin(); it != buckets[index].end(); ++it) {
if (*it == key) return;
}
这个循环可以分解为:
// 1. 初始化:获取指向链表开头的迭代器
auto it = buckets[index].begin();
// 2. 条件检查:如果迭代器还没到达末尾
while (it != buckets[index].end()) {
// 3. 解引用迭代器,获取当前元素的值
if (*it == key) {
return; // 找到重复元素,直接返回
}
// 4. 移动到下一个元素
++it;
}
假设我们有一个链表:[1, 2, 3, 4]
链表: [1] -> [2] -> [3] -> [4] -> null
↑ ↑
begin() end()
begin()
指向第一个元素 1
end()
指向最后一个元素之后的位置(null)vector<int> vec = {1, 2, 3, 4, 5};
// 正向遍历
for (auto it = vec.begin(); it != vec.end(); ++it) {
cout << *it << " "; // 输出:1 2 3 4 5
}
// 反向遍历
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
cout << *it << " "; // 输出:5 4 3 2 1
}
list<int> lst = {1, 2, 3, 4, 5};
// 正向遍历
for (auto it = lst.begin(); it != lst.end(); ++it) {
cout << *it << " "; // 输出:1 2 3 4 5
}
// 反向遍历
for (auto it = lst.rbegin(); it != lst.rend(); ++it) {
cout << *it << " "; // 输出:5 4 3 2 1
}
vector<int> vec = {1, 2, 3, 4, 5};
// 传统迭代器写法
for (auto it = vec.begin(); it != vec.end(); ++it) {
cout << *it << " ";
}
// 现代范围for循环(推荐)
for (const auto& element : vec) {
cout << element << " ";
}
void add(int key) {
int index = hash(key);
// 使用范围for循环检查是否已存在
for (const auto& element : buckets[index]) {
if (element == key) {
return; // 已存在,直接返回
}
}
// 不存在,则添加
buckets[index].push_back(key);
}
只能读取元素,只能向前移动
// 例如:istream_iterator
只能写入元素,只能向前移动
// 例如:ostream_iterator
可以读写元素,只能向前移动
// 例如:forward_list的迭代器
可以读写元素,可以向前和向后移动
// 例如:list的迭代器
可以读写元素,可以随机访问任意位置
// 例如:vector的迭代器
vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin();
// 错误:在遍历过程中修改容器
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it == 2) {
vec.erase(it); // 错误!迭代器失效
}
}
vector<int> vec = {1, 2, 3, 4, 5};
// 方法1:使用erase的返回值
for (auto it = vec.begin(); it != vec.end();) {
if (*it == 2) {
it = vec.erase(it); // erase返回下一个有效的迭代器
} else {
++it;
}
}
// 方法2:使用remove-erase惯用法
vec.erase(remove(vec.begin(), vec.end(), 2), vec.end());
class MyHashSet {
private:
vector<list<int>> buckets;
public:
bool contains(int key) {
int index = hash(key);
// 使用迭代器遍历链表
for (auto it = buckets[index].begin(); it != buckets[index].end(); ++it) {
if (*it == key) {
return true; // 找到元素
}
}
return false; // 未找到元素
}
};
void remove(int key) {
int index = hash(key);
// 使用迭代器查找并删除
for (auto it = buckets[index].begin(); it != buckets[index].end(); ++it) {
if (*it == key) {
buckets[index].erase(it); // 删除当前元素
return;
}
}
}
begin()
指向第一个元素,end()
指向末尾之后的位置auto
用于自动类型推导*it
用于解引用,获取迭代器指向的值++it
用于移动到下一个元素理解迭代器是掌握C++ STL的关键,它是连接算法和容器的桥梁!
到此为止
// 拉链法
class MyHashSet {
//
private:
static const int BASE = 1009;
vector<list<int>> buckets;
// 哈希函数
int hash(int key) {
return key % BASE;
}
public:
MyHashSet() {
buckets.resize(BASE);
}
void add(int key) {
int index = hash(key);
// 检查是否已存在?
for (auto it = buckets[index].begin(); it != buckets[index].end(); ++it) {
if (*it == key) return;
}
// 不存在, 则直接添加
buckets[index].push_back(key);
}
void remove(int key) {
int index = hash(key);
for (auto it = buckets[index].begin(); it != buckets[index].end();++it) {
if (*it == key) {
buckets[index].erase(it);
return;
}
}
}
bool contains(int key) {
int index = hash(key);
// 现代范围for循环(推荐)
for (const auto& element : buckets[index]) {
if (element == key) {
return true;
}
}
return false;
}
};
使用位运算实现类似bitmap的数据结构。每个int的32位可以表示32个不同的key,大大节省空间。
class MyHashSet {
private:
vector<int> buckets;
// 设置指定位的值
void setBit(int bucket, int bit, bool val) {
if (val) {
// 设置位为1:使用OR操作
buckets[bucket] |= (1 << bit);
} else {
// 设置位为0:使用AND操作
buckets[bucket] &= ~(1 << bit);
}
}
// 获取指定位的值
bool getBit(int bucket, int bit) {
return (buckets[bucket] >> bit) & 1;
}
public:
MyHashSet() {
// 10^6 / 32 ≈ 31250,向上取整到32000
buckets.resize(32000, 0);
}
void add(int key) {
int bucket = key / 32; // 确定桶的位置
int bit = key % 32; // 确定位的位置
setBit(bucket, bit, true);
}
void remove(int key) {
int bucket = key / 32;
int bit = key % 32;
setBit(bucket, bit, false);
}
bool contains(int key) {
int bucket = key / 32;
int bit = key % 32;
return getBit(bucket, bit);
}
};
class MyHashSet:
def __init__(self):
# 10^6 / 32 ≈ 31250,向上取整到32000
self.buckets = [0] * 32000
def set_bit(self, bucket: int, bit: int, val: bool) -> None:
if val:
# 设置位为1
self.buckets[bucket] |= (1 << bit)
else:
# 设置位为0
self.buckets[bucket] &= ~(1 << bit)
def get_bit(self, bucket: int, bit: int) -> bool:
return (self.buckets[bucket] >> bit) & 1
def add(self, key: int) -> None:
bucket = key // 32
bit = key % 32
self.set_bit(bucket, bit, True)
def remove(self, key: int) -> None:
bucket = key // 32
bit = key % 32
self.set_bit(bucket, bit, False)
def contains(self, key: int) -> bool:
bucket = key // 32
bit = key % 32
return self.get_bit(bucket, bit)
设置位为1: x |= (1 << pos)
1 << pos
创建一个只有第pos位为1的掩码|=
操作将该位设置为1,其他位保持不变设置位为0: x &= ~(1 << pos)
~(1 << pos)
创建一个除了第pos位为0,其他位都为1的掩码&=
操作将该位设置为0,其他位保持不变获取位的值: (x >> pos) & 1
>>
右移操作将目标位移到最低位& 1
只保留最低位的值优点:
缺点:
将哈希表设计成二维数组,第一维用于哈希分桶,第二维用于存储具体元素。
class MyHashSet {
private:
static const int BUCKETS = 1000;
static const int ITEMS_PER_BUCKET = 1001;
vector<vector<int>> table;
int hash(int key) {
return key % BUCKETS;
}
int pos(int key) {
return key / BUCKETS;
}
public:
MyHashSet() {
table.resize(BUCKETS);
}
void add(int key) {
int hash_key = hash(key);
// 如果桶为空,初始化
if (table[hash_key].empty()) {
table[hash_key].resize(ITEMS_PER_BUCKET, 0);
}
table[hash_key][pos(key)] = 1;
}
void remove(int key) {
int hash_key = hash(key);
if (!table[hash_key].empty()) {
table[hash_key][pos(key)] = 0;
}
}
bool contains(int key) {
int hash_key = hash(key);
return !table[hash_key].empty() && table[hash_key][pos(key)] == 1;
}
};
class MyHashSet:
def __init__(self):
self.buckets = 1000
self.items_per_bucket = 1001
self.table = [[] for _ in range(self.buckets)]
def hash(self, key: int) -> int:
return key % self.buckets
def pos(self, key: int) -> int:
return key // self.buckets
def add(self, key: int) -> None:
hash_key = self.hash(key)
# 如果桶为空,初始化
if not self.table[hash_key]:
self.table[hash_key] = [0] * self.items_per_bucket
self.table[hash_key][self.pos(key)] = 1
def remove(self, key: int) -> None:
hash_key = self.hash(key)
if self.table[hash_key]:
self.table[hash_key][self.pos(key)] = 0
def contains(self, key: int) -> bool:
hash_key = self.hash(key)
return (self.table[hash_key] and
self.table[hash_key][self.pos(key)] == 1)
方法 | 时间复杂度 | 空间复杂度 | 实现难度 | 适用场景 |
---|---|---|---|---|
超大数组 | O(1) | O(数据范围) | 简单 | 数据范围小,性能要求高 |
拉链法 | O(1)平均 | O(n) | 中等 | 通用场景,面试首选 |
位运算 | O(1) | O(数据范围/32) | 困难 | 空间敏感,位运算熟练 |
定长拉链 | O(1) | O(数据范围) | 中等 | 数据范围已知 |
当负载因子过高时,需要扩容:
// 扩容示例
void resize() {
vector<list<int>> old_buckets = buckets;
buckets.resize(buckets.size() * 2);
// 重新哈希所有元素
for (const auto& bucket : old_buckets) {
for (int key : bucket) {
add(key);
}
}
}
哈希表的设计体现了计算机科学中经典的时空权衡思想:
在实际应用中,需要根据具体场景选择合适的设计:
哈希表的设计不仅考察了算法基础,更体现了系统设计的能力,是面试中的经典题目。