哈希-unordered系列关联式容器

目录

  • 1. unordered系列关联式容器
    • 1.1 unordered_map
    • 1.2 unordered_set
    • 1.3 与map/set区别
  • 2. 底层结构
    • 2.1 哈希概念
    • 2.2 哈希冲突
    • 2.3 哈希函数
    • 2.4 哈希冲突的解决
      • 2.4.1 闭散列(开放定址法)
        • 2.4.1.1 非整形取模
      • 2.4.2 开散列
    • 2.5 闭散列和开散列的整体代码
  • 3. 封装

1. unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2 N log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。
最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同

1.1 unordered_map

容器概念和使用说明

1.2 unordered_set

容器概念和使用说明

1.3 与map/set区别

用法极其类似,会用map/set就会用unordered_map/unordered_set,而不同点在于:

  1. unordered系列容器的查找效率是高于map/set的。
  2. 该类容器中的数据依次遍历后并不是有序的,而map/set则为有序。
  3. unordered系列只支持单向迭代器,而map/set还支持反向。

2. 底层结构

之所以效率会比map/set高是因为底层用了哈希表而非红黑树。

2.1 哈希概念

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

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

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

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

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

简单来说就是:插入一个元素时,根据元素的关键码key通过哈希函数计算出一个存储位置,将元素放在该位置中即完成了数据的插入

2.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),即:不同的元素的关键码通过哈希函数计算出要存储的位置(哈希地址)是相同的,这种现象就叫做哈希冲突或者哈希碰撞

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

一般而言在哈希表中,值和存储位置是多对一的关系

发生哈希冲突该如何处理呢?

2.3 哈希函数

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

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

常见哈希函数:
直接定址法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B。
优点:简单、均匀。
缺点:需要事先知道关键字的分布情况。
使用场景:适合查找比较小且连续的情况(范围集中)。
比如:统计一个字符串中每个字符出现的次数,由于所有字符的数量就128个,因此可以开辟128个空间直接让字符本身的ascii值作为存储位置然后进行统计。
但是若数据比较分散,比如统计一组数字的出现次数,这组数字的大小差距非常大的时候,那么再使用这种方法就不合适了。

