1. 程式人生 > >【資料結構】雜湊表/散列表

【資料結構】雜湊表/散列表

本篇博文,旨在介紹雜湊表的基本概念及其用法;介紹了減少雜湊衝突的方法;並用程式碼實現了雜湊表的線性探測和雜湊桶

雜湊表的基本概念

雜湊表是一種儲存結構,它通過key值可以直接訪問該key值在記憶體中從儲存位置;

將關鍵值對映到表中的位置訪問資料,這個對映函式叫做雜湊函式,儲存資料的表叫做散列表;

構造雜湊表的幾種方法

1、直接定址法

根據關鍵值直接確定元素存取的位置,該雜湊函式是一個線性函式;例如 Hash(key) = A*key+B,其中,A和B都為常數

2、除留餘數法

取出關鍵值後,將關鍵值除以該散列表的長度從而獲得的餘數,作為雜湊地址;Hash(key) = key%m

3、平方取中法

取關鍵字平方後的中間幾位為雜湊地址。由於一個數的平方的中間幾位與這個數的每一位都有關,因而,平方取中法產生衝突的機會相對較小。

平方取中法中所取的位數由表長決定。

例: K = 456 , K2 = 207936 若雜湊表的長度m=102,則可取79(中間兩位)作為雜湊函式值。

4、摺疊法

將關鍵字分割成位數相同的幾部分,最後一部分位數可以不同,然後取這幾部分的疊加和(去除進位)作為雜湊地址。數位疊加可以有移位疊加和間界疊加兩種方法。 

5、隨機數法

選擇一隨機函式,取關鍵字的隨機值作為雜湊地址,通常用於關鍵字長度不同的場合。

6、數學分析法

找出數字的規律,儘可能利用這些資料來構造衝突機率較低的雜湊地址。


雜湊衝突/雜湊碰撞

雜湊衝突出現的原因

關鍵值通過任何的雜湊函式獲得的雜湊地址都有可能是重複的,這種情況叫做雜湊衝突;

載荷因子

散列表的載荷因子的定義為:a = 表中的元素個數/散列表的長度

對於開放定址法,載荷因子特別重要,一般情況下嚴格控制在0.7-0.8

超過0.8,雜湊表的效率會很低

過低的話,浪費的空間會更大


任何的雜湊函式都是無法避免的,在處理雜湊衝突的方法中,我們介紹下面兩種方法

a 用開放定址法來處理雜湊衝突

a1  線性探測

當有需要插入元素的位置已經有元素存在時,則向後遍歷尋找空位置


a2  二次探測

當插入位置衝突時,則向後尋找,但不是每次只挪動一個位置,而是衝突次數的平方次(1,4,9....)


b 用開鏈法處理雜湊衝突

雜湊桶是一種順序表和連結串列的結合體,定義一個數組,陣列儲存的內容是節點的指標,下面用一個個桶來儲存元素;

這樣可以提高雜湊表的效率


如何減少雜湊衝突

a 素數

使用素數做除數可以有效的減少雜湊衝突

//素數表
const int _PrimeSize = 28;
static const unsigned long _PrimeList[_PrimeSize] =
{
	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
};

b 字串雜湊演算法

字串雜湊演算法就是將一個字串轉化成一個Key值

字串雜湊演算法有很多

下面這個連結有詳細的介紹

程式碼實現

開放定址法的線性檢測

節點的定義:

//定義列舉,表示節點的狀態
enum Status
{
	EXIST,
	DELETE,
	EMPTY,
};

//節點的定義
template<typename K,typename V>
struct HashNode
{
	K _key;
	V _value;
	Status _status;

	HashNode(const K& key = K(), const V& value = V())
		:_key(key)
		, _value(value)
		, _status(EMPTY)
	{}
};

狀態位標識著這個位置有沒有值存在,還是之前存在已被刪除

雜湊表的定義:

//HashFunc是採用的字串雜湊演算法
template<typename K, typename V, typename HashFunc = __HashFunc<K>>
class HashTable
{
	typedef HashNode<K, V> Node;
public:
	//預設的建構函式
	HashTable()
	{}

	//建構函式
	HashTable(size_t size);

	//將K值轉換成雜湊值
	size_t HashFunC(const K& key);

	//插入一組值
	pair<Node*, bool> Insert(const K& key,const V& value);

	//查詢元素
	Node* find(const K& key);

	//刪除一個節點
	void Remove(const K& key);

protected:
	vector<Node> _v;//用vector的下標標誌位置
	size_t _size;//雜湊表中元素的數量
protected:
	//交換兩個雜湊表
	void Swap(HashTable<K, V> &h);
	
	//進行容量的判別,進行擴容
	void CheckCapacity();
};

插入函式的實現:

	pair<Node*, bool> Insert(const K& key,const V& value)
	{
		//檢查是否需要擴容
		CheckCapacity();

		//對K值進行取餘,判斷插入的位置
		size_t index = HashFunC(key);

		//如果存在,則迴圈著繼續尋找
		while (_v[index]._status == EXIST)
		{
			index++;
			if (index == _v.size())
				index = 0;
		}

		_v[index]._key = key;
		_v[index]._value = value;
		_v[index]._status = EXIST;

		_size++;

		return make_pair<Node*,bool>(&_v[index] ,true);
	}

插入函式,要找到關鍵值對應的位置,衝突的話後移;找到後放入對應的Key和value值,並且將狀態位改變即可

