哈希及模拟实现

news2024/11/26 11:31:53

文章目录

  • 哈希
    • 1. 哈希相关概念
      • 1.1 哈希概念
      • 1.2 哈希冲突
      • 1.3 哈希函数
      • 1.4 哈希冲突解决
        • 1.4.1 闭散列/开放定址法
          • (1)线性探测
          • (2) 二次探测
        • 1.4.2 开散列/哈希桶
    • 2. 开放定址法的实现
      • 2.1 结构
      • 2.2 插入Insert
        • 2.2.1 传统写法
        • 2.2.2 现代写法
      • 2.3 查找Find
      • 2.4 删除Erase
      • 2.5 整体代码
    • 3. 哈希桶法的实现
      • 3.1 结构
      • 3.2 插入Inert
        • 析构函数
        • 插入代码
      • 3.3 查找Find
      • 3.4 删除Erase
      • 3.5 添加仿函数
      • 3.6 除留余数法的扩容
      • 3.7 计算最大桶
      • 3.8 性能方面测试
      • 3.9 整体代码
    • 4. 开放定址法 VS 哈希桶法

哈希

1. 哈希相关概念

1.1 哈希概念

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

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

当向该结构中:

  • 插入元素
    根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
  • 搜索元素
    对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

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

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

在这里插入图片描述

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

1.2 哈希冲突

对于两个数据元素的关键字 k i k_i ki k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,但有:Hash( k i k_i ki) ==Hash( k j k_j kj),即: 不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
发生哈希冲突该如何处理呢?

1.3 哈希函数

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

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

常见哈希函数

1. 直接定址法–(常用)

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况,分布集中

2. 除留余数法–(常用)

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key % p(p<=m), 将关键码转换成哈希地址。
使用场景:数据范围不集中, 分布分散

1.4 哈希冲突解决

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

1.4.1 闭散列/开放定址法

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

(1)线性探测

比如1.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

  • 插入

    • 通过哈希函数获取待插入元素在哈希表中的位置

    • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素

    在这里插入图片描述

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

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

(2) 二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: H i H_i Hi = ( H 0 H_0 H0 + i 2 i^2 i2 )% m, 或者: H i H_i Hi = ( H 0 H_0 H0 - i 2 i^2 i2 )% m。其中:i =1,2,3…, H 0 H_0 H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

在这里插入图片描述

对于1.1中如果要插入44,产生冲突,使用解决后的情况为:

在这里插入图片描述

因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷

1.4.2 开散列/哈希桶

开散列概念

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

在这里插入图片描述

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

2. 开放定址法的实现

2.1 结构

实现具体函数之前,我们先根据上面对开放定址法介绍,写出此结构的大框架。

  1. 首先我们要将具体数据存储在哈希表中,存储的是一个pair类型的键值对,同时要考虑删除数据后的处理,是要置空吗?不是,置空的话会影响查找(查找到空就结束),所以必须要定义一个枚举类型的状态值来表示该元素在哈希表中的状态;将这两项封装成HashData的类,来表示开放定址法中的存储节点。
  2. 需要一个哈希表里面来存储数据,并且需要一个变量来表示存储数据的有效个数;那么这个哈希表怎么实现呢?可以像顺序表部分一样给成:节点的指针,size和capacity形式,但是考虑到后续哈希表存满情况下要扩容时,这样实现就比较麻烦;更好的方式是直接给一个vector里面保存节点的结构
enum State
{
    EMPTY,
    EXIST,
    DELETE
};

template <class K, class V>
struct HashData
{
    pair<K, V> _kv;
    State _state = EMPTY;
};

template <class K, class V>
class HashTable
{
public:
private:
    // 可以实现成原始的方式: HashTable* tables, size, capacity
    // 直接用vector实现
    vector<HashData<K, V>> _tables;
    size_t _n = 0; // 存储的数据有效个数
};

2.2 插入Insert