除留余数法–(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数(这里的p通常为容器的容量大小),按照哈希函数:Hash(key) = key % p(p<=m),将关键码转换成哈希地址。
解决上面数据非常分散的情况时就可以使用该方法,但是此方法还有一个问题,对于关键码是整数可以直接取模,若关键码是浮点数甚至是自定义类型的对象时该怎么办呢?后面会介绍解决方法。

2.4 哈希冲突的解决

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

2.4.1 闭散列(开放定址法)

意思是当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去,即我的位置被占了,那我就去占别人的位置。

如何寻找下一个空位置有几种方法:

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

插入:
通过哈希函数获取待插入元素在哈希表中的位置,如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素。
哈希-unordered系列关联式容器_第1张图片
key为44时造成哈希冲突,需要往后寻找一个空位置存放。

若找到最后一个位置都没找到空位置时,则回到最开头继续向后寻找

删除:
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索,因此线性探测采用状态标记的伪删除法来删除一个元素。

一般而言需要定义三个状态,分别是:存在EXIST、空EMPTY和删除DELETE。

扩容:

散列表的载荷因子定义为: α=填入表中的元素个数 / 散列表的长度。

α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之,α越小,标明填入表中的元素越少,产生冲突的可能性就越小。实际上,散列表的平均查找长度是载荷因子α的函数,只是不同处理冲突的方法有不同的函数。

对于开放定址法,荷载因子是特别重要因素,应严格限制在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中(cache missing)按照指数曲线上升。

载荷因子越大,冲突的概率就越大,空间利用率越大
载荷因子越小,冲突的概率就越小,空间利用率越小

2.4.1.1 非整形取模

由于线性探测法使用到的哈希函数是除留余数法,因此需要解决非整形无法进行直接取模的问题,解决方法是对其进行二次映射,使用仿函数将其与某个整形对应起来即可

除了线性探测外还有其它方法,比如二次探测
线性探测的缺点在于冲突的多了各个元素之间的存储位置会互相影响,数据连续堆积在一起造成拥堵,也会影响后续插入查找的效率,针对这个问题可以使用二次探测法来进行的缓解。

它与线性探测的思路非常相似,不同点在于若计算出的哈希地址冲突了,二次探测会依次向后探测 2 i 2^i 2i个距离的位置是否为空(i >= 0),而线性探测则是依次向后一个一个位置进行探测是否为空,因此二次探测是很大程度上缓解了数据冲突多了发生拥堵的问题。

但不管是线性还是二次探测本质都是需要占用别人的位置,因此数据间的存储位置必然会相互影响,使得插入查找的效率降低,那么有没有一种冲突了也不占用别人位置的方法呢?

2.4.2 开散列

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

通俗点说就是哈希表每个位置存储一个链表(指针),把通过哈希函数计算出相同的哈希地址的元素依次头插到当前位置的链表中,这样每条链表中元素的关键码都是相同的。

哈希-unordered系列关联式容器_第2张图片
关于扩容:

闭散列法是必须要扩容的,因为一个位置只能存储一个值,那么必然会出现插满的情况,而开散列法由于是指针数组,在结构上是不需要开辟的,因为指针可以动态申请空间,但实际上也是需要扩容的,如果不阔一直插入元素就会导致某些链表过长,那么查找的效率会有消耗,因此需要扩容,而且扩容逻辑也有些区别,对于开散列,当负载因子等于1的时候就要扩容了,理想情况下是每个桶里只有一个元素。

2.5 闭散列和开散列的整体代码

都需要复用的代码:

//默认哈希函数
template <class K>
struct DefaultHashFunc {
	//逻辑很简单
	//直接强转成无符号整形返回
	//可以解决负数、浮点数和指针这组内置类型无法直接取模的问题
	//但是依然无法解决自定义类型取模
	const size_t operator()(const K& key) {
		return (size_t)key;
	}
};

//针对自定义类型可以使用模板特化来处理
//比如最常见的string做key
template<>
struct DefaultHashFunc<string> {
	//针对字符串转整形有专门的字符串哈希算法
	//最常用的如:BKDR HASH
	// @detail 本算法由于在Brian Kernighan与Dennis Ritchie的《The C Programming Language》一书被展示而得名
	//是一种简单快捷的hash算法,也是Java目前采用的字符串的Hash算法(累乘因子为31)
	const size_t operator()(const string& str) {
		size_t hash = 0;
		for (char c : str) {
			hash = hash * 131 + c;
		}
		return hash;
	}
};
//还可以根据具体类型需求增加对应的模板特化

闭散列:

namespace close_address {
	//定义三个状态
	enum STATE {
		EMPTY, EXIST, DELETE
	};

	//结点数据
	template<class K, class V>
	struct HashDate {
		pair<K, V> _kv;
		STATE _state = EMPTY;
	};

	template<class K, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable {
		typedef HashTable<K, V, HashFunc> Hash_Table;
	public:
		HashTable() {
			//先开一部分空间
			_table.resize(10);
		}

		pair<const K, V>* find(const K& key) {
			size_t size = _table.size();
			HashFunc hf;
			size_t hashi = hf(key) % size;

			//不为空就继续找
			while (_table[hashi]._state != EMPTY) {
				//存在且key相等说明找到了
				if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key) {
					return (pair<const K, V>*) & _table[hashi]._kv;
				}
				hashi = (hashi + 1) % size;
			}
			return nullptr;
		}

		bool insert(const pair<K, V>& kv) {
			if (find(kv.first)) {
				return false;
			}
			size_t size = _table.size();
			//插入前需要先考虑扩容
			if (_n * 10 / size >= 7) {
				size_t newSize = size * 2;
				//先创建一个新哈希表
				Hash_Table newTable;
				//扩容
				newTable._table.resize(newSize);
				//遍历旧表依次插入到新表重新建立映射关系
				for (size_t i = 0; i < size; ++i) {
					//这里复用insert不会死递归
					//因为新表已经扩容为原来的二倍
					//因此当n不变size变大时不会满足上面的扩容条件
					//只会走下面的探测逻辑
					//存在就插入
					if (_table[i]._state == EXIST) {
						newTable.insert(_table[i]._kv);
					}
				}
				_table.swap(newTable._table);
			}
			HashFunc hf;
			//线性探测
			size_t hashi = hf(kv.first) % size;
			//找到空位置
			while (_table[hashi]._state == EXIST) {
				hashi = (hashi + 1) % size;
			}
			_table[hashi]._kv = kv;
			_table[hashi]._state = EXIST;
			++_n;
			return true;
		}

		bool erase(const K& key) {
			if (!find(key)) {
				return false;
			}
			size_t size = _table.size();
			size_t hashi = key % size;
			while (_table[hashi]._state != EMPTY) {
				//找到后直接将该位置的状态改为删除即可
				if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key) {
					_table[hashi]._state = DELETE;
					return true;
				}
				hashi = (hashi + 1) % size;
			}
			return false;
		}

	private:
		vector<HashDate<K, V>> _table;
		//用来衡量是否需要扩容
		size_t _n = 0;
	};
}

