主頁 > 軟體設計 > 哈希表底層探索

哈希表底層探索

2021-06-14 07:27:59 軟體設計

文章目錄

  • 1.unordered_ map、unordered_set
    • 1.1介紹
    • 1.2效率對比
    • 1.3實戰演練
  • 2.哈希
    • 2.1哈希的概念
    • 2.2哈希函式
    • 2.2.1設計原則
    • 2.2.2常見哈希函式
        • 2.2.2.1常用哈希函式
        • 2.2.2.2 不常用哈希函式
    • 2.3哈希沖突
  • 3.閉散列
    • 3.1線性探測
    • 3.2閉散列擴容-載荷因子
    • 3.3二次探測
    • 3.4平均查找長度
    • 3.5模擬實作
  • 4.開散列
    • 4.1開散列概念
    • 4.2開散列擴容
    • 4.3總結(開散列和閉散列的比較)
    • 4.4代碼實作以及效果驗證

1.unordered_ map、unordered_set

1.1介紹

在C++98之中提供了四個關聯式容器,分別是:map、set、multimap、multiset,他們的底層是紅黑樹,查詢時候的效率可以達到longN,但是當資料量非常大的時候,也不是很理想,于是在C++11之中,又增加了四個用法一樣的關聯式容器,只是底層的實作采用的是哈希表,這四個關聯式容器分別是:unordered_map、unordered_set、unordered_multimap、unordered_multiset

它們的底層用的是哈希桶,因此迭代器是單向的

1.2效率對比

#include <iostream>
#include <set>
using namespace std;
#include <time.h>
#include <unordered_set>

int main()
{
	
	srand((unsigned)time(NULL));

	set<int> RBset;
	unordered_set<int> Hash;

	int begin1=clock();
	for (int i = 0; i < 500000;i++)
	{
		RBset.insert(rand() % 1000);
	}
	int end1 = clock();

	int begin2 = clock();
	for (int i = 0; i < 500000; i++)
	{
		Hash.insert(rand() % 1000);
	}
	int end2 = clock();


	cout << "set和unordered_set效率對比" << endl << endl;
	cout << "紅黑樹set:" << end1 - begin1 << endl << endl;
	cout << "哈希et:" << end2 - begin2 << endl;

	system("pause");
	return 0;
}

image-20210608101210628

1.3實戰演練

哈希常見面試題

2.哈希

2.1哈希的概念

map和set底層用的都是紅黑樹,進行元素的查找和插入都需要經過logN的時間復雜度,即需要通過元素之間的多次比較才能夠完成,

理想的搜索方法是,不經過任何比較,一次性直接從表中獲取元素,哈希就是這種設計理念,通過某種函式(hashFunc)使元素的存盤位置與它的關鍵碼之間能夠建立起對應的映射關系,在O(1)的時間內,插入、獲取元素

向結構中插入元素:
根據待插入元素的關鍵碼,用哈希函式計算出對應的存盤位置,并且按照此位置行程存放

搜索結構中的元素:
對元素關鍵碼進行計算,將函式回傳值作為元素的存盤位置進行搜索,如果對應的位置的元素與搜索元素可以匹配,則搜索成功

這種方法即為哈希(散列)方法,哈希方法中使用的函式稱之為哈希函式,構造出來的結構稱為哈希表

2.2哈希函式

2.2.1設計原則

哈希函式設計的不合理,會導致哈希沖突,哈希函式一般按照以下幾點原則進行設計:

1.哈希函式的定義域必須包括需要存盤的全部關鍵碼,而如果散串列允許有m個地址時,其值域必須在0到m-1之間

2.哈希函式計算出來的地址能夠均勻分布在整個空間之中

3.哈希函式應該比較簡單

2.2.2常見哈希函式

2.2.2.1常用哈希函式

1.直接定址法(常用)

取關鍵字的某個線性函式作為散列地址:hash(key)=A*key + B

優點:簡單、均勻
缺點:實作需要知道關鍵字的分布情況,并且只適合查找比較小,且連續分布的情況