查詢函式的實現:

        Node* find(const K& key)
	{
		//對K值進行取餘,判斷插入的位置
		size_t index = HashFunC(key);

		//如果存在,則繼續尋找
		while (_v[index]._status == EXIST)
		{
			//若相等,判斷狀態是否是刪除
			//若刪除,則沒找到,返回空
			//若沒刪除,則返回該位置的地址
			if (_v[index]._key == key)
			{
				if (_v[index]._status == DELETE)
					return NULL;

				return &_v[index];
			}

			index++;
			if (index == _size)
				index = 0;
		}

		return &_v[index];
	}

線性探測中,查詢這塊有個小問題。當一個關鍵值是因為衝突後移的話,若後移中的某個值之後刪除了,就無法找到這個值了

刪除函式的實現:

        void Remove(const K& key)
	{
		//刪除僅需要將狀態修改
		Node* delNode = find(key);

		if (delNode)
			delNode->_status = DELETE;
	}

先查詢是否存在,存在的話只需要將狀態位修改成刪除即可

擴容函式的實現:

載荷因子超過0.7時,進行擴容

	void CheckCapacity()
	{
		//如果_v為空,則擴容到7
		if (_v.empty())
		{
			_v.resize(_PrimeList[0]);
			return;
		}

		//如果超過載荷因子,則需要擴容
		if (_size*10 / _v.size() >= 7)
		{
			/*size_t newSize = 2 * _v.size();*/

			size_t index = 0;
			while (_PrimeList[index] < _v.size())
			{
				index++;
			}
			size_t newSize = _PrimeList[index];

			//用一個臨時變數來儲存新生成的雜湊表
			//生成完成後,將其和_v交換
			HashTable<K, V> tmp(newSize);
			for (size_t i = 0; i < _v.size(); ++i)
			{
				//如果存在,則將該位置的雜湊值插入到臨時的雜湊表中
				if (_v[i]._status == EXIST)
					tmp.Insert(_v[i]._key,_v[i]._value);
			}
			//交換兩個雜湊表
			Swap(tmp);
		}
	}

開鏈法 雜湊桶

節點的定義:

//節點的定義
template<class K, class V> 
//struct HushNode  e2:拼寫錯誤,Hash not hush
struct HashNode  
{
	K _key;
	V _value;
	HashNode<K, V>* _next;
	HashNode(const K& key, const V& value)
		:_key(key)
		, _value(value)
		, _next(NULL)
	{}
};

插入函式的實現:

//插入函式的實現
	pair<Node*, bool> Insert(const K& key, const V& value)
	{
		//插入之前要先進行是否擴容的檢查
		CheckCapacity();
		//找到這個數的位置
		size_t index = _HushFunc(key);
		Node* cur = _ht[index];
		while (cur)
		{
			if (cur->_key == key)
				return make_pair(cur, false);
			cur = cur->_next;
		}
		Node* temp = new Node(key, value);
		temp->_next = _ht[index];
		_ht[index] = temp;
		return make_pair(temp, true);
	}

用pair可以返回插入成功後節點的指標(或插入失敗,相同關鍵值的指標),以及判斷插入成功與否的TRUE或FALSE

刪除函式的實現:

	//刪除函式的實現
	void Erase(const K& key)
	{
		//先進行查詢這個數的位置
		size_t index = _HushFunc(key);
		Node* pre = NULL;
		Node* cur = _ht[index];
		//刪除的時候要進行情況的分析討論
		while (cur)
		{
			if (pre == NULL)
			{
				_ht[index] = cur->_nex;
				delete cur;
			}
			else
			{
				pre->_next = cur->_next;
			}
			cur = cur->_next;

		}
                delete cur;
	}

刪除元素的時候,需要分兩種情況

a、鏈上有且僅有該節點

出現這種情況,就是pre為空時,僅需修改vector對應位置儲存的指標

b、鏈上除該節點外,還有多個節點

將cur的指向下一個節點的指標給pre的下一個

查詢函式的實現:

	//查詢函式的實現
	Node* Find(const K& key)
	{
		for (int i = 0; i<_size; i++)
		{
			Node* cur = _ht[i];
			while (cur)
			{
				if (cur->_key == key)
					return cur;
				cur = cur->_next;
			}
		}
		return NULL;
	}

擴容函式的實現:

	void CheckCapacity()
	{
		//當雜湊桶為空的時候或者負載因子為1的時候,進行擴容
		if (_ht.size() == 0 || _size / _ht.size() == 1)
		{
			//建一個新表
			HushTable temp;
			size_t newsize = GetNewSize();
			temp._ht.resize(newsize);

			//將元素放進去
			for (int i = 0; i<_ht.size(); i++)
			{
				Node* cur = _ht[i];
				while (cur)
				{
					temp.Insert(cur->_key, cur->_value);
					cur = cur->_next;
				}
			}
		}
	}

與線性探測不同的是,這裡當載荷因子為1的時候進行擴容;

雜湊桶的一種極端情況是,所有值都往同一個位置進行插入

如下:


解決方法:

當一個鏈上的節點數量達到一個百分比(自己定義,可以是100%,50%,10%),則不再儲存連結串列,而是儲存一顆紅黑樹

這一避免出現這種極端情況而帶了的弊端

雜湊表的github程式碼連結

雜湊桶的github程式碼連結