C++STL(八) :unordered_set、unordered_map的模拟实现

文章目录

  • C++STL(八) :unordered_set、unordered_map的模拟实现
    • 哈希节点与哈希表的定义
    • 哈希表模板参数的控制
    • string类型无法取模问题
    • 哈希表正向迭代器的实现
      • 迭代器成员说明
      • operator++
    • 哈希表默认成员函数实现
      • 构造函数
      • 拷贝构造函数
      • 赋值运算符重载
      • 析构函数
    • 封装完成的代码
      • 哈希表
      • 正向迭代器
      • unordered_set
      • unordered_map

C++STL(八) :unordered_set、unordered_map的模拟实现

哈希节点与哈希表的定义

哈希节点的定义如下:

  • _data存放节点的值
  • _next指向下个节点
template<class T>
struct HashNode
{
    HashNode<T>* _next;
    T _data;

    HashNode(const T& data)
    :_data(data)
    ,_next(nullptr)
    {}
};

哈希表的定义如下:

  • _table存放HashNode
  • _n是哈希表的实际存储数量,用来计算负载因子
template<class K, class T>
class HashTable
{
	typedef HashNode<T> Node; //哈希结点类型
public:
    //...
private:
	vector<Node*> _table; //哈希表
	size_t _n = 0; //哈希表中的有效元素个数
};

哈希表模板参数的控制

unordered_set是K模型的容器,而unordered_map是KV模型的容器。要想只用一份哈希表代码同时封装出K模型和KV模型的容器,我们必定要对哈希表的模板参数进行控制

如果上层使用的是unordered_set容器,那么传入哈希表的模板参数就是key和key

template<class K>
class unordered_set
{
public:
	//...
private:
	HashTable<K, K> _ht; //传入底层哈希表的是K和K
};

如果上层使用的是unordered_map容器,那么传入哈希表的模板参数就是key以及key和value构成的键值对

template<class K, class V>
class unordered_map
{
public:
	//...
private:
	HashTable<K, pair<K, V>> _ht; //传入底层哈希表的是K以及K和V构成的键值对
};

也就是说,哈希表中的模板参数T的类型是什么完全却决于上层所使用容器的种类

C++STL(八) :unordered_set、unordered_map的模拟实现_第1张图片

在哈希映射过程中,我们需要获得元素的键值,然后通过哈希函数计算出对应的哈希地址进行映射

现在由于我们在哈希结点当中存储的数据类型是T,这个T可能就是一个键值,也可能是一个键值对,对于底层的哈希表来说,它并不知道哈希结点当中存储的数据究竟是什么类型,因此需要由上层容器提供一个仿函数,用于获取T类型数据当中的键值

因此,unordered_map容器需要向底层哈希表提供一个仿函数,该仿函数返回键值对当中的键值

template<class K, class V>
class unordered_map
{
	//仿函数
	struct MapKeyOfT
	{
		const K& operator()(const pair<K, V>& kv) //返回键值对当中的键值key
		{
			return kv.first;
		}
	};
public:
	//...
private:
	HashTable<K, pair<K, V>, MapKeyOfT> _ht;
};

虽然unordered_set容器传入哈希表的T就是键值,但是底层哈希表并不知道上层容器的种类,底层哈希表在获取键值时会统一通过传入的仿函数进行获取,因此unordered_set容器也需要向底层哈希表提供一个仿函数

template<class K>
class unordered_set
{
	//仿函数
	struct SetKeyOfT
	{
		const K& operator()(const K& key) //返回键值key
		{
			return key;
		}
	};
public:
	//...
private:
	HashTable<K, K, SetKeyOfT> _ht;
};

因此,底层哈希表的模板参数现在需要增加一个,用于接收上层容器提供的仿函数

template<class K, class T, class KeyOfT>
class HashTable

string类型无法取模问题

经过上面的分析后,我们让哈希表增加了一个模板参数,此时无论上层容器是unordered_set还是unordered_map,我们都能够通过上层容器提供的仿函数获取到元素的键值。但是在我们日常编写的代码中,用字符串去做键值key是非常常见的事,比如我们用unordered_map容器统计水果出现的次数时,就需要用各个水果的名字作为键值