場景:

適用場景:查找字串中,第一次出現的單詞:構建一個陣列 hash[ch-‘a’] 即為對應的地址

不適用場景:給一批資料, 1 5 8 100000 像這資料跨度大,資料元素不連續,很容易造成空間浪費

2.除留余數法(常用)

設散串列中允許的地址數為m,通常是取一個不大于m,但是最接近或者等于m的質數num,作為除數,按照哈希函式進行計算hash(key)= key%num, 將關鍵碼轉換成哈希地址

除留余數法,最好模一個素數:

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, 429496729ul
	};
	
	size_t GetNextPrime(size_t prime)
	{
		size_t i = 0;
		for (; i <PRIMECOUNT; ++i){
			if (primeList[i] > prime)
				return primeList[i];
		}

		return primeList[i];
	}

2.2.2.2 不常用哈希函式

1.平方取中法

hash(key)=key*key -> 然后取函式回傳值的中間的幾位,作為哈希地址
比如 25^2 = 625 取中間的一位 2 作為哈希地址

比較適合不知道關鍵字的分布,而位數又不是很大的情況

2.折疊法

將關鍵字從左到右分割成位數相等的幾部分(最后一部分可以短些),然后將這幾部分疊加求和,并且按照散串列長度,取最后幾位作為散列地址

適用于不知道關鍵字分布,關鍵字位數比較多的情況

3.亂數法

選取一個隨機函式,取關鍵字的隨機函式值,作為它的哈希地址,hash(key) = random(key),random為隨機函式

通常用于關鍵字長度不等的情況

4.數學分析法

通過實作分析關鍵字,來獲取哈希地址

比如用每個人的手機號碼充當關鍵字,如果采用前三位作為哈希地址,那么沖突的概率是非常大的,如果采用的是中間3位那么沖突的概率要小很多

常用于處理關鍵字位數比較大的情況,且事前知道關鍵字的分布和關鍵字的若干位的分布情況

2.3哈希沖突

image-20210608101302312

解決哈希沖突常見的兩種方法是閉散列和開散列

3.閉散列

閉散列也叫做開放地址法,當發生哈希沖突的時候,如果哈希表未被填滿,說明在哈希表中必然還有空位置,那么可以把key存放到沖突位置的"下一個"空位置中去,尋找下一個空位置的方法有線性探測法和二次探測法

3.1線性探測

從發生沖突的位置開始,依次向后探測,直到尋找到下一個位置為止

優點:實作非常簡單
缺點:一旦發生哈希沖突,所有的沖突連在一起,容易產生資料"堆積",即不同關鍵碼占據了可利用的空位置,使得尋找某關鍵碼的位置需要進行多次比較,導致搜索效率降低

image-20210608111205854

插入:

通過哈希函式插入元素在哈希表中的位置,如果發生了哈希沖突,則使用線性探測尋找下一個空位置插入元素

image-20210608104828926

洗掉:

采用閉散列處理哈希沖突時,不能隨便洗掉哈希表中已有的元素,如果直接洗掉元素,會影響其他元素的搜索
因此線性探測采用標記的偽洗掉法來洗掉下一個元素

image-20210608105445048

3.2閉散列擴容-載荷因子

散串列的載荷因子定義為 α = 填入表中的元素 / 散串列的長度

α是散串列裝滿程度的標志因子,α越大表明裝入表中的元素越多,產生沖突的可能性也就越大,反之填入表中的元素越少,沖突可能性越低,空間利用率也就越低

閉散列:一般將載荷因子控制在 0.7-0.8以下,超過0.8查表時候的快取不中率會按照指數曲線上升(哈希可能性沖突越大),因此一般hash庫中,都將α設定在0.8以下, 閉散列,千萬不能為滿,否則在插入的時候會陷入死回圈

開散列/哈希桶:一般將載荷因子控制在1,超過1,那么鏈表就掛得越長,效率也就越低

3.3二次探測

