《C++哈希表技术深度解析:高效存储与检索的实现原理与优化实践》

前言:大厂面试中,'请实现一个哈希表'是经典考题。要给出满分答案,必须掌握:哈希函数设计原则、装载因子与扩容的关系、各种冲突解决方案的优劣比较。本文不仅涵盖这些核心知识点,更会揭示面试官期待的加分项——比如如何评估哈希函数的雪崩效应,或是解释Java HashMap与C++ unordered_map的关键差异。"

目录

一、哈希表的背景

二、哈希冲突

1.负载因子

2.哈希冲突的概念

三、处理哈希冲突

【1】闭散列

线性探测

二次探测(线性基础上了解)

【2】开散列

桶的扩容

开散列的思考

1. 只能存储key为整形的元素,其他类型怎么解决?

2.除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?

四、方法实现

线性探测法

1.节点设置

2.哈希结构

3.插入

4.查找

5.删除

总代码:test.h

链地址法

1.节点结构

2.哈希结构

3.插入

4.查找

5.删除


一、哈希表的背景

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素 时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即 O(log_2 N),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素!

(总结:对映射关系的改进形成了哈希表)

插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置 取元素比较,若关键码相等,则搜索成功

该方式即为哈希方法,哈希方法中使用的转换函数称为哈希函数构造出来的结构称为哈希表(Hash Table)(或者称散列表)

哈希表有两种高效的存储方式:线性+哈希桶(解决哈希冲突)

例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?

这就涉及到我们的哈希冲突了

二、哈希冲突

1.负载因子

负载因子的定义: 哈希表中已存储的元素数量 / 哈希表的总容量(桶的数量)

负载因子的计算公式:入=n/m

n:是哈希表中当前存储的有效元素数量
m:是哈希表的总容量(即桶数组的长度,如:vector<Node*> 的大小)

负载因子是哈希冲突概率和内存利用率的 “平衡器”

(1)负载因子越小 → 哈希冲突概率越低,插入、查找、删除的时间复杂度接近 O ( 1 ) 


(2)负载因子越大 → 哈希冲突概率越高,查找效率可能会退化至O(n)

负载因子超过阈值时会发什么?

负载因子驱动的扩容流程:

当负载因子超过阈值时,哈希表会触发扩容,流程如下:

  • 新建更大的桶数组:新容量通常是原容量的 2 倍(或接近的质数,依实现而定)
  • 重新映射所有元素:遍历旧哈希表的所有元素,用新哈希函数(或新容量重新取模)将元素插入新桶
  • 释放旧内存:销毁旧桶数组,替换为新桶数组

2.哈希冲突的概念

哈希冲突(Hash Collision):哈希冲突的本质是有限存储空间与无限可能输入之间的矛盾

不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突
或哈希碰撞。
(即:映射到哈希表的同一个桶或位置)

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。

哈希函数设计原则:

  1. 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值
  2. 域必须在0到m-1之间
  3. 哈希函数计算出来的地址能均匀分布在整个空间中
  4. 哈希函数应该比较简单

三、处理哈希冲突

解决哈希冲突两种常见的方法是:闭散列和开散列

【1】闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。

线性探测

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

插入

  1. 通过哈希函数获取待插入元素在哈希表中的位置
  2. 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,
  3. 使用线性探测找到下一个空位置,插入新元素

如上图插入44时,4位置已满,便顺延后位空位填入。

但是这里就涉及到一个问题,删除:
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影
响。因此线性探测采用标记的伪删除法来删除一个元素。

后续实现的时候采用这样的方式:

// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};

上文我们也看了,如果负载因子超过阈值的时候需要扩容,那么,什么时候需要扩容呢??

线性探测优点:实现非常简单,
线性探测缺点一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降
低。
如何缓解呢?

这我们就引入了二次探测法。

二次探测(线性基础上了解)

二次探测法在发生冲突时,从发生冲突的位置开始,按照二次方的步长,向右进行跳跃式探测,直至找到下一个未存储数据的位置

本质是通过平方运算构造伪随机探测序列:Hₗ = (H₀ + c₁i + c₂i²) % key
当c₁=0, c₂=1时为标准二次探测,避免线性探测的算术序列

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任
何一个位置都不会被探查两次。

因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容

【2】开散列

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中。

它的核心思路是:用数组 + 链表(或其他动态结构)的组合,让冲突元素 “链” 在一起,既简单又高效。