而字符串并不是整型,也就意味着字符串不能直接用于计算哈希地址,我们需要通过某种方法将字符串转换成整型后,才能代入哈希函数计算哈希地址

但遗憾的是,我们无法找到一种能实现字符串和整型之间一对一转换的方法,因为在计算机中,整型的大小是有限的,比如用无符号整型能存储的最大数字是4294967295,而众多字符能构成的字符串的种类却是无限的。鉴于此,无论我们用什么方法将字符串转换成整型,都会存在哈希冲突,只是产生冲突的概率不同而已

BKDRHash算法由于在Brian Kernighan与Dennis Ritchie的《The C Programing Language》一书被展示而得名,是一种简单快捷的hash算法,也是Java目前采用的字符串的hash算法

因此,现在我们需要在哈希表的模板参数中再增加一个仿函数,用于将键值key转换成对应的整型

template<class K, class T, class KeyOfT, class HashFunc = Hash<K>>
class HashTable

若是上层没有传入该仿函数,我们则使用默认的仿函数,该默认仿函数直接返回键值key即可,但是用字符串作为键值key是比较常见的,因此我们可以针对string类型写一个类模板的特化,此时当键值key为string类型时,该仿函数就会根据BKDRHash算法返回一个对应的整型

template<class K>
struct Hash
{
	size_t operator()(const K& key) //返回键值key
	{
		return key;
	}
};
//string类型的特化
template<>
struct Hash<string>
{
	size_t operator()(const string& s) //BKDRHash算法
	{
		size_t value = 0;
		for (auto ch : s)
		{
			value = value * 131 + ch;
		}
		return value;
	}
};

哈希表正向迭代器的实现

哈希表的正向迭代器实际上就是对哈希结点指针进行了封装,但是由于在实现++运算符重载时,可能需要在哈希表中去寻找下一个非空哈希桶,因此每一个正向迭代器中都应该存储哈希表的地址

迭代器成员说明

  • _pht:指向哈希表的指针
  • _hashi:记录当前在哈希表的哪个位置
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct __HTIterator
{
	typedef HashNode<T> Node;
	typedef __HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
	Node* _node;
	const HashTable<K, T, KeyOfT, Hash>* _pht;
	size_t _hashi;
};

operator++

由于哈希表中存放的都是指针,可以将哈希表理解为一个“链表的数组”,所以在进行++操作时要进行运算符重载

Self& operator++()
{
    if (_node->_next)
    {
        // 当前桶还有节点,走到下一个节点
        _node = _node->_next;
    }
    else
    {
        // 当前桶已经走完了,找下一个桶开始
        ++_hashi;
        while (_hashi < _pht->_tables.size())
        {
            if (_pht->_tables[_hashi])
            {
                _node = _pht->_tables[_hashi];
                break;
            }

            ++_hashi;
        }

        if (_hashi == _pht->_tables.size())
        {
            _node = nullptr;
        }
    }
    return *this;
}

哈希表默认成员函数实现

构造函数

哈希表中有两个成员变量,当我们实例化一个对象时:

  • _table会自动调用vector的默认构造函数进行初始化
  • _n会根据我们所给的缺省值被设置为0

因此我们不需要编写构造函数,使用默认生成的构造函数就足够了,但是由于我们后面需要编写拷贝构造函数,编写了拷贝构造函数后,默认的构造函数就不会生成了,此时我们需要使用default关键字显示指定生成默认构造函数

//构造函数
HashTable() = default; //显示指定生成默认构造函数

拷贝构造函数

哈希表在拷贝时需要进行深拷贝,否则拷贝出来的哈希表和原哈希表中存储的都是同一批结点。

哈希表的拷贝构造函数实现逻辑如下:

  1. 将哈希表的大小调整为ht._table的大小
  2. 将ht._table每个桶当中的结点一个个拷贝到自己的哈希表中
  3. 更改哈希表当中的有效数据个数