線性探測的缺陷是產生哈希沖突,容易導致沖突的資料堆積在一起,這是因為線性探測是逐個的找下一個空位置
二次探測為了緩解這種問題(不是解決),對下一個空位置的查找進行了改進(跳躍式查找):

POS = (H+i^2)%m || POS = (H - i^2)%m
其中:i=1、2、3…
H是發生哈希沖突的位置
m是哈希表的大小

image-20210608122036001

3.4平均查找長度

先將值放入哈希表之中

平均查找次數 = 查找每個值的次數總和/值的個數

3.5模擬實作

設計程序:

1.整體結構:

在這里插入圖片描述

2.擴容實作:

image-20210610172502295

3.演算法技巧
在這里插入圖片描述

4.封裝成set、map的代碼和驗證

image-20210610172657663

5.代碼和實驗效果

閉散列代碼:

#ifndef _HASH_HPP_
#define _HASH_HPP_

using namespace std;
#include <iostream>
#include <vector>
#include <assert.h>
#include <string> 


//閉散列
//set<K> -> HashTable<K,K>
//map<K,V> -> HashTable<K,pair<K,V>>
namespace YCH_CLOSE_HASH
{

	enum State
	{
		EMPTY,//空
		DELETE,//存在
		EXIST//洗掉
	};

	template<class T>
	struct HashNode//存盤的節點
	{
		State _state = EMPTY;//節點狀態,默認為空狀態(預設值)
		T _t;//節點的值

		HashNode(const T&t=T())
			:_t(t)
		{}

	};

	//迭代器的構造

	//提前宣告哈希表
	template <class K, class T, class KeyofT, class Hash>//宣告和定義不能同時候預設引數
	class HashTable;

	template<class K, class T, class KeyofT, class Hash>
	struct  HashIterator
	{
		typedef HashIterator<K, T, KeyofT, Hash> Self;
		typedef HashNode<T> Node;
		typedef HashTable<K, T, KeyofT, Hash> HashTable;

		Node *_node;
		HashTable *_pht;//迭代器里面還需要包括哈希表


		HashIterator(Node* node, HashTable *pth)
			:_node(node)
			, _pht(pth)
		{}

		T& operator*()
		{
			assert(_node != nullptr);
			return _node->_t;
		}

		T* operator ->()
		{
			assert(_node != nullptr);
			return &(_node->_t);
		}

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

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

		Self& operator++()//尋找當前位置的后面一個有元素的位置
		{
			//先找到當前節點所在位置,然后往后尋找下一個節點的位置
			KeyofT kf;

			size_t begin = _pht->HashFunc(kf(_node->_t));//得到映射的哈希位置
			//不清楚是否發生沖突因此還要再次尋找
			size_t index = begin;
			size_t i = 1;
			int flag = 1;

			while (_pht->_tables[index]._state != EMPTY)//等于空就停止,這也是為什么要用HashNode的原因,直接判斷,如果洗掉了,也會出現空
			{
				if (_pht->_tables[index]._state == EXIST&&kf(_node->_t) == kf(_pht->_tables[index]._t))//存在且K值對應
				{
					break;
				}
				
				index = (begin + i*i*flag) % _pht->_tables.size();//二次探測

				if (flag == -1)
				{
					i++;//增大值
					flag = 1;
				}
				else
				{
					flag = -1;//變換方向
				}
			}

			//此時index的位置就是第一個當前位置的資料,然后往后遍歷整張表,輸出元素
			for (int i = index+1; i < _pht->_tables.size(); i++)
			{
				if (_pht->_tables[i]._state == EXIST)
				{
					/*_node->_t = _pht->_tables[i]._t; //BUG,這是將原來節點里面的值也給更改了
					_node->_state = _pht->_tables[i]._state;*/
					
					_node = &(_pht->_tables[i]);
					return *this;
				}
			}

			//return _pht->End();這樣不行,沒有接受值,只是回傳了一個下一個位置的迭代器
			_node = nullptr;
			return *this;
		}