对于插入来说,我们这里使用的是线性探测法,那就意味着会发生冲突,从发生冲突的位置开始,依次向后++,直到寻找到下一个空位置为止;此时还会引发另一个问题,若空间不足了,需要扩容,但是对于哈希表并不能直接等到空间存满了才扩容,一旦达到某个程度,就会因哈希冲突出现造成效率下降的问题,为了解决这类问题我们引入了一个负载因子(载荷因子)的概念,负载因子 = 表中元素个数 / 散列长度

  • 负载因子越大,冲突的概率越高,查找效率越低,空间利用率越高
  • 负载因子越小,冲突的概率越低,查找效率越高,空间利用率越低

负载因子的出现是一种以空间换时间的方法,一般将负载因子限制在0.7左右,超过了这个值就需要扩容了

Insert有两种写法:

  1. 给一个新表newtables,遍历旧表将元素重新映射到新表,最后交换新旧表。但是这个过程的代码与将元素映射到旧表的代码相 同,出现了代码冗余,可以将这部分代码封装成一个类的成员函数,直接调用这个函数

  2. 重新创建一个HashTable对象, 直接去复用Insert,最后交换新旧表

2.2.1 传统写法

我这里没有封装成员函数,是直接写的,代码冗余

bool Insert(const pair<K, V> &kv)
{
	//提前处理一下表为0的情况
    if (_tables.size() == 0)
    {
        _tables.resize(10);    //一定要使用resize, reserve只会改变capacity,不会改变size
    }

    // 当前表为0直接除时, 会出现除0错误,所以上面有提前处理
    if (_n * 10 / _tables.size() >= 7)    //这里也可以考虑强转成double
    {
        size_t newsize = _tables.size() * 2;

        // 重新开一个新表
        vector<HashData<K, V>> newtables(newsize);    

        // 遍历旧表, 重新映射到新表
        for (auto &data : _tables)
        {
            if (data._state == EXIST)
            {
                // 重新计算在新表的位置
                int hashi = data._kv.first % newtables.size();
                size_t i = 1;
                size_t index = hashi;
                while (newtables[index]._state == EXIST)
                {
                    index = hashi + i; 
                    index %= newtables.size();
                    ++i;
                }
                newtables[index]._kv = data._kv;
                newtables[index]._state = EXIST;
            }
        }
        _tables.swap(newtables); 
    }

    size_t hashi = kv.first % _tables.size();

    // 线性探测
    size_t i = 1;
    size_t index = hashi;
    while (_tables[index]._state == EXIST)
    {
        index = hashi + i;         
        index %= _tables.size();
        ++i;
    }

    // vector的[]会检查下标是否小于capacity
    _tables[index]._kv = kv;
    _tables[index]._state = EXIST;
    ++_n;

    return true;
}

2.2.2 现代写法

bool Insert(const pair<K, V> &kv)
{
    if (Find(kv.first)) //元素已经存在, 就不要插入了
    {
        return false;
    }

    // 负载因子 = 表中元素个数 / 散列长度
    // 这里控制负载因子超过0.7就扩容

    // 当前表为0直接除时, 会出现除0错误
    if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7) //这里也可以考虑强转成double
    {
        // 1. 表为空,扩不上去
        // 2. reserve改变vector的capacity,size不变
        // v.reserve(_tables.capacity() * 2);

        size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;

        // 采用复用的方式,当然也可以考虑把代码封装成类
        HashTable<K, V> newht; //重新创建一个对象
        newht._tables.resize(newsize);

        // 这里逻辑是: 先重新映射旧表的关系, 映射完后交换, 再去插入新的元素
        for (auto &data : _tables)
        {
            if (data._state == EXIST)
            {
                newht.Insert(data._kv);
            }
        }
        _tables.swap(newht._tables); // 底层交换的是vector的3个指针
    }

    size_t hashi = kv.first % _tables.size();

    // 线性探测
    size_t i = 1;
    size_t index = hashi;
    while (_tables[index]._state == EXIST)
    {
        index = hashi + i;          //  hashi + i*i;  二次探测
        index %= _tables.size();
        ++i;
    }

    //vector的[]会检查下标是否小于capacity
    _tables[index]._kv = kv;
    _tables[index]._state = EXIST;
    ++_n;

    return true;
}