//拷贝构造函数
HashTable(const HashTable& ht)
{
	//1、将哈希表的大小调整为ht._table的大小
	_table.resize(ht._table.size());
	//2、将ht._table每个桶当中的结点一个个拷贝到自己的哈希表中(深拷贝)
	for (size_t i = 0; i < ht._table.size(); i++)
	{
		if (ht._table[i]) //桶不为空
		{
			Node* cur = ht._table[i];
			while (cur) //将该桶的结点取完为止
			{
				Node* copy = new Node(cur->_data); //创建拷贝结点
				//将拷贝结点头插到当前桶
				copy->_next = _table[i];
				_table[i] = copy;
				cur = cur->_next; //取下一个待拷贝结点
			}
		}
	}
	//3、更改哈希表当中的有效数据个数
	_n = ht._n;
}

赋值运算符重载

实现赋值运算符重载函数时,可以通过参数间接调用拷贝构造函数,之后将拷贝构造出来的哈希表和当前哈希表的两个成员变量分别进行交换即可,当赋值运算符重载函数调用结束后,拷贝构造出来的哈希表会因为出了作用域而被自动析构,此时原哈希表之前的数据也就顺势被释放了

//赋值运算符重载函数
HashTable& operator=(HashTable ht)
{
	//交换哈希表中两个成员变量的数据
	_table.swap(ht._table);
	swap(_n, ht._n);

	return *this; //支持连续赋值
}

析构函数

因为哈希表当中存储的结点都是new出来的,因此在哈希表被析构时必须进行结点的释放。在析构哈希表时我们需要依次取出非空的哈希桶,遍历哈希桶当中的结点并进行释放

//析构函数
~HashTable()
{
	//将哈希表当中的结点一个个释放
	for (size_t i = 0; i < _table.size(); i++)
	{
		if (_table[i]) //桶不为空
		{
			Node* cur = _table[i];
			while (cur) //将该桶的结点取完为止
			{
				Node* next = cur->_next; //记录下一个结点
				delete cur; //释放结点
				cur = next;
			}
			_table[i] = nullptr; //将该哈希桶置空
		}
	}
}

封装完成的代码

哈希表

//哈希结点的定义
template<class T>
struct HashNode
{
	T _data;
	HashNode<T>* _next;

	//构造函数
	HashNode(const T& data)
		:_data(data)
		, _next(nullptr)
	{}
};
template<class K>
struct Hash
{
	size_t operator()(const K& key) //返回键值key
	{
		return key;
	}
};
//string类型的特化
template<>
struct Hash<string>
{
	size_t operator()(const string& s) //BKDRHash算法
	{
		size_t value = 0;
		for (auto ch : s)
		{
			value = value * 131 + ch;
		}
		return value;
	}
};
//哈希表的实现
template<class K, class T, class KeyOfT, class HashFunc = Hash<K>>
class HashTable
{
	//将正向迭代器类声明为哈希表类的友元
	template<class K, class T, class KeyOfT, class HashFunc>
	friend struct __HTIterator;
	//friend struct __HTIterator;
	typedef HashNode<T> Node; //哈希结点类型
public:
	typedef __HTIterator<K, T, KeyOfT, HashFunc> iterator; //正向迭代器的类型

	iterator begin()
	{
		size_t i = 0;
		while (i < _table.size()) //找到第一个非空哈希桶
		{
			if (_table[i]) //该哈希桶非空
			{
				return iterator(_table[i], this); //返回该哈希桶中的第一个结点的正向迭代器
			}
			i++;
		}
		return end(); //哈希桶中无数据,返回end()
	}
	iterator end()
	{
		return iterator(nullptr, this); //返回nullptr的正向迭代器
	}

	//构造函数
	HashTable() = default; //显示指定生成默认构造