		Self operator++(int)//后置++
		{
			//先找到當前節點所在位置,然后往后尋找下一個節點的位置
			KeyofT kf;

			size_t begin = _pht->HashFunc(kf(_node->_t));//得到映射的哈希位置
			//不清楚是否發生沖突因此還要再次尋找
			size_t index = begin;
			size_t i = 1;
			int flag = 1;

			while (_pht->_tables[index]._state != EMPTY)//等于空就停止,這也是為什么要用HashNode的原因,直接判斷,如果洗掉了,也會出現空
			{
				if (_pht->_tables[index]._state == EXIST&&kf(_node->_t) == kf(_pht->_tables[index]._t))//存在且K值對應
				{
					break;
				}

				index = (begin + i*i*flag) % _pht->_tables.size();//二次探測

				if (flag == -1)
				{
					i++;//增大值
					flag = 1;
				}
				else
				{
					flag = -1;//變換方向
				}
			}

			//此時index的位置就是第一個當前位置的資料,然后往后遍歷整張表,輸出元素

			Node *copy_node = _node;//保存一份
			for (int i = index + 1; i < _pht->_tables.size(); i++)
			{
				if (_pht->_tables[i]._state == EXIST)
				{
					/*_node->_t = _pht->_tables[i]._t; //BUG,這是將原來節點里面的值也給更改了
					_node->_state = _pht->_tables[i]._state;*/

					_node = &(_pht->_tables[i]);


					return Self(copy_node, _pht);
				}
			}

			//return _pht->End();這樣不行,迭代器本身沒有改變,只是回傳了一個下一個位置的迭代器
			_node = nullptr;
			return Self(copy_node, _pht);
		}


	};

	//哈希表的構造


	size_t GetNextPrime(size_t prime)
	{
		static 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, 429496729ul
		};

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

		return primeList[i];
	}

	template<class K, class T, class KeyofT, class Hash>//默認給int進行比較
	class HashTable
	{
	public:
		//構造迭代器
		typedef HashIterator<K, T, KeyofT, Hash> Iterator;
		friend  Iterator; //<=>HashIterator<K, T, KeyofT, Hash>;//將迭代器宣告為,哈希表的友元類,即可以訪問哈希表的私有成員

		Iterator Begin()
		{
			for (int i = 0; i < _tables.size(); i++)//找到第一個不為空的值
			{
				if (_tables[i]._state == EXIST)
					return Iterator(&_tables[i], this);
			}
			return End();
		}

		Iterator End()
		{
			return Iterator(nullptr, this);
		}

		//構造插入函式

		size_t HashFunc(const K& key)//哈希函式
		{
			Hash hf;
			return hf(key) % _tables.size();
		}

		pair<Iterator,bool> Insert(const T & t)
		{
			//判斷要插入的元素是否已經存在
			KeyofT kf;
			Iterator ret = Find(kf(t));
			if (ret!=End())//已經存在了,multiset/multimap則不需要
				return make_pair(ret,false);
				
			//進行擴容檢測
			if (_size == 0 || (_size / _tables.size() * 10 > 7))//當前個數為0或者載荷因子超過了,則進行擴容
			{
				//size_t newsize = _size == 0 ? 10 : 2 * _tables.size();//初始化給10,后續擴容兩倍

				//選取素數
				size_t newsize = GetNextPrime(_tables.size());

				//擴容之后,需要重新計算元素的位置

				HashTable<K, T, KeyofT, Hash> newtable;
				newtable._tables.resize(newsize);

				for (auto&e : _tables)
				{
					if (e._state == EXIST)
						newtable.Insert(e._t);
				}
				_tables.swap(newtable._tables);//進行交換
			}


			//查找插入的位置

			//KeyofT kf;//獲取元素型別
			//Hash hf;//將元素轉為整形

			size_t begin = HashFunc(kf(t));//獲取映射位置
			size_t index = begin;
			size_t i = 1;
			int flag = 1;
			while (_tables[index]._state == EXIST)//發生沖突,則繼續尋找
			{
				index = (begin + i*i*flag) % _tables.size();//二次探測

				if (flag == -1)
				{
					i++;//增大值
					flag = 1;
				}
				else
				{
					flag = -1;//變換方向
				}
			}

			//此時已經找到位置了,進行元素的添加
			
			_tables[index]._t = t;
			_tables[index]._state = EXIST;
			_size++;

			return make_pair(Iterator(&_tables[index],this),true);
		}

		Iterator Find(const K& key)//查找的時候需要注意,查找的值不一定存在
		{
			if (_size == 0)//為空
				return Iterator(nullptr,this);

			//Hash hf;//轉整形
			KeyofT kf;//拿K值

			size_t begin = HashFunc(key);//轉為整形,獲取映射位置.
			size_t index = begin;
			size_t i = 1;
			int flag = 1;

			while (_tables[index]._state != EMPTY)//等于空就停止,這也是為什么要用HashNode的原因,直接判斷,如果洗掉了,也會出現空
			{
				if (_tables[index]._state == EXIST&&key == kf(_tables[index]._t))//存在且K值對應
				{
					return Iterator(&_tables[index],this);
				}

				index = (begin + i*i*flag) % _tables.size();//二次探測

				if (flag == -1)
				{
					i++;//增大值
					flag = 1;
				}
				else
				{
					flag = -1;//變換方向
				}
			}
			//當前值不存在
			return End();
		}

		bool Erase(const K& key)//先找到再洗掉
		{
			HashNode<T>* node = Find(key);
			if (node)
			{
				node->_state = DELETE;//偽洗掉
				_size--;
				return true;
			}
			return false;
		}


	private:
		vector<HashNode<T>> _tables;//底層結構
		size_t _size = 0;//存盤的資料的個數
	};
};