插入元素时:

  1. 通过哈希函数计算 key 的哈希值,确定要放入数组的哪个 “桶”(即:数组索引 )
  2. 若该桶对应的链表为空,直接插入
  3. 若已存在元素(发生冲突 ),就把新元素追加到链表末尾

查找/删除元素时:

  1. 先通过哈希函数找到对应桶
  2. 再遍历链表逐个匹配 key

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

桶的扩容

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可
能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希
表进行增容,那该条件怎么确认呢?

开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。

开散列的思考

1. 只能存储key为整形的元素,其他类型怎么解决?

我们需要为哈希表增加一个 仿函数(也叫哈希函数对象 ),该仿函数的作用是,把 key 转换成一个可用于取模的整数。

1.若 key 本身能较方便地转换为整数,且转换后不易引发哈希冲突,直接使用哈希表默认的仿函数参数即可

2.若 key 无法直接转换为整数,就需要我们自行实现一个仿函数,并传递给哈希表

实现这类仿函数的核心要求是:让 key 的每个部分(字符、字段等)都参与计算,尽可能保证不同 key 转换后的整数值互不相同

// 哈希函数采用处理余数法,被模的key必须要为整形才可以处理,此处提供将key转化为整形的方法
// 整形数据不需要转化
template<class T>
class DefHashF
{
public:
    size_t operator()(const T& val)
   {
        return val;
}
};
// key为字符串类型,需要将其转化为整形
class Str2Int
{
public:
    size_t operator()(const string& s)
   {
        const char* str = s.c_str();
        unsigned int seed = 131; // 31 131 1313 13131 131313
        unsigned int hash = 0;
        while (*str)
       {
            hash = hash * seed + (*str++);
       }
        
        return (hash & 0x7FFFFFFF);
   }
};
// 为了实现简单,此哈希表中我们将比较直接与元素绑定在一起
template<class V, class HF>
class HashBucket
{
    // ……
private:
    size_t HashFunc(const V& data)
   {
        return HF()(data.first)%_ht.capacity();
   }
};

2.除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?

第一种:直接查表

// 预计算适合哈希表扩容的素数序列(近似2倍递增)
const std::vector<size_t> prime_table = {
    53,         // ≈2^6
    97,         // ≈2^7 + 25
    193,        // ≈2^8 - 63
    389,        // ≈2^9 - 123
    769,        // ≈2^10 - 255
    1543,       // ≈2^11 + 23
    3079,       // ≈2^12 - 57
    6151,       // ≈2^13 - 217
    12289,      // ≈2^14 + 1 (已知的费马素数)
    24593,      // ≈2^15 - 79
    49157,      // ≈2^16 - 107
    98317,      // ≈2^17 - 307
    196613,     // ≈2^18 - 395
    393241,     // ≈2^19 - 87
    786433,     // ≈2^20 - 255
    1572869,    // ≈2^21 - 155
    3145739,    // ≈2^22 - 869
    6291469,    // ≈2^23 - 211
    12582917,   // ≈2^24 - 107
    25165843,   // ≈2^25 - 109
    50331653,   // ≈2^26 - 11
    100663319,  // ≈2^27 - 9
    201326611,  // ≈2^28 - 397
    402653189,  // ≈2^29 - 3
    805306457,  // ≈2^30 - 167
    1610612741, // ≈2^31 - 27
};

第二种:动态查找

size_t GetNextPrime(size_t prime)
 {
 const int PRIMECOUNT = 28;
 static const size_t primeList[PRIMECOUNT] =
 {
 53ul, 97ul, 193ul, 389ul, 769ul,
 1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
 49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
 1572869ul, 3145739ul, 6291469ul, 12582917ul, 
25165843ul,
 50331653ul, 100663319ul, 201326611ul, 402653189ul, 
805306457ul,
 1610612741ul, 3221225473ul, 4294967291ul
 };
 size_t i = 0;
 for (; i < PRIMECOUNT; ++i)
 {
 if (primeList[i] > prime)
 return primeList[i];
 }return primeList[i];
 }

四、方法实现

线性探测法

1.节点设置

enum State{EMPTY, EXIST, DELETE};
//节点结构
template<class T,class V>
struct Node
{
	//数据
	pair<T, V> dict;
	//状态
	enum State state = EMPTY;
};

2.哈希结构

//Ha_Sh结构
template<class T,class V>
class Ha_Sh
{
public:
 
