数据结构之哈希表

1.什么是哈希表

HashTable-散列表/哈希表,是根据关键字(key)而直接访问在内存存储位置的数据结构。

它通过一个关键值的函数将所需的数据映射到表中的位置来访问数据,这个映射函数叫做散列函数,存放记录的数组叫做散列表。

【常见哈希函数】

>> 直接定址法取关键字的某个线性函数为散列地址,Hash(Key)= Key 或 Hash(Key)= A*Key +B,A、B为常数。这样的散列函数优点是简单、均匀,也不会产生冲突,但问题是需要事先知道关键字的分布情况,适合查找表较小且连续的情况。

>> 除留余数法设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key % p p<=m,将关键码转换成哈希地址。

>> 平方取中法假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227作为散列地址;再比如关键字是4321,那么它的平方就是18671041,抽取中间的3位就可以是671或者710用作散列地址。平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况。

>> 折叠法折叠法是将关键字从左到右分割成位数相等的几部分(注意:最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。比如:关键字是9876543210,散列表表长为三位,我们将它分成四组987|654|321|0|,然后将它们叠加求和987+654+321+0=1962,再求后3位得到散列地址为962。有时可能这还不能够保证分布均匀,不妨从一段向另一端来回折叠后对齐相加。比如将987和321反转,再与654和0相加,编程789+654+123+0=1566,此时的散列地址为566。折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。

>> 随机数法选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数,通常应用于关键字长度不能采用此法。

>> 数学分析法设有n个d位数,每一位可能有r中不同的符号,这r中不同的符号在个位上出现的频率不一定相同,可能在某些位置上分布均匀写,每种符号出现的机会均等;在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。比如:假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现冲突,还可以对出去出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等方法,总之:为了提供一个散列函数,能够合理的将关键字分配到散列表的 位置。数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况。

在这不得不说的是一个叫做哈希冲突的问题:因为不同的key通过散列函数所计算的位置很可能相同,所以这多个值就会造成冲突,我们也就叫做哈希冲突,为了解决哈希冲突,研究了这几种方法。

这里写图片描述

哈希解决冲突方法:开放定址法和拉链法。

开放定址法:

也叫做闭散列法,我们在这里提供两种思路,一种是线性探测,一种是二次探测。这两种方法删除元素都不是真正的删除元素,都是标记删除。

线性探测:线性探测说的就是,当你插入的一个数和所在位置上的数冲突了,这个时候,你就向后找,找到为空的位置进行存放这个数。

二次探测:使用二次探查法,在表中寻找“下一个”空桶的公式为:Hi = (H0 + i^2)%m, Hi = (H0 - i^2)%m, i = 1,2,3…,(m-1)/2


线性探测代码:

#pragma  once

#include <iostream>
using namespace std;

enum Stat{EMPTY,EXIST,DELETE};  //元素状态。

template<typename T>
class HashTable
{
public:
	HashTable()
	{
		_hashtable = new T[10];
		_size = 0;
		_capacity = 10;
		_states = new Stat[10];
		memset(_hashtable,0,_capacity*sizeof(T));
		memset(_states,0,40);  //开始每个元素的状态都设置为EMPTY.
	}

	~HashTable()
	{
		if (_hashtable != NULL)
		{
			delete [] _hashtable;
			_hashtable = NULL;
		}
		if (_states != NULL)
		{
			delete [] _states;
			_states = NULL;
		}
	}

	bool Insert(const T& key)
	{
		if (_capacity == _size)
		{
			return false;
		}

		size_t index = HashFun(key);

		while (_states[index] == EXIST)
		{
			if (_hashtable[index] == key)  //哈希表中有这个元素,不需要再次插入。
			{
				return false;
			}

			++index;          //线性探测
			index %= _capacity;  //到哈希表末尾没有空的位置可以插入的话,再次从前面查找是否有插入位置。
		}

		_hashtable[index] = key;
		_size++;
		_states[index] = EXIST;
		return true;
	}

	bool Find(const T&key,size_t & index)
	{
		 index = HashFun(key);  //将查找的元素的下标带出去。
		size_t hashindex = index;

		while (_states[index] != EMPTY) //查找的时候是查找到一个空的位置就停下来了,所以删除的时候不能真正的删除,只能标记删除。
		{
			if (_hashtable[index] == key && _states[index] == EXIST)//查找的元素可能被删除了,此时应该返回false.
			{
				return true;
			}

			++index;
			index %= _capacity;  //查找到哈希表的末尾都没有找到的话,再看看前面有没有要查找的元素。

			if (hashindex == index)  //找了一圈都没有找到要查找的元素,跳出循环。没有这个条件的话,当哈希表满了却没有要查找的元素会死循环。
			{
				return false;
			}
		}

		return false;
	}

	bool Remove(const T& key)
  	{
		size_t index = 0;
		if (Find(key,index) )
		{
			_states[index] = DELETE;
			_size--;
			return true;
		}
		return false;
	}

private:

	size_t HashFun(const T& key)  //哈希函数。
	{
		return key%_capacity;
	}
	

private:
	T *_hashtable;
	Stat *_states;
	size_t _capacity;
	size_t _size;

};
二次探测代码:

#pragma  once 
#include<iostream>
using namespace std;
#include<string>

enum State{EMPTY, EXIST, DELETE};


template<class K>
class HashFunDef
{
public:
	size_t operator()(const K &key)
	{
		return key;
	}
};

//字符串转换为整型的一个函数。
static size_t BKDRHash(const char *str)
{
	unsigned int seed = 131;
	unsigned int hash = 0;
	while (*str)
	{
		hash = hash *seed +(*str++);
	}
	return (hash & 0x7FFFFFFF);
}


template<>
class HashFunDef<string>  //函数对象的特例化,特例化为字符串
{
public:
	size_t operator()(const string &key)
	{
		return BKDRHash(key.c_str());
	}

};

template<class K,class V,class Fun = HashFunDef<K> >
class HashTable
{
public:
	HashTable(size_t capacity = 10):_capacity(capacity),_size(0)
	{
		_capacity = GetNextPrim(_capacity);
		_hashTable = new pair<K,V>[_capacity];
		_state = new State[_capacity];
		memset(_state,0,sizeof(State)*_capacity);
	}

	//拷贝构造函数
	HashTable(HashTable<K,V> & ht):_hashTable(NULL),_state(NULL)
	{
		HashTable<K,V> temp(ht._capacity);

		for (size_t index = 0; index < ht._capacity; ++index)
		{
			if (ht._state[index] == EXIST)
			{
				temp.Insert(ht._hashTable[index].first,ht._hashTable[index].second);
			}
		}

		swap(_state,temp._state);
		swap(_hashTable,temp._hashTable);
		swap(_size,temp._size);
		swap(_capacity,temp._capacity);
	}

	~HashTable()
	{
		if (_hashTable != NULL)
		{
			delete [] _hashTable;
			_hashTable = NULL;
		}
		if (_state != NULL)
		{
			delete [] _state;
			_state = NULL;
		}
	}

	bool Insert(const K &key,const V & value)
	{
		//要检查哈希冲突,扩容。
		CheckCapacity();
		size_t hashIdx = HashFun1(key);  //插入位置。
		size_t i = 1;

		while (_state[hashIdx] == EXIST)
		{
			if (_hashTable[hashIdx].first == key) //如果这个key存在哈希表中,不用插入,
			{
				return false;
			}
			hashIdx = HashFun2(hashIdx,i++); //冲突后找空位置。
		}
		
		_hashTable[hashIdx] = pair<K,V>(key,value);  //插入元素。这个元素就是键值对。
		_state[hashIdx] = EXIST;
		_size++;

		return true;
	}

	bool Find(const K & key, size_t& index)//在哈希表中找key,并将找到元素的下标通过index带回。
	{
		size_t Hashidx = HashFun1(key);
		size_t i  =1 ;

		while (_state[Hashidx]  != EMPTY)
		{
			if (_state[Hashidx] == EXIST && _hashTable[Hashidx].first == key)
			{
				index = Hashidx;
				return true;
			}
			Hashidx = HashFun2(Hashidx,i++);
		}

		return false;
	}

	void PrintHashTable()
	{
		for (size_t idx = 0; idx < _capacity; ++idx)
		{

			if (_state[idx] == EXIST)
			{
				cout << "key = " << _hashTable[idx].first << " : " <<"value = " << _hashTable[idx].second <<endl;
			}
		}
	}

	bool Remove(const K &key)
	{
		size_t index = 0;
		if (Find(key,index))
		{
			_state[index] = DELETE;
			_size--;
			return true;
		}
		return false;
	}





private:
	//首先还是要通过哈希函数判断key值插在什么位置,如果冲突了,就要二次探测。
	int HashFun1(const K & key)
	{
	//	return key % _capacity;
		return ( Fun()(key) % _capacity ) ; //这里用了一个函数对象是因为如果key是字符串不能直接求余,先要把字符串转换为对应的整数。
	}
	//二次探测 H(i) = H0 +i*i
	//         H(i-1) = H0 + (i-1)*(i-1)
	//         H(i) - H(i-1) = 2*i-1;   H(i) = H(i-1) +2*i-1;
	//        
	int HashFun2(size_t prevHashIdx,size_t i)
	{
		return (prevHashIdx + (i << 1) -1 ) % _capacity;//这里模上_capacity是因为H(i-1) +2*i-1可能超出哈希表的长度,所以要模。
	}

	//0.7~0.8
	void CheckCapacity()
	{
		size_t capacity;
		if ( (double)_size / _capacity >= 0.7 )  //负债因子 = 存放的元素个数/表长 当负载因子>= 0.7的时候就扩容。
		{
			capacity = GetNextPrim(_capacity);

			HashTable<K,V>temp(capacity);
			//int index = 0;
			for (int index = 0; index < _capacity; ++index)
			{
				if (_state[index] == EXIST)
				{
				//	temp._hashTable[index] = _hashTable[i];   不能这么直接插入元素,完全把插入位置搞错了,到时候通过key%_capacity就找不到要找的元素了。
				//	temp._state[index] = EXIST;
				//	index++;

					temp.Insert(_hashTable[index].first,_hashTable[index].second); //通过键值插入

				}
			}

			swap(_state,temp._state);
			swap(_hashTable,temp._hashTable);
			//_size = temp._size;
			//_capacity = temp._capacity;
			swap(_size,temp._size);
			swap(_capacity,temp._capacity);

		}
	}


	size_t GetNextPrim(size_t prev) 
	{
		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
		}; //素数表

		for (size_t idx = 0; idx < _PrimeSize; ++idx)
		{
			if (prev < _PrimeList[idx])
			{
				return _PrimeList[idx];
			}
		}
		return -1;
	}


private:
	pair<K,V> *_hashTable; //此时的哈希表存放的是键值对。
	State *_state;
	size_t _capacity;
	size_t _size;

};

#include "hashtable的二次探测.hpp"



void test2()
{
	HashTable<int,int> ht(100);
	ht.Insert(1,1);
	ht.Insert(26,2);
	ht.Insert(31,3);
	ht.Insert(42,4);
	ht.Insert(56,5);
	ht.Insert(62,6);
	ht.Insert(74,7);
	ht.Insert(20,200);
	
	ht.PrintHashTable();
	ht.Remove(42);
	HashTable<int,int> ht2(ht);
	cout <<endl <<endl;
	ht2.PrintHashTable();

	HashTable<int,string> ht3;
	ht3.Insert(11,"huangjia");
	ht3.Insert(24,"doew");
	cout <<endl <<endl;
	ht3.PrintHashTable();
	ht3.Remove(11);
	cout <<endl <<endl;
	ht3.PrintHashTable();


	HashTable<string, string,HashFunDef<string> > ht1;
	ht1.Insert("hash", "哈希");
	ht1.Insert("哈希", "hash");
	ht1.PrintHashTable();

	size_t idx = 0;
	ht1.Find("哈希", idx);

}



int main()
{
	test2();

	cout << "hello..." <<endl;
	system("pause");
	return 0;
}






  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值