Insert执行逻辑,最好调试观察一下

在这里插入图片描述

2.3 查找Find

查找时按照Key值去查找,逻辑是: 如果当前表为空就直接返回nullptr;在当前位置状态不为空的前提下,如果此位置元素状态是存在且刚好等于key,则返回此位置的地址,否则返回nullptr,但是还存在这样一种极端情况,所以在处理时如果已经查找了一圈, 那么说明全是存在 + 删除,直接跳出循环返回nullptr

在这里插入图片描述

HashData<K, V> *Find(const K &key)
{
    if (_tables.size() == 0)
        return nullptr;

    size_t hashi = key % _tables.size();

    // 线性探测
    size_t i = 1;
    size_t index = hashi;
    while (_tables[index]._state != EMPTY)
    {
        if (_tables[index]._state == EXIST && _tables[index]._kv.first == key)
        {
            return &_tables[index];
        }
        index = hashi + i;
        index %= _tables.size();
        ++i;

        // 如果已经查找了一圈, 那么说明全是存在 + 删除
        if (index == hashi)
        {
            break;
        }
    }
    return nullptr;
}

2.4 删除Erase

删除这里采用伪删除法,即并不是真正意义上的删除,而只是将这个位置对应的状态改为DELETE

删除的逻辑是: 先查找,如果此元素在表中, 则将这个位置对应状态改为DELETE,表中元素个数减1;如果此元素不在表中,则直接返回false

// 伪删除法: 没有真正地把这个数据删除, 只是把这位置对应的状态标记为删除
bool Erase(const K &key)
{
    HashData<K, V> *ret = Find(key);   //找的是地址
    if (ret)
    {
        ret->_state = DELETE;
        --_n;
        return true;
    }
    else
    {
        return false;
    }
}

2.5 整体代码

namespace OpenAddress
{
	enum State
	{
		EMPTY,
		EXIST,
		DELETE
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state=EMPTY;
	};

	template<class K,class V>
	class HashTable
	{
	public:

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))  //元素已经存在, 就不要插入了
			{
				return false;
			}

			//负载因子 = 表中元素个数 / 散列长度
			//这里控制负载因子超过0.7就扩容

			//当前表为0直接除时, 会出现除0错误
			if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)    
			{
				//1. 表为空,扩不上去
				//2. reserve改变vector的capacity,size不变
				//v.reserve(_tables.capacity() * 2);

				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;

				//采用复用的方式,当然也可以考虑把代码封装成类
				HashTable<K, V> newht;		//重新创建一个对象
				newht._tables.resize(newsize);  

				//这里逻辑是: 先重新映射旧表的关系, 映射完后交换, 再去插入新的元素
				for (auto& data : _tables)
				{
					if (data._state == EXIST)
					{
						newht.Insert(data._kv);
					}
				}
				_tables.swap(newht._tables);    //底层交换的是vector的3个指针
			}


			size_t hashi = kv.first % _tables.size();

			//线性探测
			size_t i = 1;
			size_t index = hashi;
			while (_tables[index]._state == EXIST)
			{
				index = hashi + i;			 //hashi + i*i;  二次探测
				index %= _tables.size();
				++i;
			}

			//vector的[]会检查下标是否小于capacity
			_tables[index]._kv = kv;
			_tables[index]._state = EXIST;
			++_n;

			return true;
		}

		HashData<K, V>* Find(const K& key)
		{
			if (_tables.size() == 0)
				return nullptr;

			size_t hashi = key % _tables.size();

			//线性探测
			size_t i = 1;
			size_t index = hashi;
			while (_tables[index]._state != EMPTY)
			{
				if (_tables[index]._state == EXIST
					&& _tables[index]._kv.first == key)
				{
					return&_tables[index];
				}
				index = hashi + i;			
				index %= _tables.size();
				++i;

				//如果已经查找了一圈, 那么说明全是存在 + 删除
				if (index == hashi)
				{
					break;
				}
			}
			return nullptr;
		}

		//伪删除法: 没有真正地把这个数据删除, 只是把这位置对应的状态标记为删除
		bool Erase(const K& key)
		{
			HashData<K, V>* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;
				--_n;
				return true;
			}
			else
			{
				return false;
			}
		}
	private:
		//可以实现成原始的方式: HashTable* tables, size, capacity
		//直接用vector实现
		vector<HashData<K,V>> _tables;
		size_t _n=0;   //存储的数据个数
	};
}