#endif

封裝:

map封裝:

#pragma once
#include "hash.hpp"


namespace YCH_MAP
{
	//內置哈希轉換函式 <-> 常用int 和 string 
	//如果K型別不支持取模,就需要配上一個仿函式來進行使用
	template<class K>
	struct Hash
	{
		size_t operator() (const K&key)
		{
			return key;
		}
	};

	//string型別常用,進行特化
	template<>
	struct Hash<string>
	{
		size_t operator() (const string &key)
		{
			size_t count = 0;
			for (auto&e : key)
			{
				count = count * 131 + e;// 字串轉整形求哈希地址常用值131,可以減少沖突
			}
			return count;
		}
	};

	template<class K, class V, class hash = Hash<K>>
	class ych_unordered_map
	{

	private:
		struct map_KeyofT
		{
			const K& operator()(const pair<const K, V>& kv)
			{
				return kv.first;
			}
		};

	public:
		typedef typename YCH_CLOSE_HASH::HashTable<K, pair<K, V>, map_KeyofT, 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);
		}


	private:
		YCH_CLOSE_HASH::HashTable<K, pair<K, V>, map_KeyofT, hash> _ht;

	};
};

set封裝:

#pragma once
#include "hash.hpp"


namespace YCH_MAP
{
	
	template<class K, class hash = Hash<K>>
	class ych_unordered_set
	{

	private:
		struct set_KeyofT
		{
			const K& operator()(const K& k)
			{
				return k;
			}
		};

	public:
		typedef typename YCH_CLOSE_HASH::HashTable<K, K, set_KeyofT, hash>::Iterator iterator;

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

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

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

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


	private:
		YCH_CLOSE_HASH::HashTable<K, K, set_KeyofT, hash> _ht;

	};
};

實驗檢測:

#include "hash.hpp"
#include "hashmap.hpp"
#include "hashset.hpp"


void test_map()
{

	YCH_MAP::ych_unordered_map<int, int> map;



	map.insert(make_pair(1, 1));
	map.insert(make_pair(2, 2));
	map.insert(make_pair(3, 3));
	map.insert(make_pair(4, 4));

	for (auto&e : map)
	{
		cout << e.first << " " << e.second << endl;
	}

	cout << "_______測驗2_______" << endl;

	YCH_MAP::ych_unordered_map<string, string> map2;
	map2.insert(make_pair("蘋果", "好吃"));
	map2.insert(make_pair("香蕉", "bu好吃"));
	map2.insert(make_pair("哈密瓜", "還可以"));
	map2.insert(make_pair("鳳梨", "也還可以"));
	map2.insert(make_pair("水蜜桃", "也還不錯"));

	for (auto&e : map2)
	{
		cout << e.first << " " << e.second << endl << endl;
	}
}