	Ha_Sh()
	{
		_table.resize(10);
	}
 
 
private:
	//哈希存储
	vector<Node<T, V>> _table;
	//数据有效个数
	int size = 0;
};

3.插入

//计算插入下标
int val = date.first % _table.size();

int sum = date.first;
//循环找位置
while (_table[val].state == existence)
{
	//更新下标
	val = (++sum) % _table.max_size();
}
//插入
_table[val].dict = date;
_table[val].state = EXIST;
size++;

//计算负载因子(0.75)
double factor = (double)size / _table.size();
//准备扩容
if (factor > 0.75)
{
	vector<Node<T, V>> table(2 * _table.size());
	//转移元素
	for (int i = 0; i < _table.size(); i++)
	{
		if (_table[i].state == EXIST)
		{
			table[i] = _table[i];
		}
	}
	_table.swap(table);
}

//取整
template<class T>
struct HaShiFunc
{
	//如果是整型
	const size_t operator()(const T& date)
	{
		return size_t(date);
	}
};
//字符型(特化)
template<>
struct HaShiFunc<string>
{
	const size_t operator()(const string& date)
	{
		size_t _date = 0;
		for (auto e : date)
		{
           //去重算法
			_date *= 131;
			_date += e;
		}
		return _date;
	}
};

4.查找

查找我们暂时返回当前位置的哈希数据结构(值+状态)

注意:可以把 Key 值封装一下,防止修改

查找思路:根据映射下标去绕圈似的查找,如果找到空状态依旧没有找到就返回空指针

V* find(const T& key) {
    size_t index = find_index(key);
    if (index == _table.size()) {
        return nullptr;
    }
    return &_table[index].data.second;
}

5.删除

bool erase(const T& key) {
    size_t index = find_index(key);
    if (index == _table.size()) {
        return false;
    }

    _table[index].state = DELETE;
    _size--;
    return true;
}

总代码:test.h

#define _CRT_SECURE_NO_WARNINGS
#include <vector>
#include <utility>
#include <string>
#include <algorithm>

// 节点状态枚举
enum State { EMPTY, EXIST, DELETE };

// 哈希节点结构
template<class T, class V>
struct HashNode {
    std::pair<T, V> data;
    State state = EMPTY;
};

// 哈希函数对象
template<class T>
struct HashFunc {
    size_t operator()(const T& key) const {
        return static_cast<size_t>(key);
    }
};

// 字符串特化版本
template<>
struct HashFunc<std::string> {
    size_t operator()(const std::string& key) const {
        size_t hash = 0;
        for (char ch : key) {
            hash = hash * 131 + ch;
        }
        return hash;
    }
};

// 哈希表主体
template<class T, class V, class Hash = HashFunc<T>>
class HashTable {
public:
    HashTable(size_t initial_size = 10) : _table(initial_size), _size(0) {}

    bool insert(const std::pair<T, V>& data) {
        // 检查是否需要扩容
        if (load_factor() > 0.75) {
            rehash();
        }

        size_t index = hash_index(data.first);
        size_t start = index;
        size_t probe_count = 1;

        // 线性探测
        while (_table[index].state == EXIST) {
            // 键已存在,插入失败
            if (_table[index].data.first == data.first) {
                return false;
            }

            // 二次探测:index = (start + probe_count * probe_count) % _table.size();
            // 这里使用线性探测
            index = (index + 1) % _table.size();
            probe_count++;

            // 防止无限循环
            if (index == start) {
                return false;
            }
        }

        // 插入数据
        _table[index].data = data;
        _table[index].state = EXIST;
        _size++;
        return true;
    }

    bool erase(const T& key) {
        size_t index = find_index(key);
        if (index == _table.size()) {
            return false;
        }

        _table[index].state = DELETE;
        _size--;
        return true;
    }

    V* find(const T& key) {
        size_t index = find_index(key);
        if (index == _table.size()) {
            return nullptr;
        }
        return &_table[index].data.second;
    }

    size_t size() const { return _size; }
    bool empty() const { return _size == 0; }

private:
    std::vector<HashNode<T, V>> _table;
    size_t _size;

    double load_factor() const {
        return static_cast<double>(_size) / _table.size();
    }

    size_t hash_index(const T& key) const {
        Hash hash_func;
        return hash_func(key) % _table.size();
    }