3. 哈希桶法的实现

3.1 结构

实现具体函数之前,我们先根据上面对哈希桶法介绍,写出此结构的大框架。

首先哈希桶法不像开放定址法需要给出节点的状态值,它是来了一个元素就挂在当前桶对应的位置下面,所以需用当前节点的指针,所以HashNode中存储的是节点指针和键值对;同时vector中里面不在是这个节点本身了,而是以指针的形式保存这个节点的地址,是一种指针数组类型,就像数组里面挂链表的形式,然后这个哈希桶类需要在添加一个模板参数,稍后介绍。

template<class K, class V>
struct HashNode
{
    HashNode<K, V> *_next;
    pair<K, V> _kv;

    HashNode(const pair<K, V> &kv)
        : _next(nullptr), _kv(kv)
    {
    }
};
template <class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
    typedef HashNode<K, V> Node;
private:
    vector<Node*> _tables; //vector里面挂节点(链表),是指针数组类型
    size_t _n = 0;          //存储有效数据个数
};

3.2 插入Inert

由于是数组挂链表的形式,实现的是链表的插入所以采用头插,虽然这里并不会存在像开放定址法一样所有的桶都挂满了,看起来每个桶可以无限的向后挂元素,但是一旦桶中链表节点非常多还是会影响哈希表的性能,所以还是要对哈希表进行增容。开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以对哈希表增容

Insert的逻辑: 向vector中挂数组的思路,就是构建当前的映射位置,在此位置上头插节点就行。

重点来看扩容的思路:

  • 扩容时我们可以像开放定址法的现代写法一样创建一个新的HashTable,完成代码复用交换新旧表,对于这种扩容方式,实际上很浪费空间,因其在插入过程中都是在拷贝节点,在一定程度上浪费了空间,并且在析构时会析构两次,又是一次性能的缺失。
  • 优化: 我们可以将旧表中的节点头插到新表指定的映射位置, 这样就不需要拷贝创建新节点,但这样需要注意的是:要将旧表的每一个元素:_tables[i]清理掉或者都置nullptr,防止同一块空间析构两次造成浅拷贝。

因为是链表结构,默认生成的析构函数会释放掉vecto本身,但是不能将链表的节点释放,所以我们需要自己写一个析构函数将链表节点的空间释放。

HashTable<K, V> newht;
newht._tables.resize(newsize);
for (auto cur : _tables)
{
    while (cur)
    {
        newht.Insert(cur->_kv);
        cur = cur->_next;
    }
}
_tables.swap(newht._tables);

析构函数

~HashTable()   //保存下一个位置,释放当前位置
{
    for (auto &cur : _tables)
    {
        while (cur)
        {
            Node *next = cur->_next;
            delete cur;
            cur = next;
        }
        cur = nullptr;
    }
}

插入代码

bool Insert(const pair<K, V> &kv)
{
    if (Find(kv.first))  //元素已经存在, 就不要插入了
    {
        return false;
    }

    // 负载因子等于1时扩容   --- 还是实现成传统写法
    if (_n == _tables.size())
    {
        size_t newsize =_tables.size() == 0 ? 10 : _tables.size() * 2;

        vector<Node *> newtables(newsize, nullptr);

        for (auto &cur : _tables)
        {
            while (cur)
            {
                Node *next = cur->_next;

                // 需要重新计算映射关系
                size_t hashi = kv.first % _tables.size();

                // 头插到新表
                cur->_next = _tables[hashi];
                _tables[hashi] = cur;
                cur = next;
            }
        }
        _tables.swap(newtables);
    }

    size_t hashi = kv.first % _tables.size();

    // 直接插入就行 —— 这里用头插
    Node *newnode = new Node(kv);
    newnode->_next = _tables[hashi];
    _tables[hashi] = newnode;

    ++_n;
    return true;
}

