面试题答案
一键面试实现主要步骤
- 数据结构调整:
- 自定义一个哈希表结构,例如
MyHashMap
,包含一个数组用于存储键值对和一些元数据(如当前元素数量、容量等)。数组的每个元素可以是一个Option<(K, V)>
,其中K
是键类型,V
是值类型。
struct MyHashMap<K, V> { table: Vec<Option<(K, V)>>, count: usize, capacity: usize, }
- 自定义一个哈希表结构,例如
- 哈希函数:
- 定义一个哈希函数,将键映射到数组的索引。可以使用标准库中的
Hash
trait来实现,例如:
use std::hash::Hash; fn hash_function<K: Hash>(key: &K, capacity: usize) -> usize { key.hash(&mut std::collections::hash_map::DefaultHasher::new()) % capacity }
- 定义一个哈希函数,将键映射到数组的索引。可以使用标准库中的
- 插入方法:
- 计算键的初始哈希值。
- 线性探测数组,找到一个空的位置或键相同的位置进行插入。如果找到键相同的位置,更新值;如果找到空位置,插入键值对。
impl<K, V> MyHashMap<K, V> { fn insert(&mut self, key: K, value: V) where K: Hash + Eq, { if self.count >= self.capacity * 0.75 { self.resize(); } let mut index = hash_function(&key, self.capacity); loop { match self.table[index] { None => { self.table[index] = Some((key, value)); self.count += 1; break; } Some((ref k, _)) if k == &key => { self.table[index] = Some((key, value)); break; } _ => { index = (index + 1) % self.capacity; } } } } }
- 查找方法:
- 计算键的初始哈希值。
- 线性探测数组,查找键对应的位置。如果找到键相同的位置,返回值;如果找到空位置,说明键不存在。
impl<K, V> MyHashMap<K, V> { fn get(&self, key: &K) -> Option<&V> where K: Hash + Eq, { let mut index = hash_function(key, self.capacity); loop { match self.table[index] { None => return None, Some((ref k, ref v)) if k == key => return Some(v), _ => { index = (index + 1) % self.capacity; } } } } }
- 删除方法:
- 计算键的初始哈希值。
- 线性探测数组,查找键对应的位置。如果找到键相同的位置,将该位置设为特殊的“已删除”状态(例如
Some((None, None))
,但实际应用中可能需要更优雅的设计),并调整后续元素的位置以保持哈希表的一致性。
impl<K, V> MyHashMap<K, V> { fn remove(&mut self, key: &K) where K: Hash + Eq, { let mut index = hash_function(key, self.capacity); loop { match self.table[index] { None => return, Some((ref k, _)) if k == key => { self.table[index] = None; self.count -= 1; self.fix_deleted(index); return; } _ => { index = (index + 1) % self.capacity; } } } } fn fix_deleted(&mut self, index: usize) { let mut current = index; let mut next = (current + 1) % self.capacity; while let Some((_, _)) = self.table[next] { let (key, value) = self.table[next].take().unwrap(); self.table[next] = None; self.count -= 1; self.insert(key, value); current = next; next = (current + 1) % self.capacity; } } }
- 扩容方法:
- 当哈希表的负载因子达到一定阈值(如0.75)时,需要对哈希表进行扩容。
- 创建一个新的更大容量的数组,将原数组中的所有键值对重新插入到新数组中。
impl<K, V> MyHashMap<K, V> { fn resize(&mut self) { let new_capacity = self.capacity * 2; let mut new_table = Vec::with_capacity(new_capacity); new_table.resize(new_capacity, None); for entry in self.table.drain(..) { if let Some((key, value)) = entry { let mut index = hash_function(&key, new_capacity); loop { match new_table[index] { None => { new_table[index] = Some((key, value)); break; } _ => { index = (index + 1) % new_capacity; } } } } } self.table = new_table; self.capacity = new_capacity; } }
可能面临的挑战及解决方法
- 聚集问题:
- 挑战:线性探测容易导致聚集现象,即连续的多个位置被占用,使得后续插入、查找和删除操作的平均时间复杂度增加。
- 解决方法:可以采用二次探测法(每次探测的步长为
1, 4, 9, 16...
等平方数)或双重哈希法(使用两个哈希函数,第二个哈希函数用于计算探测步长)来减少聚集。
- 性能问题:
- 挑战:在负载因子较高时,线性探测的性能会显著下降。
- 解决方法:合理设置负载因子阈值,当负载因子达到一定值时及时扩容,保持哈希表的稀疏性,从而提高性能。
- 删除操作的复杂性:
- 挑战:删除操作不仅要删除键值对,还需要调整后续元素的位置以保持哈希表的一致性,否则可能影响查找和插入操作。
- 解决方法:如上述代码中,在删除元素后,通过
fix_deleted
方法重新插入后续被占用位置的元素,以修复哈希表的结构。