	//拷贝构造函数
	HashTable(const HashTable& ht)
	{
		//1、将哈希表的大小调整为ht._table的大小
		_table.resize(ht._table.size());
		//2、将ht._table每个桶当中的结点一个个拷贝到自己的哈希表中(深拷贝)
		for (size_t i = 0; i < ht._table.size(); i++)
		{
			if (ht._table[i]) //桶不为空
			{
				Node* cur = ht._table[i];
				while (cur) //将该桶的结点取完为止
				{
					Node* copy = new Node(cur->_data); //创建拷贝结点
					//将拷贝结点头插到当前桶
					copy->_next = _table[i];
					_table[i] = copy;
					cur = cur->_next; //取下一个待拷贝结点
				}
			}
		}
		//3、更改哈希表当中的有效数据个数
		_n = ht._n;
	}
	//赋值运算符重载函数
	HashTable& operator=(HashTable ht)
	{
		//交换哈希表中两个成员变量的数据
		_table.swap(ht._table);
		swap(_n, ht._n);

		return *this; //支持连续赋值
	}
	//析构函数
	~HashTable()
	{
		//将哈希表当中的结点一个个释放
		for (size_t i = 0; i < _table.size(); i++)
		{
			if (_table[i]) //桶不为空
			{
				Node* cur = _table[i];
				while (cur) //将该桶的结点取完为止
				{
					Node* next = cur->_next; //记录下一个结点
					delete cur; //释放结点
					cur = next;
				}
				_table[i] = nullptr; //将该哈希桶置空
			}
		}
	}
	//获取本次增容后哈希表的大小
	size_t GetNextPrime(size_t prime)
	{
		const int PRIMECOUNT = 28;
		//素数序列
		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 = 0; i < PRIMECOUNT; i++)
		{
			if (primeList[i] > prime)
				return primeList[i];
		}
		return primeList[i];
	}
	//插入函数
	pair<iterator, bool> Insert(const T& data)
	{
		KeyOfT kot;
		//1、查看哈希表中是否存在该键值的键值对
		iterator ret = Find(kot(data));
		if (ret != end()) //哈希表中已经存在该键值的键值对(不允许数据冗余)
		{
			return make_pair(ret, false); //插入失败
		}

		//2、判断是否需要调整哈希表的大小
		if (_n == _table.size()) //哈希表的大小为0,或负载因子超过1
		{
			//增容
			//a、创建一个新的哈希表,新哈希表的大小设置为原哈希表的2倍(若哈希表大小为0,则将哈希表的初始大小设置为10)
			HashFunc hf;
			vector<Node*> newtable;
			//size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;
			//newtable.resize(newsize);

			newtable.resize(GetNextPrime(_table.size()));
				
			//b、将原哈希表当中的结点插入到新哈希表
			for (size_t i = 0; i < _table.size(); i++)
			{
				if (_table[i]) //桶不为空
				{
					Node* cur = _table[i];
					while (cur) //将该桶的结点取完为止
					{
						Node* next = cur->_next; //记录cur的下一个结点
						size_t index = hf(kot(cur->_data))%newtable.size(); //通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)
						//将该结点头插到新哈希表中编号为index的哈希桶中
						cur->_next = newtable[index];
						newtable[index] = cur;

						cur = next; //取原哈希表中该桶的下一个结点
					}
					_table[i] = nullptr; //该桶取完后将该桶置空
				}
			}
			//c、交换这两个哈希表
			_table.swap(newtable);
		}

		//3、将键值对插入哈希表
		HashFunc hf;
		size_t index = hf(kot(data)) % _table.size(); //通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)
		Node* newnode = new Node(data); //根据所给数据创建一个待插入结点
		//将该结点头插到新哈希表中编号为index的哈希桶中
		newnode->_next = _table[index];
		_table[index] = newnode;

		//4、哈希表中的有效元素个数加一
		_n++;
		return make_pair(iterator(newnode, this), true);
	}
	//查找函数
	iterator Find(const K& key)
	{
		if (_table.size() == 0) //哈希表大小为0,查找失败
		{
			return end();
		}

		KeyOfT kot;
		HashFunc hf;
		size_t index = hf(key) % _table.size(); //通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)
		//遍历编号为index的哈希桶
		HashNode<T>* cur = _table[index];
		while (cur) //直到将该桶遍历完为止
		{
			if (kot(cur->_data) == key) //key值匹配,则查找成功
			{
				return iterator(cur, this);
			}
			cur = cur->_next;
		}
		return end(); //直到该桶全部遍历完毕还没有找到目标元素,查找失败
	}
	//删除函数
	bool Erase(const K& key)
	{
		KeyOfT kot;
		HashFunc hf;
		//1、通过哈希函数计算出对应的哈希桶编号index(除数不能是capacity)
		size_t index = hf(key) % _table.size();
		//2、在编号为index的哈希桶中寻找待删除结点
		Node* prev = nullptr;
		Node* cur = _table[index];
		while (cur) //直到将该桶遍历完为止
		{
			if (kot(cur->_data) == key) //key值匹配,则查找成功
			{
				//3、若找到了待删除结点,则删除该结点
				if (prev == nullptr) //待删除结点是哈希桶中的第一个结点
				{
					_table[index] = cur->_next; //将第一个结点从该哈希桶中移除
				}
				else //待删除结点不是哈希桶的第一个结点
				{
					prev->_next = cur->_next; //将该结点从哈希桶中移除
				}
				delete cur; //释放该结点
				//4、删除结点后,将哈希表中的有效元素个数减一
				_n--;
				return true; //删除成功
			}
			prev = cur;
			cur = cur->_next;
		}
		//假删除可能会导致迭代器失效
			
		return false; //直到该桶全部遍历完毕还没有找到待删除元素,删除失败
	}