3.3 查找Find

与开放定址法思路相同,只是这里走链表节点去查找的

Node *Find(const K &key)
{
    if (_tables.size() == 0)
        return nullptr;

    size_t hashi = key % _tables.size();
    Node *cur = _tables[hashi];
    while (cur)
    {
        if (cur->_kv.first == key)
        {
            return cur;
        }
        cur = cur->_next;
    }
    return nullptr;
}

3.4 删除Erase

就是一个单链表头删的过程

bool Erase(const K &key)  //从链表中删除
{
    size_t hashi = key % _tables.size();
    Node *cur = _tables[hashi];
    Node *prev = nullptr; //保存前一个节点

    while (cur) //链表头删的过程
    {
        if (cur->_kv.first == key)
        {
            if (prev == nullptr)
            {
                _tables[hashi] = cur->_next;
            }
            else
            {
                prev->_next = cur->_next;
            }

            delete cur;
            return true;
        }
        else
        {
            prev = cur;
            cur = cur->_next;
        }
    }
    return false;
}

3.5 添加仿函数

上面的代码几个重要接口已经写完了,我们向其中插入<int,int>这样的键值对完全是没有问题的,但是一旦想要向之前map和set中一样统计水果次数这个代码就运行不起来了,原因是插入的是<string,int>这样的键值对,string无法转换成整型,没有办法取模构建映射关系,所以我们要提供一个仿函数把字符串转整型,同时我们在自己使用时不需要显示传这个仿函数,因此我们可以考虑模板的特化,整型还是整型,参数是string就转成整型,这样就保证了整型系列本身可以转换,字符串通过模板的特化转换。

我们要这样写时,要在上面几个接口取模操作时,外面套一层仿函数对象,具体请看整体代码

字符串仿函数逻辑: 定义一个int变量,每次去字符串元素加到这个变量中,同时这个变量乘31。

参考: 各种字符串Hash函数 - clq - 博客园 (cnblogs.com)

// 整型系列本身可以转换
template <class K>
struct HashFunc
{
    size_t operator()(const K &key)
    {
        return key;
    }
};

// 模板特化  --- 实现字符串比较不用自己显示传仿函数, 和库里保持一致
template <>
struct HashFunc<string>
{
    // BKDR算法
    size_t operator()(const string &s)
    {
        size_t hash = 0;
        for (auto ch : s)
        {
            hash += ch;
            hash *= 31;
        }
        return hash;
    }
};

3.6 除留余数法的扩容

对于哈希来说SGI版本采用了这种方式扩容避免哈希冲突

// size_t newsize = GetNextPrime(_tables.size());
size_t GetNextPrime(size_t prime)
{
    // SGI  --- 除留余数法模素数
    static const int __stl_num_primes = 28;
    static const unsigned long __stl_prime_list[__stl_num_primes] =
        {
            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, 4294967291};

    size_t i = 0;
    for (; i < __stl_num_primes; ++i)
    {
        if (__stl_prime_list[i] > prime)
            return __stl_prime_list[i];
    }

    return __stl_prime_list[i];
}

3.7 计算最大桶

直接遍历更新这个哈希表计算出所挂节点最长的桶即可,定义一个变量遍历的过程去比较更新

// 最大桶
size_t MaxBucketSize()
{
    size_t max = 0;

    for (int i = 0; i < _tables.size(); ++i)
    {
        Node *cur = _tables[i];
        size_t size = 0;
        while (cur)
        {
            ++size;
            cur = cur->_next;
        }

        printf("[%d]->[%d]\n", i, size);
        if (size > max)
        {
            max = size;
        }
    }
    return max;
}

3.8 性能方面测试