void test_set()
{
	cout << "-------set封裝測驗----" << endl;
	YCH_MAP::ych_unordered_set<int> set;

	set.insert(1);
	set.insert(12);
	set.insert(13);
	set.insert(10);
	set.insert(8);

	for (auto &e : set)
	{
		cout << e << endl;
	}

	cout << "_____查找10,并且輸出____" << endl;
	YCH_MAP::ych_unordered_set<int>::iterator it = set.find(10);
	if (it != set.end())
		cout << *it << endl;

}
int main()
{
	test_map();
	test_set();



	system("pause");
	return 0;
}

實驗效果:

image-20210610182737872

4.開散列

4.1開散列概念

開散列又名哈希桶/開鏈法,首先對關鍵碼集合采用散列函式計算散列地址,具有相同地址的關鍵碼歸于同一子集合,每一個子集合稱為一個桶,各個桶中的元素通過一個單鏈表串聯起來,各個鏈表的頭節點存盤在哈希表中

image-20210611200549093

4.2開散列擴容

桶的個數是一定的,不斷的插入元素,會導致單個的桶的長度很長,影響哈希表的性能,理想的情況下是每個桶下面只有一個節點,哈希桶的載荷因子控制在1,當大于1的時候就進行擴容,這樣平均下來,每個桶下面只有一個節點;

**與開散列進行比較:**看起來哈希桶之中存盤節點的指標開銷比較大,其實不然,開散列的載荷因子保證小于0.7,來確保有足夠的空間降低哈希沖突的概率,而表項的空間消耗遠遠高于指標所占的空間效率,因此哈希桶更能節省空間

image-20210611205857742

4.3總結(開散列和閉散列的比較)

**1.開散列的載荷因子:**α<=1,即平均每個桶下面掛一個節點,平均時間復雜度為1

2.開散列產生哈希沖突時:直接頭插至鏈表之中,而閉散列就會出現哈希沖突,容易出現踩踏效應(二次探測也只是緩解這種情況)

3.開散列的優缺點:

優點:
不同位置沖突時,不再互相干擾,載荷因子一般控制在1

缺點:
迭代器遍歷輸出的時候,不是有序輸出的,

**延伸:**要想做到有序輸出,那么必須再用個list保存一份,哈希表里面存個地址指向對應的list,輸出的時候就輸出list里面的內容,但是這樣的空間和時間消耗的代價就更大了

**4.開散列的優化:**如果所有的資料都沖突到一個桶下面了,怎么辦?

1.在桶下面掛紅黑樹:極限也是lgN的時間復雜度,但是也只是這一會而已,當增容的時候,這種現象就會緩解

2.多階哈希:多個哈希表,沖突的時候,掛到另外一個哈希表上,長度不一樣,對應的位置就不一樣

image-20210611205935284

5.開散列增容那一下的性能

image-20210611205920197

4.4代碼實作以及效果驗證

#ifndef _HASH_HPP_
#define _HASH_HPP_

using namespace std;
#include <iostream>
#include <vector>
#include <assert.h>
#include <string> 

static size_t GetNextPrime(size_t prime)
{
	static 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, 429496729ul
	};

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

	return primeList[i];
}

namespace YCH_OPEN_HASH
{
	//節點
	template<class T>
	struct HashLink
	{
		HashLink<T> *_next;
		T _t;

		HashLink(const T& t)
			:_t(t)
			,_next(nullptr)
		{}
	};

	//前置宣告
	template<class K, class T, class KeyofT, class Hash>
	class HashTable;

