在计算机科学中,哈希表是最重要的数据结构之一,它能在平均O(1)时间复杂度内完成数据的插入、删除和查找。闭散列(Open Addressing)是解决哈希冲突的经典策略,与开链法(Separate Chaining)不同,它将所有元素直接存储在底层数组中。
线性探测是闭散列最简单的实现方式,其冲突解决策略可以概括为:
这种方案的优势在于:
关键理解:线性探测形成的"元素簇"会显著影响性能。当负载因子升高时,这些簇会越来越长,导致操作时间复杂度从O(1)退化为O(n)。
传统数组实现无法区分"从未使用"和"曾经使用但已删除"的位置,这会导致查找算法提前终止。我们引入三种状态:
EMPTY:初始状态,查找终止条件EXIST:有效数据标记DELETE:特殊标记,查找时需跳过但插入时可复用cpp复制enum State {
EMPTY, // 0b00
EXIST, // 0b01
DELETE // 0b10
};
这种设计带来两个重要特性:
对于整型类型,直接使用静态转换是最简单有效的方式:
cpp复制template<class K>
struct HashFunc {
size_t operator()(const K& key) const {
return static_cast<size_t>(key);
}
};
需要注意的细节:
size_t保证足够的数值空间const修饰保证线程安全BKDR哈希算法因其简单高效成为字符串哈希的经典选择:
cpp复制template<>
struct HashFunc<std::string> {
size_t operator()(const std::string& key) const {
size_t seed = 131; // 31/131/1313/13131...
size_t hash = 0;
for(char ch : key) {
hash = hash * seed + ch;
}
return hash;
}
};
算法特点:
直接取模可能造成分布不均,改进方案:
cpp复制size_t hashi = hash(key) % _tables.size();
// 优化为:
size_t hashi = hash(key);
if constexpr (sizeof(size_t) > 4) {
hashi = (hashi >> 32) ^ hashi; // 混合高位和低位
}
hashi %= _tables.size();
cpp复制bool Insert(const pair<K, V>& kv) {
// 唯一性检查
if(Find(kv.first)) return false;
// 扩容检查与执行
if(_tables.empty() || _size*10/_tables.size() >= 7) {
size_t newSize = _tables.empty() ? 10 : _tables.size()*2;
// 创建临时表并重新哈希
HashTable<K,V,Hash> newTable;
newTable._tables.resize(newSize);
for(auto& data : _tables) {
if(data._state == EXIST) {
newTable.Insert(data._kv);
}
}
_tables.swap(newTable._tables);
}
// 线性探测插入
size_t hashi = _Hash(kv.first);
while(_tables[hashi]._state == EXIST) {
hashi = (hashi+1) % _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
++_size;
return true;
}
关键优化点:
cpp复制HashData<K,V>* Find(const K& key) {
if(_tables.empty()) return nullptr;
size_t start = _Hash(key);
size_t hashi = start;
do {
if(_tables[hashi]._state == EMPTY) {
break;
}
if(_tables[hashi]._state == EXIST &&
_tables[hashi]._kv.first == key) {
return &_tables[hashi];
}
hashi = (hashi+1) % _tables.size();
} while(hashi != start);
return nullptr;
}
注意事项:
cpp复制bool Erase(const K& key) {
auto* data = Find(key);
if(!data) return false;
data->_state = DELETE;
--_size;
return true;
}
设计要点:
原始方案在扩容时需要重建整个哈希表,可以改进为:
cpp复制void Reserve(size_t n) {
if(n > _tables.size()) {
HashTable<K,V,Hash> newTable;
newTable._tables.resize(n);
for(auto& data : _tables) {
if(data._state == EXIST) {
newTable.Insert(data._kv);
}
}
_tables.swap(newTable._tables);
}
}
使用场景:
cpp复制template<class K, class V, class Hash>
struct __HashIterator {
typedef HashData<K,V> Data;
typedef __HashIterator<K,V,Hash> Self;
Data* _ptr;
const HashTable<K,V,Hash>* _ht;
Self& operator++() {
_ptr++;
while(_ptr != _ht->_tables.data()+_ht->_tables.size()) {
if(_ptr->_state == EXIST) break;
_ptr++;
}
return *this;
}
};
注意事项:
基础版本非线程安全,可添加:
cpp复制class HashTable {
//...
mutable std::mutex _mutex;
bool Insert(const pair<K,V>& kv) {
std::lock_guard<std::mutex> lock(_mutex);
//...原实现
}
};
锁粒度优化:
cpp复制void TestInsert() {
HashTable<int, string> ht;
// 基础插入
assert(ht.Insert({1, "one"}));
// 重复插入
assert(!ht.Insert({1, "uno"}));
// 冲突处理
assert(ht.Insert(11, "eleven")); // 假设11与1冲突
}
void TestErase() {
//...类似结构
}
测试重点:
关键指标测量:
cpp复制auto start = std::chrono::high_resolution_clock::now();
// 测试操作
auto end = std::chrono::high_resolution_clock::now();
std::cout << "操作耗时: "
<< std::chrono::duration_cast<std::chrono::microseconds>(end-start).count()
<< "μs\n";
优化方向:
增强版Print函数:
cpp复制void Print() const {
for(size_t i=0; i<_tables.size(); ++i) {
std::cout << "[" << i << "] ";
switch(_tables[i]._state) {
case EMPTY: std::cout << "EMPTY"; break;
case EXIST: std::cout << _tables[i]._kv.first; break;
case DELETE: std::cout << "DEL(" << _tables[i]._kv.first << ")"; break;
}
std::cout << "\n";
}
}
输出示例:
code复制[0] EMPTY
[1] 10
[2] DEL(20)
[3] 30
cpp复制size_t hashi = _Hash(key);
size_t i = 0;
while(_tables[hashi]._state == EXIST) {
hashi = (hashi + i*i) % _tables.size();
i++;
}
优势:
cpp复制class CuckooHashTable {
vector<HashData> _table1;
vector<HashData> _table2;
size_t _hash1(const K& key);
size_t _hash2(const K& key);
bool InsertHelper(const pair<K,V>& kv, int tableIdx, int depth) {
// 递归实现插入与踢出
}
};
特点:
实现思路:
适用场景:
cpp复制void Clear() noexcept {
for(auto& data : _tables) {
if(data._state == EXIST) {
data._kv.~pair<K,V>();
}
data._state = EMPTY;
}
_size = 0;
}
~HashTable() {
Clear();
_tables.clear();
}
注意事项:
cpp复制bool Insert(const pair<K,V>& kv) {
auto* found = Find(kv.first);
if(found) return false;
try {
// 可能抛出异常的操作
if(needExpand()) {
Reserve(_tables.size()*2);
}
size_t hashi = _Hash(kv.first);
while(_tables[hashi]._state == EXIST) {
hashi = (hashi+1) % _tables.size();
}
new (&_tables[hashi]._kv) pair<K,V>(kv);
_tables[hashi]._state = EXIST;
++_size;
} catch(...) {
// 恢复状态
return false;
}
return true;
}
关键点:
实测建议:
典型性能数据:
| 操作 | 负载0.5 | 负载0.7 | 负载0.9 |
|---|---|---|---|
| 插入 | 120ns | 180ns | 650ns |
| 查找 | 80ns | 130ns | 550ns |
| 删除 | 90ns | 140ns | 160ns |
场景再现:
解决方案:
cpp复制while(_tables[hashi]._state != EMPTY) {
// ...查找逻辑
if(++probeCount > _tables.size()) {
break; // 强制终止
}
}
表现症状:
改进方法:
cpp复制size_t hash = std::hash<K>{}(key);
hash ^= (hash >> 32) | (hash << 32);
优化策略:
cpp复制struct HashData {
State _state;
union {
pair<K,V> _kv;
size_t _next; // 用于内存池
};
};
cpp复制template<typename Key, typename Value>
class LRUCache {
HashTable<Key, Value> _hashTable;
list<Key> _lruList;
size_t _capacity;
void Get(const Key& key) {
auto* data = _hashTable.Find(key);
if(data) {
_lruList.splice(_lruList.begin(), _lruList,
find(_lruList.begin(), _lruList.end(), key));
return data->_kv.second;
}
// ...缓存未命中处理
}
};
cpp复制void WordCount(const string& filename) {
HashTable<string, size_t> wordCount;
ifstream file(filename);
string word;
while(file >> word) {
auto* data = wordCount.Find(word);
if(data) {
++data->_kv.second;
} else {
wordCount.Insert({word, 1});
}
}
// 输出统计结果
for(auto& data : wordCount) {
cout << data.first << ": " << data.second << "\n";
}
}
cpp复制class SimpleDBIndex {
HashTable<Key, vector<Record*>> _index;
void AddRecord(Record* record) {
auto* bucket = _index.Find(record->key);
if(bucket) {
bucket->push_back(record);
} else {
_index.Insert({record->key, {record}});
}
}
vector<Record*> Query(const Key& key) {
auto* bucket = _index.Find(key);
return bucket ? *bucket : vector<Record*>{};
}
};
| 特性 | 哈希表 | 平衡树 | 跳表 |
|---|---|---|---|
| 平均查找 | O(1) | O(log n) | O(log n) |
| 有序性 | 无 | 有 | 有 |
| 内存使用 | 中 | 低 | 高 |
| 实现复杂度 | 低 | 高 | 中 |
罗宾汉哈希(Robin Hood Hashing):
跳房子哈希(Hopscotch Hashing):
瑞士表(Swiss Table):
经典教材:
开源实现:
论文资源:
在实际工程中,哈希表的选择需要综合考虑数据特征、访问模式、内存约束和并发需求等因素。闭散列方案虽然简单,但在特定场景下仍然具有不可替代的优势。