void TestHashTable4()
{
	size_t N = 100000;
	HashBucket::HashTable<int, int> ht;
	srand(time(0));
	for (size_t i = 0; i < N; ++i)
	{
		size_t x = rand() + i;
		ht.Insert(make_pair(x, x));
	}

	cout << ht.MaxBucketSize() << endl;
}

运行结果:

在这里插入图片描述

我们可以看到哈希表的性能还是很好的,增删查改时间复杂度: O(1)。虽然说哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

哈希函数提高效率方式:

  1. 负载因子的控制
  2. 单个桶超过一定长度,这个桶改挂红黑树

3.9 整体代码

namespace HashBucket
{

	template<class K, class V>
	struct HashNode
	{
		HashNode<K, V>* _next;
		pair<K, V> _kv;

		HashNode(const pair<K, V>& kv)
			:_next(nullptr)
			,_kv(kv)
		{
		}
	};

	//整型系列本身可以转换
	template<class K>
	struct HashFunc
	{
		size_t operator()(const K& key)
		{
			return key;
		}
	};

	//模板特化  --- 实现字符串比较不用自己显示传仿函数, 和库里保持一致
	template<>
	struct HashFunc<string>
	{
		//BKDR算法
		size_t operator()(const string& s)
		{
			size_t hash = 0;
			for (auto ch : s)
			{
				hash += ch;
				hash *= 31;
			}
			return hash;
		}
	};

	template<class K,class V, class Hash= HashFunc<K>>
	class HashTable
	{
	public:
		typedef HashNode<K, V> Node;

		//size_t newsize = GetNextPrime(_tables.size());
		size_t GetNextPrime(size_t prime)
		{
			// SGI  --- 除留余数法模素数
			static const int __stl_num_primes = 28;
			static const unsigned long __stl_prime_list[__stl_num_primes] =
			{
				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, 4294967291
			};

			size_t i = 0;
			for (; i < __stl_num_primes; ++i)
			{
				if (__stl_prime_list[i] > prime)
					return __stl_prime_list[i];
			}

			return __stl_prime_list[i];
		}

		~HashTable()			//保存下一个位置,释放当前位置
		{
			for (auto&cur : _tables)
			{
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}

				cur = nullptr;
			}
		}


		Node* Find(const K& key)
		{
			Hash hash;
			if (_tables.size() == 0)
				return nullptr;

			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}


		bool Erase(const K& key)    //从链表中删除
		{
			Hash hash;		//这是仿函数需要的
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;       //保存前一个节点

			while (cur)					//链表头删的过程
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}

					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
			{
				return false;
			}

			Hash hash;

			//负载因子等于1时扩容
			if (_n == _tables.size())
			{
				//size_t newsize =_tables.size() == 0 ? 10 : _tables.size() * 2;
				size_t newsize = GetNextPrime(_tables.size());    //模素数

				vector<Node*> newtables(newsize, nullptr);

				for (auto& cur : _tables)
				{
					while (cur)
					{
						Node* next = cur->_next;

						//需要重新计算映射关系
						size_t hashi = hash(kv.first) % _tables.size();
						
						//头插到新表
						cur->_next = _tables[hashi];
						_tables[hashi] = cur;
						cur = next;
					}
				}
				_tables.swap(newtables);
			}

			size_t hashi = hash(kv.first) % _tables.size();

			//直接插入就行 —— 这里用头插
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			
			++_n;
			return true;
		}

		//最大桶
		size_t MaxBucketSize()
		{
			size_t max = 0;

			for (int i = 0; i < _tables.size(); ++i)
			{
				Node* cur = _tables[i];
				size_t size = 0;
				while (cur)
				{
					++size;
					cur = cur->_next;
				}

				printf("[%d]->[%d]\n", i, size);
				if (size > max)
				{
					max = size;
				}
			}
			return max;
		}

	private:
		vector<Node*> _tables;   //vector里面挂节点(链表),是指针数组类型
		size_t _n=0;                //存储有效数据个数
	};
}

4. 开放定址法 VS 哈希桶法

应用哈希桶法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:由于开放地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <=0.7,而表项所占空间又比指针大的多,所以使用哈希桶法反而比开地址法节省存储空间。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/538767.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springcloud基于web的智慧养老平台