	//迭代器
	template<class K, class T,class Ref,class Ptr ,class KeyofT, class Hash>
	struct HashIterator
	{
		typedef HashLink<T> Node;
		typedef HashTable<K, T, KeyofT, Hash> HashTable;
		typedef HashIterator<K, T, Ref, Ptr, KeyofT,Hash> Self;

		Node *_node;//節點
		HashTable *_pht;//哈希表指標,++的時候需要計算位置

		HashIterator(Node *node,HashTable* tables)//建構式需要傳入節點指標,和哈希表指標
			:_node(node)
			, _pht(tables)
		{}

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

		Ptr operator->()
		{
			assert(_node);
			return &(_node->_t);//回傳去的是一個地址,使用的時候編譯器優化,減少了一個箭頭
		}

		KeyofT kf;
		Self &operator++()//前置
		{
			size_t pos = _pht->HashFunc(kf(_node->_t),_pht->_tables);//獲取當前位置
			pos++;
			_node = _node->_next;
			if (_node == nullptr)//當前鏈表走完了,尋找下一個節點
			{
				for (int i = pos; i < _pht->_tables.size(); i++)
				{
					if (_pht->_tables[i] != nullptr)
					{
						_node = _pht->_tables[i];
						break;
					}
				}
			}
			return *this;
		}

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

	};



	template<class K, class T, class KeyofT,class Hash>
	class HashTable
	{
	public:
		typedef HashLink<T> Node;
		typedef HashIterator<K, T, T&, T*, KeyofT, Hash> Iterator;
		typedef HashIterator<K, T, const T&, const T*, KeyofT, Hash> Const_Iterator;
		
		//迭代器友元
		friend HashIterator<K, T, T&, T*, KeyofT, Hash>;
		friend Const_Iterator;


		Iterator Begin()
		{
			for (int i = 0; i < _tables.size(); i++)
			{
				if (_tables[i] != nullptr)
					return Iterator(_tables[i], this);
			}
			return Iterator(nullptr, this);
		}

		Iterator End()
		{
			return Iterator(nullptr, this);
		}


		KeyofT kf;//提取key值

		//哈希函式
		size_t HashFunc(const K& key,const vector<HashLink<T>*> tables)
		{
			Hash hf;
			return hf(key) % tables.size();
		}

		pair<Iterator,bool> Insert(const T& t)
		{
			//判斷是否存在
			if (_tables.size())//防止%0
			{
				Iterator fi = Find(kf(t));
				if (fi != End())//存在
					return make_pair(fi, false);
			}

			//判斷是否需要擴容
			if (_size == _tables.size())//α<=1
			{
				size_t newsize = GetNextPrime(_tables.size());
				vector<Node*> newtables(newsize, nullptr);//構造一個新表出來

				for (int i = 0; i < _tables.size(); i++)
				{
					Node* node = _tables[i];
					while (node)//當前位置有節點
					{
						Node *next = node->_next;//保存當前鏈表的下一個位置

						size_t index = HashFunc(kf(node->_t),newtables);//得到位置
						node->_next = newtables[index];
						newtables[index] = node;

						node = next;
					}
					_tables[i] = nullptr;//原表置空
				}

				//兩表交換
				newtables.swap(_tables);
			}

			//插入節點

			size_t index = HashFunc(kf(t),_tables);//尋找插入位置
			Node *newnode = new Node(t);//構造一個節點
			newnode->_next = _tables[index];//頭插
			_tables[index] = newnode;
			_size++;

			return make_pair(Iterator(_tables[index], this), true);
		}

		//查找
		Iterator Find(const K& k)
		{
			size_t index=HashFunc(k, _tables);

			Node *cur = _tables[index];
			while (cur&&(kf(cur->_t) !=k))
			{
				cur = cur->_next;
			}
			return Iterator(cur, this);
		}

		洗掉
		bool Erase(const K& k)
		{
			size_t intdex = HashFunc(k, _tables);
			Node* cur = _tables[index];
			Node *prev = nullptr;
			while (cur&&kf(cur->_t) != k)
			{
				prev = cur;
				cur = cur->_next;
			}
			if (cur == nullptr)//沒找到
				return false;

			prev->_next = cur->_next;
			delete cur;
			_size--;
			return true;
		}