    size_t find_index(const T& key) const {
        size_t index = hash_index(key);
        size_t start = index;

        do {
            if (_table[index].state == EXIST && _table[index].data.first == key) {
                return index;
            }

            if (_table[index].state == EMPTY) {
                break;
            }

            index = (index + 1) % _table.size();
        } while (index != start);

        return _table.size(); // 未找到
    }

    void rehash() {
        size_t new_size = get_next_prime(_table.size() * 2);
        std::vector<HashNode<T, V>> new_table(new_size);

        for (size_t i = 0; i < _table.size(); ++i) {
            if (_table[i].state == EXIST) {
                size_t new_index = HashFunc<T>()(_table[i].data.first) % new_size;

                // 处理新表中的冲突
                while (new_table[new_index].state == EXIST) {
                    new_index = (new_index + 1) % new_size;
                }

                new_table[new_index] = _table[i];
            }
        }

        _table.swap(new_table);
    }

    size_t get_next_prime(size_t prime) const {
        static constexpr size_t prime_list[] = {
            53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
            49157, 98317, 196613, 393241, 786433, 1572869, 3145739,
            6291469, 12582917, 25165843, 50331653, 100663319,
            201326611, 402653189, 805306457, 1610612741, 3221225473
        };

        auto it = std::upper_bound(std::begin(prime_list), std::end(prime_list), prime);
        return it != std::end(prime_list) ? *it : prime_list[std::size(prime_list) - 1];
    }
};

链地址法

1.节点结构

//节点结构
template<class T, class V>
struct Node
{
	Node(const pair<T, V>& date)
		:dict(date)
	{ }
	//数据
	pair<T, V> dict;
	//下一个节点
	Node<T, V>* _next = nullptr;
};

2.哈希结构

//Ha_Sh结构
template<class T, class V, class HashFunc = HaShiFunc<T>>
class Ha_Sh
{
public:
	typedef Node<const T, V> Node;
 
	Ha_Sh()
	{
		_table.resize(5);
	}
	~Ha_Sh()
	{
		Node* cur = nullptr;
		Node* next = nullptr;
		for (int i = 0; i < _table.size(); i++)
		{
			cur = _table[i];
			while (cur)
			{
				next = cur->_next;
				//释放cur
				delete cur;
				cur = next;
			}
			_table[i] = nullptr;
		}
        size = 0;
	}
 
private:
	//哈希存储
	vector<Node*> _table;
	//数据有效个数
	size_t size = 0;
};

3.插入

//插入
bool insert(const pair<T, V>& date)
{
	HashFunc Hash;
	//负载因子是否为1
	if (size == _table.size())
	{
		vector<Node*> newtable(_table.size() * 2, nullptr);
		Node* cur = nullptr;
		Node* next = nullptr;
		//重新映射
		for (int i = 0; i < _table.size(); i++)
		{
			cur = _table[i];
			while (cur)
			{
				next = cur->_next;
				size_t val = Hash(cur->dict.first) % newtable.size();
				//头插
				cur->_next = newtable[val];
				newtable[val] = cur;
				//下一个节点
				cur = next;
			}
			_table[i] = nullptr;
		}
		_table.swap(newtable);
	}
	//计算下标
	size_t val = Hash(date.first) % _table.size();
	Node* _date = new Node(date);
	//头插
	_date->_next = _table[val];
	_table[val] = _date;
	size++;
	return true;
}

4.查找

//查找
Node* Find(const T& date)
{
	if (size == 0)
	{
		return nullptr;
	}
    //计算下标
	size_t val = Hash(date) % _table.size();
    //遍历链表
	Node* cur = _table[val];
	while (cur)
	{
		if (cur->dict.first == date)
		{
			return cur;
		}
		cur = cur->_next;
	}
	return nullptr;
}

5.删除

//删除
bool Erase(const T& date)
{
	Node* cur = Find(date);
	if (cur)
	{
		HashFunc Hash;
		size_t val = Hash(date) % _table.size();
		Node* sum = _table[val];
		for (int i = 0; i < _table.size(); i++)
		{
			//如果删的是头
			if (cur == sum)
			{
				_table[i] = cur->_next;
				break;
			}
			//删中间节点
			while (sum->_next && sum->_next->dict.first != date)
			{
				sum = sum->_next;
			}
			if (sum->_next && sum->_next->dict.first == date)
			{
				sum->_next = cur->_next;
				break;
			}
		}
		delete cur;
		size--;
		return true;
	}
	return false;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值