字符串哈希函数

开散列:

namespace hash_bucket {
	template<class K, class V>
	struct HashDate {
		pair<K, V> _kv;
		HashDate<K, V>* _next;

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

		}
	};

	template<class K, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable {
		typedef HashDate<K, V> Node;
	public:
		HashTable() {
			_table.resize(10);
		}
		
		HashTable(const HashTable<K, V, HashFunc>& ht) {
			//resize到表的容量(vector的size)
			_table.resize(ht.capacity());
			for (size_t i = 0; i < ht._table.size(); ++i) {
				Node* cur = ht._table[i];
				while (cur) {
					insert(cur->_kv);
					cur = cur->_next;
				}
			}
		}

		~HashTable() {
			for (size_t i = 0; i < _table.size(); ++i) {
				Node* cur = _table[i];
				while (cur) {
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
			}
		}

		bool insert(const pair<K, V>& kv) {
			if (find(kv.first)) {
				return false;
			}
			size_t size = _table.size(); 
			HashFunc hf;
			if (_n == size) {
				size_t newSize = size * 2;
				//建立新标
				vector <Node*> newTable;
				newTable.resize(newSize);
				for (size_t i = 0; i < size; ++i) {
					//依次取下来旧表中的结点重新建立映射关系后头插到新表
					Node* cur = _table[i];
					while (cur) {
						Node* next = cur->_next;
						size_t hashi = hf(cur->_kv.first) % newSize;
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;
						cur = next;
					}
					_table[i] = nullptr;
				}
				//最后交换一下
				_table.swap(newTable);
			}
			
			size_t hashi = hf(kv.first) % size;
			Node* newNode = new Node(kv);

			//头插
			newNode->_next = _table[hashi];
			_table[hashi] = newNode;
			++_n;
			return true;
		}
		bool erase(const K& key) {
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* prev = nullptr;
			Node* cur = _table[hashi];
			while (cur) {
				if (cur->_kv.first == key) {
					//头删
					if (prev == nullptr) {
						_table[hashi] = cur->_next;
					}
					//中间位置删除
					else {
						prev->_next = cur->_next;
					}
					delete cur;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}
		Node* find(const K& key) {
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur) {
				if (cur->_kv.first == key) {
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}

		size_t size() const {
			return _n;
		}

		size_t capacity() const {
			return _table.size();
		}

		void print() {
			for (size_t i = 0; i < _table.size(); ++i) {
				Node* cur = _table[i];
				printf("[%d]->", i);
				while (cur) {
					cout << cur->_kv.first << ':' << cur->_kv.second << "->";
					cur = cur->_next;
				}
				puts("NULL");
			}
			cout << endl;
		}
	private:
		//指针数组
		vector <Node*> _table;
		size_t _n = 0;
	};
}

3. 封装

HashTable.h:

#pragma once
#include 
#include 

using namespace std;

//默认哈希函数
template <class K>
struct DefaultHashFunc {
	const size_t operator()(const K& key) {
		return (size_t)key;
	}
};

//针对自定义类型可以使用模板特化来处理
//比如最常见的string做key
template<>
struct DefaultHashFunc<string> {
	const size_t operator()(const string& str) {
		size_t hash = 0;
		for (char c : str) {
			hash = hash * 131 + c;
		}
		return hash;
	}
};
//还可以根据具体类型需求增加对应的模板特化
//...

namespace hash_bucket {

	template<class T>
	struct HashDate {
		T _data;
		HashDate<T>* _next;

		HashDate(const T& data) :_data(data), _next(nullptr) {

		}
	};
	//迭代器要用到哈希表
	//因此需要前置声明
	template<class K, class T, class KeyOfT, class HashFunc>
	class HashTable;

	template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>
	struct HashTableIterator {
		typedef HashDate<T> Node;
		typedef	HashTableIterator<K, T, Ptr, Ref, KeyOfT, HashFunc> Self;
		//普通迭代器类型
		typedef	HashTableIterator<K, T, T*, T&, KeyOfT, HashFunc>   iterator;
		//需要考虑this指针类型为const
		HashTableIterator(Node* node, HashTable<K, T, KeyOfT, HashFunc>* pht) :_node(node), _pht(pht) {

		}
  		
		HashTableIterator(Node* node, const HashTable<K, T, KeyOfT, HashFunc>* pht) :_node(node), _pht(pht) {

		}
		//对于普通迭代器对象来说,该函数是拷贝构造
  		//对于const迭代器来说,该函数是构造
		HashTableIterator(const iterator& it) :_node(it._node), _pht(it._pht) {

		}

		Self& operator++() {
			//若当前指针不为空则依次遍历桶里的每个元素
			if (_node->_next) {
				_node = _node->_next;
			}
			//否则需要找到下一个桶的位置
			else {
				//要找桶就必须要有表
				//通过遍历表才能找到下个桶的位置
				KeyOfT kot;
				HashFunc hf;
				size_t hashi = hf(kot(_node->_data)) % _pht->_table.size();
				++hashi;
				while (hashi < _pht->_table.size()) {	
					if (_pht->_table[hashi]) {
						_node = _pht->_table[hashi];
						return *this;
					}
					++hashi;
				}
				_node = nullptr;
			}
			return *this;
		}

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

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

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

		Node* _node;
		const HashTable<K, T, KeyOfT, HashFunc>* _pht;
	};

	template<class K, class T, class KeyOfT, class HashFunc = DefaultHashFunc<K>>
	class HashTable {
		typedef HashDate<T> Node;
		//迭代器类中会用到哈希表类中的私有对象
		//因此需要设置友元声明
		template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>
		friend struct HashTableIterator;
	public:
		typedef HashTableIterator<K, T, T*, T&, KeyOfT, HashFunc>				iterator;
		typedef HashTableIterator<K, T, const T*, const T&, KeyOfT, HashFunc>   const_iterator;
		iterator begin() {
			//找到第一个桶
			for (size_t i = 0; i < _table.size(); ++i) {
				if (_table[i]) {
					return iterator(_table[i], this);
				}
			}
			return iterator(nullptr, this);
		}

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

		const_iterator begin() const {
			//找到第一个桶
			for (size_t i = 0; i < _table.size(); ++i) {
				if (_table[i]) {
					return const_iterator(_table[i], this);
				}
			}
			return const_iterator(nullptr, this);
		}

		const_iterator end() const {
			return const_iterator(nullptr, this);
		}

		HashTable() {
			_table.resize(10);
		}
		
		HashTable(const HashTable<K, T, HashFunc>& ht) {
			//resize到表的容量(vector的size)
			_table.resize(ht.capacity());
			for (size_t i = 0; i < ht._table.size(); ++i) {
				Node* cur = ht._table[i];
				while (cur) {
					insert(cur->_kv);
					cur = cur->_next;
				}
			}
		}

		~HashTable() {
			for (size_t i = 0; i < _table.size(); ++i) {
				Node* cur = _table[i];
				while (cur) {
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
			}
		}

		pair<iterator, bool> insert(const T& data) {
			KeyOfT kot;
			iterator ret = find(kot(data));
			if (ret != end()) {
				return make_pair(ret, false);
			}
			size_t size = _table.size(); 
			HashFunc hf;
			if (_n == size) {
				size_t newSize = size * 2;
				//建立新表
				vector <Node*> newTable;
				newTable.resize(newSize);
				for (size_t i = 0; i < size; ++i) {
					//依次取下来旧表中的结点重新建立映射关系后头插到新表
					Node* cur = _table[i];
					while (cur) {
						Node* next = cur->_next;
						size_t hashi = hf(kot(cur->_data)) % newSize;
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;
						cur = next;
					}
					_table[i] = nullptr;
				}
				//最后交换一下
				_table.swap(newTable);
			}
			
			size_t hashi = hf(kot(data)) % size;
			Node* newNode = new Node(data);

			//头插
			newNode->_next = _table[hashi];
			_table[hashi] = newNode;
			++_n;
			return make_pair(iterator(newNode, this), true);
		}
		bool erase(const K& key) {
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* prev = nullptr;
			Node* cur = _table[hashi];
			KeyOfT kot;
			while (cur) {
				if (kot(cur->_data) == key) {
					//头删
					if (prev == nullptr) {
						_table[hashi] = cur->_next;
					}
					//中间位置删除
					else {
						prev->_next = cur->_next;
					}
					--_n;
					delete cur;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}
		iterator find(const K& key) {
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			KeyOfT kot;
			while (cur) {
				if (kot(cur->_data) == key) {
					return iterator(cur, this);
				}
				cur = cur->_next;
			}
			return iterator(nullptr, this);
		}

		size_t size() const {
			return _n;
		}

		size_t capacity() const {
			return _table.size();
		}

		void print() {
			for (size_t i = 0; i < _table.size(); ++i) {
				Node* cur = _table[i];
				printf("[%d]->", i);
				while (cur) {
					cout << cur->_kv.first << ':' << cur->_kv.second << "->";
					cur = cur->_next;
				}
				puts("NULL");
			}
			cout << endl;
		}
	private:
		//指针数组
		vector <Node*> _table;
		size_t _n = 0;
	};
}

UnorderedSet.h:

#include "HashTable.h"

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

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

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

		pair<const_iterator, bool> insert(const K& data) {
			//return _ht.insert(data);
			pair<typename hash_bucket::HashTable<K, K, SetKeyOfT>::iterator, bool> ret = _ht.insert(data);
			return pair<const_iterator, bool>(ret.first, ret.second);
		}
	private:
		hash_bucket::HashTable<K, K, SetKeyOfT> _ht;
	};
}

UnorderedMap.h:

#include "HashTable.h"

namespace lzh {
	template<class K, class V>
	class unordered_map {
		struct MapKeyOfT {
			const K& operator()(const pair<const K, V>& kv) {
				return kv.first;
			}
		};
	public:
		typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT>::iterator iterator;
		typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT>::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<iterator, bool> insert(const pair<K, V>& data) {
			return _ht.insert(data);
		}

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

你可能感兴趣的:(c++,数据结构)