	private:
		vector<Node*> _tables;
		size_t _size;
	};
};

#endif

#pragma once
#include "hash.hpp"


namespace YCH_MAP
{
	//內置哈希轉換函式 <-> 常用int 和 string 
	//如果K型別不支持取模,就需要配上一個仿函式來進行使用
	template<class K>
	struct Hash
	{
		size_t operator() (const K&key)
		{
			return key;
		}
	};

	//string型別常用,進行特化
	template<>
	struct Hash<string>
	{
		size_t operator() (const string &key)
		{
			size_t count = 0;
			for (auto&e : key)
			{
				count = count * 131 + e;// 字串轉整形求哈希地址常用值131,可以減少沖突
			}
			return count;
		}
	};

	template<class K, class V, class hash = Hash<K>>
	class ych_unordered_map
	{

	private:
		struct map_KeyofT
		{
			const K& operator()(const pair<const K, V>& kv)
			{
				return kv.first;
			}
		};

	public:
		typedef typename YCH_OPEN_HASH::HashTable<K, pair<const K, V>, map_KeyofT, hash>::Iterator iterator;

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

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

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

		V& operator[](const K& k)
		{
			return ((insert(make_pair(k, V()))).first)->second;
		}


	private:
		YCH_OPEN_HASH::HashTable<K, pair<const K, V>, map_KeyofT, hash> _ht;

	};
};
#pragma once
#include "hash.hpp"


namespace YCH_MAP
{
	
	template<class K, class hash = Hash<K>>
	class ych_unordered_set
	{

	private:
		struct set_KeyofT
		{
			const K& operator()(const K& k)
			{
				return k;
			}
		};

	public:
		//typedef typename YCH_CLOSE_HASH::HashTable<K, K, set_KeyofT, hash>::Iterator iterator;閉散列
		typedef typename YCH_OPEN_HASH::HashTable<K, K, set_KeyofT, hash>::Iterator iterator;//開散列


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

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

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

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


	private:
		//YCH_CLOSE_HASH::HashTable<K, K, set_KeyofT, hash> _ht;
		YCH_OPEN_HASH::HashTable<K, K, set_KeyofT, hash> _ht;


	};
};


#include "hash.hpp"
#include "hashmap.hpp"
#include "hashset.hpp"

void test_open_map()
{
	cout << "____YCH_MAP::ych_unordered_map<int, int> map___" << endl ;
	YCH_MAP::ych_unordered_map<int, int> map;
	
	map.insert(make_pair(1, 1));
	map.insert(make_pair(54, 54));
	map.insert(make_pair(55, 55));
	map.insert(make_pair(56, 56));
	map.insert(make_pair(54, 54));
	map.insert(make_pair(108, 108));

	auto it = map.begin();

	while (it != map.end())
	{
		cout << it->first << " " << it->second << endl;
		++it;
	}
	cout << endl;


	cout << "____[]測驗____<<endl";
	map[500]++;
	map[200]=100;
	map[111] = 153;

	for (auto&e : map)
	{
		cout << e.first<<" "<<e.second<< endl;
	}
}

void test_open_set()
{
	cout << "_____YCH_MAP::ych_unordered_set<string> set______ " << endl;
	YCH_MAP::ych_unordered_set<string> set;
	set.insert("排序");
	set.insert("字串");
	set.insert("演算法");
	set.insert("演算法");
	set.insert("字串");
	set.insert("哈希表");

	for (auto&e : set)
	{
		cout << e << endl;
	}
}

int main()
{
	test_open_map();	
	test_open_set();



	system("pause");
	return 0;

}

在這里插入圖片描述

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/287187.html

標籤:其他

上一篇:你可能不知道這三個好用的python函式

下一篇:【新冠疫苗預約】Fiddler抓包新冠疫苗預約介面及腳本實作

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more