【C++】哈希表

哈希表

  • [1. 关联式容器的对比](#1. 关联式容器的对比)
  • [2. 哈希结构](#2. 哈希结构)
    • [2.1 概念](#2.1 概念)
    • [2.2 哈希冲突](#2.2 哈希冲突)
    • [2.3 哈希函数](#2.3 哈希函数)
      • [2.3.1 哈希函数设计原则](#2.3.1 哈希函数设计原则)
      • [2.3.2 常见的哈希函数](#2.3.2 常见的哈希函数)
    • [2.4 解决哈希冲突的方法](#2.4 解决哈希冲突的方法)
      • [2.4.1 闭散列](#2.4.1 闭散列)
        • [2.4.1.1 线性探测](#2.4.1.1 线性探测)
        • [2.4.1.2 二次探测](#2.4.1.2 二次探测)
      • [2.4.2 开散列](#2.4.2 开散列)
      • [2.4.3 负载因子](#2.4.3 负载因子)
      • [2.4.4 开散列与闭散列的比较](#2.4.4 开散列与闭散列的比较)
    • [2.5 模拟实现](#2.5 模拟实现)
      • [2.5.1 非整形类型转化为整数](#2.5.1 非整形类型转化为整数)
      • [2.5.2 哈希表---闭散列模拟实现](#2.5.2 哈希表—闭散列模拟实现)
        • [2.5.2.1 哈希表结构的定义](#2.5.2.1 哈希表结构的定义)
        • [2.5.2.2 查询](#2.5.2.2 查询)
        • [2.5.2.3 插入](#2.5.2.3 插入)
        • [2.5.2.4 删除](#2.5.2.4 删除)
      • [2.5.3 哈希桶---开散列模拟实现](#2.5.3 哈希桶—开散列模拟实现)
        • [2.5.3.1 哈希表结构的定义](#2.5.3.1 哈希表结构的定义)
        • [2.5.3.2 查询](#2.5.3.2 查询)
        • [2.5.3.3 插入](#2.5.3.3 插入)
        • [2.5.3.4 删除](#2.5.3.4 删除)
  • [3. 哈希表封装unorered_map、unordered_set](#3. 哈希表封装unorered_map、unordered_set)
    • [3.1 哈希表的模拟实现](#3.1 哈希表的模拟实现)
      • [3.1.1 迭代器](#3.1.1 迭代器)
        • [3.1.1.1 构造函数](#3.1.1.1 构造函数)
        • [3.1.1.2 begin()+end()](#3.1.1.2 begin()+end())
        • [3.1.1.3 operator++()](#3.1.1.3 operator++())
        • [3.1.1.4 operator*()](#3.1.1.4 operator*())
        • [3.1.1.5 operator->()](#3.1.1.5 operator->())
        • [3.1.1.6 operator==()](#3.1.1.6 operator==())
        • [3.1.1.7 operator!=()](#3.1.1.7 operator!=())
      • [3.1.2 insert()](#3.1.2 insert())
      • [3.1.3 完整代码](#3.1.3 完整代码)
    • [4.1 unorered_set的模拟实现](#4.1 unorered_set的模拟实现)
      • [4.1.1 完整代码](#4.1.1 完整代码)
    • [5.1 unorered_map的模拟实现](#5.1 unorered_map的模拟实现)
      • [5.1.1 operator[]](#5.1.1 operator[])
      • [5.1.2 完整代码](#5.1.2 完整代码)

1. 关联式容器的对比

  1. 在C++98中,STL提供了底层为红黑树的关联式容器,如:set、map等,查询效率为O(longn),即最差情况下需要比较红黑树的高度次,当节点数目很多时,需要比较的次数多,查询效率低。在C++11中,STL提供了4个unordered系列的关联式容器,如:unordered_set、unordered_map等,底层结构为哈希表,通过存储值和存储位置的映射的关联关系,查询效率可以达到常数级别,即:O(1)。
  2. unordered_map、unordered_set底层结构为哈希结构,其容器内元素的顺序是无序的,单向迭代器。map、set底层结构为红黑树结构,其其容器内元素的顺序是有序的,双向迭代器。两者的成员函数使用方式基本相同、都有multi版本、key是唯一的。
  3. 在性能上差别:对于插入、删除操作,如果容器中元素重复值很多 或者 元素重复值不是很多时,map、set效率低于unordered_map、unordered_set。如果容器中元素无重复值时,map、set效率优于unordered_map、unordered_set。因为unordered系列容器存在哈希冲突、扩容有消耗,导致效率降低。

2. 哈希结构

2.1 概念

概念:存储值和存储位置的映射的关联关系。

  1. 顺序结构和平衡树,关键码和存储位置未建立关联关系,因此在查找元素时,关键码需要进行多次比较。顺序结构的查找效率为O(n),平衡树的查找效率为高度次O(longn)。搜索效率取决于在搜索过程中关键码比较的次数。

  2. 哈希方法的思想:在元素关键码k和元素存储位置p之间建立起一一映射关联关系H,使得p = H(k),H为哈希函数。因此在查找时,通过哈希函数(hash(key) = key%size=存储位置)的转化,使得查询效率达到常数级别,即:O(1)。

  3. 哈希方法中,使用的转换函数称为哈希函数(散列函数),构造出来的结构称为哈希表(散列表)。

2.2 哈希冲突

概念:不同的关键码通过同一哈希函数计算出相同的存储位置,称为哈希冲突或者哈希碰撞。把具有不同关键码而具有相同存储位置的数据元素称为"同义词"。

问:为什么会引起哈希冲突妮?

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

2.3 哈希函数

2.3.1 哈希函数设计原则

  1. 哈希函数的定义域必须包含需存储的全部关键码。若哈希表中有n个地址,其值域必须在[0, n-1]。

  2. 哈希函数计算出的存储位置能够均匀分布在空间内。

  3. 哈希函数比较简单。

2.3.2 常见的哈希函数

  1. 直接定址法

此处哈希函数一般为线性函数,hash(key) = A*key + B。

优点:计算简单,不会产生冲突。

缺点:需要事先知道数据的分布范围情况,若分布不连续,会造成内存空间大量浪费。

适用场景:查找数据范围比较小且连续的情况。

  1. 除留余数法

hash(key) = key % size。

优点:计算简单,适用于数据范围较广,是一种常见的哈希函数。

缺点:哈希冲突。 适用场景:整数的求模运算(int->存储位置)。

💡Tips:哈希函数设计的越精妙,哈希冲突效率就越低,搜索效率就越高,但仍无法避免哈希冲突。

2.4 解决哈希冲突的方法

2.4.1 闭散列

概念:闭散列,也称为开放定址法。若哈希表中未被装满,说明哈希表中还存在"空位置", 就可以把key放到冲突位置的下一个"空位置"处。

缺点:空间利用率较低,这也是哈希的缺陷。

  • 问:如何找下一个空位置妮?
  • 答:线性探测、二次探测等。
2.4.1.1 线性探测

概念:从冲突位置开始,依次往后进行探测,若探测到哈希表结尾位置,从头开始探测,直到找到下一个空位置为止。hash(key) = key%size + i(i>=0)。

缺点:一旦发生哈希冲突,所以的冲突堆积在一起,就会造成"数据堆积",各个数据相互影响,如同"恶性循环",即:每个关键码占据了可利用的空位置,使得找寻某个关键码时,不能直接找到,需要进行多次比较,搜索效率降低。

2.4.1.2 二次探测

概念:hash(key) = key%size + i^2(i>=0)。

2.4.2 开散列

概念:开散列,也称为开散列拉链法(开链法)、开散列哈希桶。首先先通过哈希函数计算出key在哈希表中的地址,将具有相同关键码key的元素归于同一个子集中,每个子集代表着一个桶,桶中的元素通过单链表连接起来,链表的头节点就存在哈希表中。

2.4.3 负载因子

概念:负载因子:也称为载荷因子,n = 实际存储的数据个数 / size。

负载因子越大,哈希冲突就越高,搜索效率就越低,空间利用率就越高。

对于闭散列,负载因子要控制在0.7~0.8以下,超过0.8,cpu缓存不命中按照指数曲线上升。

2.4.4 开散列与闭散列的比较

  1. 应用链地址法处理哈希冲突时,要增加链接的指针,似乎增加了存储开销。而开地址法必须保持大量空间空闲,以确保搜索效率,如:二次探测的负载因子<=0.7,而表所占的空间比指针大,所以使用开地址法反而比链地址法更节省空间。

  2. 闭散列负载因子到了0.7就需要扩容了,开散列负载因子到1就需要扩容了。

2.5 模拟实现

2.5.1 非整形类型转化为整数

  1. 哈希结构中,计算key所对应的存储位置需要用到求模运算%,但%两操作符必须为整形,对于string、自定义类型不适用,所以我们需要显示写一个仿函数,此仿函数的功能是将任意数据类型转化为整形。只需确保相同的key值转化为整形是唯一的。

  2. 仿函数hash作用:将各种数据类型K -> int->存储位置, eg : string、Data、Person -> int->存储值。 K类型有两种情况:本身为size_t或者可以强制类型(相近类型)转化为size_t、string或者其他自定义类型。hash默认情况下为第一种,若想要显示控制转化过程,需要自己手动写手动传。

cpp 复制代码
template<class K>  
struct HashFunc
{
	size_t operator()(const K& key) //size_t、可强制类型转化为size_t
	{
		return (size_t)key;
	}
};
cpp 复制代码
//全特化,特化后若为显示传模板参数,默认情况下会去调用全特化,必须需要一个模板作为基础
template<>   //因为string为常见类型,所以将其进行特化
{
	size_t operator()(const string& s)
	{
		size_t hashi = 0;
		for (auto& e : s)
		{
			hashi += e;
			hashi *= 131;  //BKDRhash算法,哈希冲突小,将字符串转化为整形
		}
		return hashi;
	}
};
  • BKDRHash算法是由Brian Kernighan与Dennis Ritchie的《The C Programming Language》一书被展示而得名,是一种简单快捷的hash算法,也是Java目前采用的字符串的Hash算法(累乘因子为31)。BKDRHash算法的原理是将字符串转化为一个整数,然后通过一系列位运算和取模运算,将整数压缩到一个固定范围内的值,最终得到哈希值。
cpp 复制代码
struct HashFunPerson //Person
{
	size_t operator()(const Person& p)
	{
		size_t hashi = 0;
		hashi += p._id;
		hashi *= 131;
		return hashi;
	}
};
cpp 复制代码
struct Person
{
	string _name;
	size_t _id;  //身份证号码,唯一
	size_t age;
};

struct HashFunPerson //Person
{
	size_t operator()(const Person& p)
	{
		size_t hashi = 0;
		hashi += p._id;
		hashi *= 131;
		return hashi;
	}
};
cpp 复制代码
struct Data 
{
	size_t _year;
	size_t _month;
	size_t _day;
};

struct HashFunDate  //Data
{
	size_t operator()(const Data& d)
	{
		size_t hashi = 0;
		hashi += d._year;
		hashi *= 131;
		hashi += d._month;
		hashi *= 131;
		hashi += d._day;
		hashi *= 131;
		return hashi;
	}
};

2.5.2 哈希表---闭散列模拟实现

2.5.2.1 哈希表结构的定义
cpp 复制代码
enum State  //状态
{
	Empty,
	Exist,
	Delete
};

template<class K, class V>
struct HashData{  
	pair<K, V> _kv; //数据值
	State _state = Empty;  //状态
};

/*注意:仿函数hash作用:将各种数据类型K -> int->存储位置, eg : string、Data、Person -> int->存储值
        K类型有两种情况:本身为size_t或者可以强制类型(相近类型)转化为size_t、string或者其他自定义类型
		hash默认情况下为第一种,若想要显示控制转化过程,需要自己手动写手动传*/
template<class K, class V, class Hash = HashFunc<K>>  
class HashTable{   
public:
    HashTable(size_t n = 10) //构造函数
	{
		_table.resize(n);
	}

private:
	vector<HashData<K, V>> _table; //哈希表的物理结构为数组 ------》指针数组
	size_t _n = 0;  //实际存储的数据个数
};
2.5.2.2 查询
cpp 复制代码
HashData<K, V>* find(const K& key) //查找
{
	Hash hs;  //仿函数类创建对象,对象调用operator()进行转化为整形
	size_t hashi = hs(key) % _table.size();  //key在哈希表中的位置
	while (_table[hashi]._state != Empty)  //直到遇到空,查找结束
	{
		if (_table[hashi]._kv.first == key && _table[hashi]._state == Exist)
			return &_table[hashi];  //找到了
        
		hashi++;  //存在和删除状态继续线性向后查找
		hashi = hashi % _table.size();  //走到了哈希表的结尾,从头查找
	}
	return nullptr;  //找不到
}
2.5.2.3 插入
cpp 复制代码
bool insert(const pair<K, V>& kv)  //插入
{
	Hash hs;
	if (find(kv.first))  return false;  //unordered_map、unordered_set中key不能重复、
		     
	if ((double)_n / _table.size() >= 0.7)  //负载因子>=0.7,进行扩容
	{
		//1.创建新表vector<HashData<K, V>> newtable(_table.size()*2);
		//2.遍历旧表,重新映射到新表 -> 冗余
		//3.新旧表交换_table.swap(newtable);

		HashTable<K, V, Hash> newHT(_table.size() * 2);  
		for (auto& e : _table) //遍历旧表,重新映射到新表
		{
			if (e._state == Exist)
				newHT.insert(e._kv); //递归,实现代码复用
		}
		_table.swap(newHT._table); 
}

	//关键码与存储位置的映射
	size_t hashi = hs(kv.first) % _table.size();  //注意:%左右操作符必须为整数 
	while (_table[hashi]._state == Exist) //发生哈希冲突,线性往后找"空",进行插入
	{
		hashi++;
		hashi = hashi % _table.size(); 
	}

	_table[hashi]._kv = kv;
	_table[hashi]._state = Exist;
	_n++;
	return true;
}
2.5.2.4 删除
cpp 复制代码
bool erase(const K& key)  //删除  不能直接删除该位置上的值,会影响其他关键码的删除,用状态来标记
{
	//查找到了,再进行删除
	HashData<K, V>* ret = find(key); 
	if (ret)   
	{
		ret->_state = Delete;
		_n--;
		return true;
	}
	else
		return false;
}

2.5.3 哈希桶---开散列模拟实现

2.5.3.1 哈希表结构的定义
cpp 复制代码
template<class K, class V>
struct HashNode { 
	typedef HashNode<K, V> Node;

	Node* _next;   //单链表
	pair<K, V> _kv;

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

template<class K, class V, class Hash = HashFunc<K>>
class HashTable{   //开散列拉链法、开散列哈希桶
public:
	typedef HashNode<K, V> Node;

	HashTable() //构造函数
	{
		_table.resize(10, nullptr);
	}

    ~HashTable()  //析构函数 ------》 对象中进行了资源申请,需要显示写
	{
		for (int i = 0; i < _table.size(); i++) 
		{
			Node* next = nullptr;
			Node* cur = _table[i];
			while (cur)  //释放单链表
			{
				next = cur->_next;
				delete cur;
				cur = next;
			}
			_table[i] = nullptr;
		}
	}

private:  //vector<list<K, V>> _table结构也可以,但带头双向循环链表空间开销大
	vector<Node*> _table;  //哈希表,里面存储的是单链表的头节点
	size_t _n = 0;  //实际存储的数据个数
};
2.5.3.2 查询
cpp 复制代码
Node* find(const K& key)  //查找
{
	Hash hs; 
	size_t hashi = hs(key) % _table.size();
	Node* cur = _table[hashi];  //key所对应的桶
	while (cur)  //遍历桶中的单链表
	{
		if (cur->_kv.first == key)  
			return cur;   //找到了

		cur = cur->_next;
	}
	return nullptr;  //没找到
}
  • 先使用哈希函数将key转化为哈希地址,找到对应的桶后,从前往后遍历单链表,直到单链表为空 或者 找到了。
2.5.3.3 插入
cpp 复制代码
bool insert(const pair<K, V>& kv)  //插入  本质:开空间,添加新节点
{
	if (find(kv.first))  return false; //key是唯一的

	//最好情况:每个桶下面只有一个元素
	if (_n == _table.size())  //扩容
	{
		vector<Node*> _newtable(_n * 2, nullptr); //开辟新的哈希表
		 //取出旧表中每个桶中的单链表上节点,重新计算(映射),链接到新表中,头插
		for (size_t i = 0; i < _table.size(); i++) 
		{
			Node* next = nullptr;
			Node* cur = _table[i];
			while (cur)  
			{
				next = cur->_next;
				cur->_next = _newtable[i];
				_newtable[i] = cur;
				cur = next;
			}
			_table[i] = nullptr;
		}
		_table.swap(_newtable);  //新旧表交换
	}
		
	Hash hs; 
	size_t hashi = hs(kv.first) % _table.size();   //起始size=10
	Node* newnode = new Node(kv);
	newnode->_next = _table[hashi];
	_table[hashi] = newnode;
	_n++;
	return true;
}
  • 先使用哈希函数将key转化为哈希地址,找到对应的桶后,头插。若表中存在待插入元素,则插入失败,因为unordered_map、unordered_set中的key是唯一的。

  • 开散列扩容:存储的元素个数等于桶的个数时,需要扩容。因为哈希表中桶的个数是一定的,随着元素的不断的插入,桶中的元素个数不断增加,再极端情况下,一个桶中有很多很多节点,导致查找性能降低,最好的情况是每个桶中只挂一个节点,每插入一次,就会发生哈希冲突,此时需要进行扩容。

2.5.3.4 删除
cpp 复制代码
bool erase(const K& key)  //删除
{
	Hash hs;
	size_t hashi = hs(key) % _table.size();
	Node* prev = nullptr;  //单链表的删除需要记录前一个节点,否则会找不到当前节点的下一个
	Node* cur = _table[hashi];
	while (cur)   
	{
		if (cur->_kv.first == key)
		{
			if (prev != nullptr) //中间以及后面位置的删除
				prev->_next = cur->_next;
			else   //头删
				_table[hashi] = cur->_next;
			delete cur;  //删除
			cur = nullptr;
			_n--;
			return true;
		}
		prev = cur;
		cur = cur->_next;
	}
	return false;  //key不存在
}
  • 先使用哈希函数将key转化为哈希地址,找到对应的桶后,从前往后遍历单链表,直到单链表为空 或者找到了,找到了才能进行删除,但不能直接使用find函数,因为find函数只返回表中要删除元素的地址,而单链表的删除需要记录删除节点的前一个节点,让前一个结点指向删除节点的下一个节点。

3. 哈希表封装unorered_map、unordered_set

  • unordered_map、unordered_set共用同一个哈希表,所以将哈希表变为模板。因为与map、set用法上基本上无区别,所以两者的实现基本相同。

3.1 哈希表的模拟实现

3.1.1 迭代器

cpp 复制代码
template<class K, class T, class KeyOfT, class Hash>   // 注意 : 前置声明
class HashTable;    //类模板的声明必须带上模板参数

template<class K, class T, class KeyOfT, class Hash>
struct Hs_iterator {    //迭代器
	typedef HashNode<T> Node;  
	typedef HashTable<K, T, KeyOfT, Hash> HT;
	typedef Hs_iterator<K, T, KeyOfT, Hash> Self;

	Node* _node;  //节点的地址 -》可实现*、!=、==、->
	HT* _hs;  //对象的地址 -》可实现++,为了找到下一个非空桶方便
};
3.1.1.1 构造函数
cpp 复制代码
Hs_iterator(Node* node, HT* hs)  //构造函数
		:_node(node)
		,_hs(hs)
    { }
3.1.1.2 begin()+end()

💡iterator begin( ) ;

  • 功能:返回哈希表中第一个非空桶的第一个节点的迭代器
cpp 复制代码
iterator begin()  //哈希表中第一个非空桶的第一个位置
{
	for (size_t i = 0; i < _table.size(); i++)
	{
		if (_table[i])
		{
			return iterator(_table[i], this);
		}
	}
	return end();  //哈希表中全为空桶
}

💡iterator end( ) ;

  • 功能:返回空指针。
cpp 复制代码
iterator end()  
{
	return iterator(nullptr, this);
}
3.1.1.3 operator++()
cpp 复制代码
//对于++,走到某个桶的结束位置,需要找下一个非空桶,此时需要借助_table数组
Self& operator++() 
{
	KeyOfT kt;
	Hash ht;

	if (_node->_next)   //当前桶还有数据,直接取当前迭代器所指向节点的下一个节点
		_node = _node->_next;
	else  //当前桶走完了,需要找下一个非空桶的第一个节点
	{
		size_t hashi = ht(kt(_node->_kv)) % _hs->_table.size();
		hashi++;
		while (hashi < _hs->_table.size())
		{
    		if (_hs->_table[hashi]) //找到了
			{
				_node = _hs->_table[hashi];
				break;
			}
			hashi++;
		}
		if (hashi == _hs->_table.size()) //不存在下一个非空桶
			_node = nullptr;
	}
	return *this;
}
  • 如果当前桶还有数据,直接取当前迭代器所指向节点的下一个节点 ; 如果当前桶走完了,需要找下一个非空桶的第一个节点。此时需要借助_table。
3.1.1.4 operator*()
cpp 复制代码
T& operator*()  //解引用
{
	return _node->_kv;
}
3.1.1.5 operator->()
cpp 复制代码
T* operator->()  //结构体指针
{
	return &_node->_kv;
}
3.1.1.6 operator==()
cpp 复制代码
bool operator==(const Self& it)  
{
	return _node == it._node;
}
3.1.1.7 operator!=()
cpp 复制代码
bool operator!=(const Self& it)
{
	return _node != it._node;
}

3.1.2 insert()

💡pair<iterator,bool> insert(const T& kv) ;

  • 功能:向红黑树中插入data。
  • insert返回值为pair<iterator, bool>,若key(unordered_set的key、unordered_map的pair的first)在哈希表中存在,因为键值key不能重复,所以pair::first指向在哈希表中与key值相等节点的迭代器,pair::second为false。若key在哈希表中不存在,pair::first指向在哈希表中新插入节点的迭代器,pair::second为true。insert相当于查找。
cpp 复制代码
pair<iterator, bool> insert(const T& kv)  //插入  本质:开空间,添加新节点
{
	KeyOfT kot;
	iterator ret = find(kot(kv));
	if (ret != end())  return make_pair(ret, false); //key是唯一的  插入失败,返回该节点在哈希表的位置(迭代器)

	//最好情况:每个桶下面只有一个元素
	if(_n == _table.size())  //负载因子为1,扩容 
	{
		vector<Node*> _newtable(_n * 2, nullptr); //开辟新的哈希表
		// 取出旧表中每个桶中的单链表上节点,重新计算(映射),链接到新表中,头插
		for (size_t i = 0; i < _table.size(); i++)
		{
			Node* next = nullptr;
			Node* cur = _table[i];
			while (cur)
			{
				next = cur->_next;
				cur->_next = _newtable[i];
				_newtable[i] = cur;
				cur = next;
			}
			_table[i] = nullptr;
		}
		_table.swap(_newtable);  //新旧表交换
	}

	Hash ht;
	size_t hashi = ht(kot(kv)) % _table.size();  //起始size=10
	Node* newnode = new Node(kv);
	newnode->_next = _table[hashi];
	_table[hashi] = newnode;
	_n++;
	return make_pair(iterator(newnode, this), true);
}

3.1.3 完整代码

cpp 复制代码
/*注意:仿函数hash作用:将各种数据类型K -> int->存储位置, eg : string、Data、Person -> int->存储值
		K类型有两种情况:本身为size_t或者可以强制类型(相近类型)转化为size_t、string或者其他自定义类型
		hash默认情况下为第一种,若想要显示控制转化过程,需要自己手动写手动传*/
template<class K, class T, class KeyOfT, class Hash>  //KeyOfT:取出T中的第一个值
class HashTable {   //开散列拉链法、开散列哈希桶
public:
	//在迭代器++中,使用了private成员_table,在类外不能访问私有成员,若想被访问,则设置成友元
	template<class K, class T, class KeyOfT, class Hash>  
	friend struct Hs_iterator;  //将类模板设置为友元类,需要加上模板参数

	typedef HashNode<T> Node; 
	typedef HashTable<K, T, KeyOfT, Hash> HT; 

	typedef Hs_iterator<K, T, KeyOfT, Hash> iterator; 

	iterator begin()  //哈希表中第一个非空桶的第一个位置
	{
		for (size_t i = 0; i < _table.size(); i++)
		{
			if (_table[i])
			{
				return iterator(_table[i], this);
			}
		}
		return end();  //哈希表中全为空桶
	}

	iterator end()  
	{
		return iterator(nullptr, this);
	}

	HashTable() //构造函数
	{
		_table.resize(10, nullptr); 
	}

	HashTable(const HT& ht) //拷贝构造函数
	{
		_table.resize(10, nullptr); //开空间

		for (int i = 0; i < ht.size(); i++)  //拷贝数据
		{
			if (ht._table[i])
				insert(ht._table[i]->_kv);
		}
	}

	HT& operator=(HT ht) //赋值运算符重载
	{
		_table.swap(ht._table);
		swap(_n, ht._n);
		return *this;
	}

	~HashTable()  //析构函数 ------》 对象中进行了资源申请,需要显示写
	{
		for (int i = 0; i < _table.size(); i++)
		{
			Node* next = nullptr;
			Node* cur = _table[i];
			while (cur)  //释放单链表
			{
				next = cur->_next;
				delete cur;
				cur = next;
			}
			_table[i] = nullptr;
		}
	}

	iterator find(const K& key)  //查找 
	{
		Hash ht;
		KeyOfT kot;
		size_t hashi = ht(key) % _table.size();
		Node* cur = _table[hashi];  //key所对应的桶
		while (cur)  //遍历桶中的单链表
		{
			if (kot(cur->_kv) == key)
				return iterator(cur, this);   //找到了

			cur = cur->_next;
		}
		return end();  //没找到
	}

	pair<iterator, bool> insert(const T& kv)  //插入  本质:开空间,添加新节点
	{
		KeyOfT kot;
		iterator ret = find(kot(kv));
		if (ret != end())  return make_pair(ret, false); //key是唯一的  插入失败,返回该节点在哈希表的位置(迭代器)

		//最好情况:每个桶下面只有一个元素
		if(_n == _table.size())  //负载因子为1,扩容 
		{
			vector<Node*> _newtable(_n * 2, nullptr); //开辟新的哈希表
			// 取出旧表中每个桶中的单链表上节点,重新计算(映射),链接到新表中,头插
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* next = nullptr;
				Node* cur = _table[i];
				while (cur)
				{
					next = cur->_next;
					cur->_next = _newtable[i];
					_newtable[i] = cur;
					cur = next;
				}
				_table[i] = nullptr;
			}
			_table.swap(_newtable);  //新旧表交换
		}

		Hash ht;
		size_t hashi = ht(kot(kv)) % _table.size();  //起始size=10
		Node* newnode = new Node(kv);
		newnode->_next = _table[hashi];
		_table[hashi] = newnode;
		_n++;
		return make_pair(iterator(newnode, this), true);
	}

	bool erase(const K& key)  //删除
	{
		Hash _ht;
		KeyOfT kot;
		size_t hashi = _ht(key) % _table.size();
		Node* prev = nullptr;  //单链表的删除需要记录前一个节点
		Node* cur = _table[hashi];
		while (cur)
		{
			if (kot(cur->_kv) == key)
			{
				if (prev != nullptr) //中间以及后面位置的删除
					prev->_next = cur->_next;
				else   //头删
					_table[hashi] = cur->_next;
				delete cur;  //删除
				cur = nullptr;
				_n--;
				return true;
			}
			prev = cur;
			cur = cur->_next;

		}
		return false;  //key不存在
	}

private:  
	vector<Node*> _table;  //哈希表,里面存储的是单链表的头节点  ------》指针数组
	size_t _n = 0;  //实际存储的数据个数
};

4.1 unorered_set的模拟实现

4.1.1 完整代码

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#include"HashTable.h"

namespace zzx {
	template<class K, class Hash = HashFunc<K>>
	class unordered_set {
	public:
		struct SetKeyOfT {  
			const K& operator()(const K& key)
			{
				return key;
			}
		};

		//typename作用:区分类域中的静态成员变量和typedef类型
		typedef typename HashTable<K, K, SetKeyOfT, Hash>::iterator iterator;

		iterator find(const K& key)
		{
			return _hs.find(key);
		}

		pair<iterator, bool> insert(const K& key)
		{
			return _hs.insert(key);
		}

		bool erase(const K& key)
		{
			return _hs.erase(key);
		}

		iterator begin()
		{
			return _hs.begin();
		}

		iterator end()
		{
			return _hs.end();
		}

	private:
		HashTable<K, K, SetKeyOfT, Hash> _hs;  
	};
}

5.1 unorered_map的模拟实现

5.1.1 operator[]

💡V& operator[ ](const K& key) ;

  • 功能:访问与key相对应的value值。即可读又可写。

  • 原理:operator[ ]底层是通过调用insert( )将键值队插入到unordered_map中。如果key存在,插入失败,insert返回与unordered_map中key值相同元素的迭代器。如果key不存在,插入成功,insert返回在unordered_map中新插入元素的迭代器。operator[ ]最后返回与key值相对应的value值的引用。

  • operator[ ] 具有插入、查找、插入+修改、查找+修改功能。

cpp 复制代码
V& operator[](const K& key)
{
	pair<iterator, bool> ret = insert(make_pair(key, V()));
	return ret.first->second;
}

5.1.2 完整代码

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#include"HashTable.h"

namespace zzx {
	template<class K, class V, class Hash = HashFunc<K>>
	class unordered_map {
	public:
		struct MapKeyOfT {
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};

		typedef typename HashTable<K, pair<K, V>, MapKeyOfT, Hash>::iterator iterator;

		iterator find(const K& key)
		{
			return _hs.find(key);
		}

		pair<iterator, bool> insert(const pair<K, V>& kv)
		{
			return _hs.insert(kv);
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = insert(make_pair(key, V()));
			return ret.first->second;
		}
		
		bool erase(const K& key)
		{
			return _hs.erase(key);
		}

		iterator begin()
		{
			return _hs.begin();
		}

		iterator end()
		{
			return _hs.end();
		}

	private:
		HashTable<K, pair<K, V>, MapKeyOfT, Hash> _hs;
	};
}
相关推荐
武子康5 分钟前
Hadoop-11-MapReduce JOIN 操作的Java实现 Driver Mapper Reducer具体实现逻辑 模拟SQL进行联表操作
java·大数据·hadoop·分布式·sql·mapreduce
希昂的学习日记9 分钟前
NIO基础
java·nio
JIAWAP11 分钟前
Linux环境安装Maven
java·linux·centos·maven
年轻的高血压患者13 分钟前
基于Java的水果商品销售网站
java·sql·mysql·servlet·架构·eclipse·idea
*Soo_Young*19 分钟前
【Spring AOP 源码解析前篇】什么是 AOP | 通知类型 | 切点表达式| AOP 如何使用
java·spring·aop
donotdothat21 分钟前
D1.排序
数据结构·算法
LabVIEW开发24 分钟前
LabVIEW透视变换
算法·计算机视觉·labview·labview开发
我是陈泽24 分钟前
AI教你如何系统的学习Python
开发语言·数据结构·人工智能·python·学习·青少年编程
无敌海苔咪31 分钟前
实验六 图像的傅立叶变换
图像处理·算法·计算机视觉·matlab·数字图像处理
惜了梦i42 分钟前
【leetcode周赛记录——405】
算法·leetcode·职场和发展