private:
	vector<Node*> _table; //哈希表
	size_t _n = 0; //哈希表中的有效元素个数
}

正向迭代器

template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
    struct __HTIterator
    {
        typedef HashNode<T> Node;
        typedef __HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
        Node* _node;
        const HashTable<K, T, KeyOfT, Hash>* _pht;

        // vector * _ptb;

        size_t _hashi;

        __HTIterator(Node* node, HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
            :_node(node)
                ,_pht(pht)
                ,_hashi(hashi)
            {}

        __HTIterator(Node* node, const HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
            :_node(node)
                , _pht(pht)
                , _hashi(hashi)
            {}

        Self& operator++()
        {
            if (_node->_next)
            {
                // 当前桶还有节点,走到下一个节点
                _node = _node->_next;
            }
            else
            {
                // 当前桶已经走完了,找下一个桶开始
                //KeyOfT kot;
                //Hash hf;
                //size_t hashi = hf(kot(_node->_data)) % _pht._tables.size();
                ++_hashi;
                while (_hashi < _pht->_tables.size())
                {
                    if (_pht->_tables[_hashi])
                    {
                        _node = _pht->_tables[_hashi];
                        break;
                    }

                    ++_hashi;
                }

                if (_hashi == _pht->_tables.size())
                {
                    _node = nullptr;
                }
            }

            return *this;
        }

        Ref operator*()
        {
            return _node->_data;
        }

        Ptr operator->()
        {
            return &_node->_data;
        }

        bool operator!=(const Self& s)
        {
            return _node != s._node;
        }
    };

unordered_set

template<class K, class Hash = HashFunc<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename hash_bucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator iterator;
		typedef typename hash_bucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator const_iterator;

		/*iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}*/

		const_iterator begin() const
		{
			return _ht.begin();
		}

		const_iterator end() const
		{
			return _ht.end();
		}

		pair<const_iterator, bool> insert(const K& key)
		{
			auto ret = _ht.Insert(key);
			return pair<const_iterator, bool>(const_iterator(ret.first._node, ret.first._pht, ret.first._hashi), ret.second);
		}

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

		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}
	private:
		hash_bucket::HashTable<K, K, SetKeyOfT, Hash> _ht;
	};

unordered_map

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

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

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

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

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

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

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

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

        private:
        HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
    };

你可能感兴趣的:(c++,c++,哈希算法,散列表,STL)