系统分析 可行性分析 在开发系统之前要进行系统可行性分析&#xff0c;目的是在用最简单的方法去解决最大的问题&#xff0c;程序一旦开发出来满足了用户的需要&#xff0c;所带来的利益也很多。下面我们将从技术、操作、经济等方面来选择这个系统最终是否开发。 1、技术可行…

一图看懂 dateutil 模块:Python datetime 模块的扩展,资料整理+笔记(大全)

本文由 大侠(AhcaoZhu)原创&#xff0c;转载请声明。 链接: https://blog.csdn.net/Ahcao2008 一图看懂 dateutil 模块&#xff1a;Python datetime 模块的扩展&#xff0c;资料整理笔记&#xff08;大全&#xff09; &#x1f9ca;摘要&#x1f9ca;模块图&#x1f9ca;类关系…

005、数据库结构

数据库结构 1、数据库集簇逻辑结构2、对象标识符3、数据库集簇物理结构4、其它目录结构表空间物理文件位置1、数据库集簇逻辑结构 • 数据库集簇逻辑结构 数据库 : 把数据逻辑分开存放。 对象是放在数据库当中。表空间: 把数据从逻辑或者物理上分割存放2、对象标识符 Postg…

Weblogic SSRF 漏洞(CVE-2014-4210)

SSRF漏洞 ​ SSRF&#xff08;服务端请求伪造&#xff09;&#xff0c;指的是攻击者在未能取得服务器所有权限时&#xff0c;利用服务器漏洞以服务器的身份发送一条构造好的请求给服务器所在内网。SSRF攻击通常针对外部网络无法直接访问的内部系统。 ​ 简单的说就是利用一个可…

《统计学习方法》——隐马尔可夫模型(下)

学习算法 HMM的学习&#xff0c;在有观测序列的情况下&#xff0c;根据训练数据是否包含状态序列&#xff0c;可以分别由监督学习算法和无监督学习算法实现。 监督学习算法 监督学习算法就比较简单&#xff0c;基于已有的数据利用极大似然估计法来估计隐马尔可夫模型的参数。…

详解二叉树

&#x1f308;目录 一、树形结构​ &#x1f333;1.1 概念1.2 其他概念1.3 树的表示形式 二、二叉树✨2.1 概念2.2 两种特殊二叉树2.3 性质2.4 二叉树存储 三、二叉树的基本操作&#x1f64c;3.1 前置说明3.2 二叉树的遍历3.3 二叉树的基本操作 四、二叉树的OJ✍️ 一、树形结构…

springboot+vue医院信管系统(源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的医院信管系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 &#x1f495;&#x1f495;作者&#xff1a;风歌&a…

Service Control Manager 服务管理器简介

在windows驱动开发流程中&#xff0c;写完sys驱动binary之后&#xff0c;为了让OS能够正确的从注册表中读取到对应的信息&#xff0c;并且将其load运行起来&#xff0c;还需要编写inf文件来描述配置驱动文件。不过这也不是必须的&#xff0c;可以通过ServiceControlManager直接…

Fiddler 抓包工具 - 全网最全最细教程,没有之一

Fiddler 简介 Fiddler 是位于客户端和服务器端的 HTTP 代理 目前最常用的 http 抓包工具之一 功能非常强大&#xff0c;是 Web 调试的利器 监控浏览器所有的 HTTP/HTTPS 流量 查看、分析请求内容细节 伪造客户端请求和服务器响应 测试网站的性能解密 HTTPS 的 Web 会话 全局…

Go语音基于zap的日志封装

zap日志封装 Zap是一个高性能、结构化日志库&#xff0c;专为Go语言设计。它由Uber开源&#xff0c;并且在Go社区中非常受欢迎。它的设计目标是提供一个简单易用、高效稳定、灵活可扩展的日志系统。 以下是Zap的一些主要特点&#xff1a; 1.高性能&#xff1a;Zap的性能非常出…

【Linux】权限的理解

&#x1f307;个人主页&#xff1a;平凡的小苏 &#x1f4da;学习格言&#xff1a;命运给你一个低的起点&#xff0c;是想看你精彩的翻盘&#xff0c;而不是让你自甘堕落&#xff0c;脚下的路虽然难走&#xff0c;但我还能走&#xff0c;比起向阳而生&#xff0c;我更想尝试逆风…

mysql exist和in的区别

一、演示用的表 为了演示二者的区别&#xff0c;先建立两张表 user 表和 order 表 二、in 的执行情况 in在查询的时候&#xff0c;首先查询子查询的表&#xff0c;然后将内表和外表做一个笛卡尔积&#xff0c;然后按照条件进行筛选。所以相对内表比较小的时候&#xff0c;…

接口测试之Jenkins+Jmeter+Ant实现持续集成

安装Jenkins&#xff0c;见手把手教小白安装Jenkins_程序员馨馨的博客-CSDN博客 一&#xff09;Linux机器上安装Jmeter 百度一下就好 二&#xff09;Linux机器上安装Ant 1、下载安装包 进入Apache Ant - Binary Distributions&#xff0c;下载安装包&#xff0c;本次安装的是版…

高仿某东商城flutter版本,个人学习flutter项目

前言 高仿某东商城flutter版本&#xff0c;个人学习flutter项目 使用flutter_redux状态管理网络使用dio进行了简单的封装使用node项目mock服务端接口(mock_server目录)目前只实现了首页&#xff0c;其他功能持续更新… 同款Android Kotlin版本&#xff08; https://github.co…

Mysql索引底层原理及其优化方案

1.深入理解Mysql索引底层数据结构与算法 1.1索引结构 索引及其数据结构&#xff1a; 二叉树红黑树Hash表B-Tree 1.1 二叉树 说明&#xff1a;二叉树是建立数据后&#xff0c;会和第一元素进行比对&#xff0c;当比较的元素小于第一个元素时&#xff0c;此时就会走第一个元素…

代码随想录算法训练营第四十三天 | 填满背包有几种方法、背包有两个维度

1049.最后一块石头的重量II 文档讲解&#xff1a;代码随想录 (programmercarl.com) 视频讲解&#xff1a;动态规划之背包问题&#xff0c;这个背包最多能装多少&#xff1f;LeetCode&#xff1a;1049.最后一块石头的重量II_哔哩哔哩_bilibili 状态&#xff1a;没想到。 思路 本…

chatgpt如何引入领域知识?mit团队利用gpt4做数据增强来提升小模型在特定领域的效果

一、概述 title&#xff1a;Dr. LLaMA: Improving Small Language Models in Domain-Specific QA via Generative Data Augmentation 论文地址&#xff1a;Paper page - Dr. LLaMA: Improving Small Language Models in Domain-Specific QA via Generative Data Augmentation…

(6)LED点阵屏

LED点阵屏由若干个独立的LED组成&#xff0c;LED以矩阵的形式排列&#xff0c;以灯珠亮灭来显示文字、图片、视频等。LED点阵屏广泛应用于各种公共场合&#xff0c;如汽车报站器、广告屏以及公告牌等 LED点阵屏分类 按颜色&#xff1a;单色、双色、全彩按像素&#xff1a;88、…

Excel模板导入导出功能测试点

近期接触的都是Web项目&#xff0c;有很多导入数据这个功能&#xff0c;导入的文件格式都是Excel&#xff0c;基本流程就是&#xff1a;下载一个Excel模板&#xff0c;填充数据&#xff0c;再将Excel表格导入&#xff0c;导入后可下载列表&#xff0c;想着这类功能的测试点基本…

springboot基于vue的地方美食分享网站

开发技术介绍 Java介绍 JavaScript是一种网络脚本语言&#xff0c;广泛运用于web应用开发&#xff0c;可以用来添加网页的格式动态效果&#xff0c;该语言不用进行预编译就直接运行&#xff0c;可以直接嵌入HTML语言中&#xff0c;写成js语言&#xff0c;便于结构的分离&…