首页 > 学院 > 开发设计 > 正文

数据结构::关于哈希表

2019-11-08 01:04:14
字体:
来源:转载
供稿:网友

学习完二叉搜索树后,我们来学习一种新的结构,哈希表

【前言】:

二叉搜索树具有lgn的搜索时间复杂度,但是是建立在数据有足够的随机性哈希表是也是具有对数时间复杂度的,但是它是以统计为基础的,并不在乎输入数据的随机性

【哈希表】:

1、定义:

*HashTable-散列表/哈希表,是根据关键字(key)而直接访问在内存存储位置的数据结构。*它通过一个关键值的函数将所需的数据映射到表中的位置来访问数据,这个映射函数叫做散列函数,存放记录的数组叫做散列表。

*哈希表可以看作是字典结构,它操作的是有名项,提供常数时间的基本操作

2、构造哈希表的几种方法:

1)直接定址法: 该方法是取关键字的某个线性函数值为哈希地址 2)除留余数法: 它是用数据元素关键字除以某个常数所得的余数作为哈希地址。 3)折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。数位叠加可以有移位叠加和间界叠加两种方法。 4)随机数法::选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。 5)*数学分析法:找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。

一般常用的方法是直接定址法和数学分析法。

取关键字的某个线性函数为散列地址,Hash(Key)= Key 或 Hash(Key)= A*Key + B,A、B为常数。

3、哈希冲突:

1)出现的原因:

当我们通过散列函数进行映射的时候,不同的键值会映射到同一个位置

我用图示说明下:

2)如何进行解决:

先介绍一个概念:负载因子

散列表的负载因子:x = 填入表中的元素的个数、散列表的长度

说明:

A:x是散列表装满程度的标志因子。由于表长是定值,x与“填入表中的元素个数”成正比,所以,x越大,表明填入表中的元素越多,产生冲突的可能性就越大;反之,x越小,标明填入表中的元素越少,产生冲突的可能型就越小。

B:对于开放定址法,负载因子是特别重要的因素,一般严格限制在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中按照指数曲线上升。因此,一些采用开放定址法的hash库,超过的话,就进行resize散列表。

C:要进行说明的一个关于负载因子的悖论,既然负载因子是这样的话,那么我们可以将负载因子设的小一点,这样的话,效率就会很高,但是这样同时会很浪费空间,所以其实这里面也是相互制约的。

A:闭散列法--也称开放定址法

  a:线性探测:

     线性探测就是当你插入一个数的位置有冲突了,那么这个时候,你就向后找,找到一个空的没有元素插入的位置进行插入。

  b:二次探测:

     是用来解决线性探测的问题的,由于线性探测在进行插入的时候,如果插入到大于负载因子的值时,就要进行扩容。下面我来用图解说明下二次探测的做法:

B:开链法(哈希桶):

为了让哈希表更加高效,人们又想出了一种解决哈希冲突的办法,就是哈希桶,这种方法简单的来说就是:顺序表和链表的结合,顺序表就像是一个指针数组,在这个数组中每一个指针底下都挂着一个桶,而这个桶就是由一个一个的节点构成。我用图示进行说明下:

*哈希桶的实现:

说明:当冲突比较大的时候底下用红黑树实现效率比较高

a:节点的定义:

//节点的定义template<class K,class V>struct HushNode{	K _key;	V _value;	//pair<K,V> _kv;	HushNode<K,V>* _next;	HushNode(const K& key,const V& value)		:_key(key)		,_value(value)		,_next(NULL)	{}};说明:这里我们可以将节点定义成pair类型的,这样当我们在实现迭代器的时候就会很方便,这个在后面我实现迭代器的时候再进行说明下。

b:插入函数的实现:

//插入函数的实现	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);	}

c:删除函数的实现:

说明:在删除的时候,我们要定义两个指针,一个是要删除的节点,一个是要删除的指针的头一个节点,进行分析情况:

*如果要删除的节点的前一个节点为空的话:

那么我们就让要被删除的这个节点的下一个节点直接指向这个位置

*如果要删除的节点的前一个节点不为空的话:

那么我们就让要被删除的节点的头一个节点的下一个指针直接指向被删除的节点的下一个指针

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->_next;				delete cur;			}			else			{				pre->_next = cur->_next;			}			cur = cur->_next;		}	}d:查找函数的实现:

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;	}

e:析构函数的实现:

~HushTable()	{		for(size_t i = 0; i<_size; i++)		{			Node* del = NULL;			Node* cur = _ht[i];			while(cur)			{				del = cur;				cur = cur->_next;				delete del;			}		}	}

【小扩展】:

当我们在考虑这块的删除的时候,我们可以可能会想到用find函数,如果用find的话,此时我们就要用交换的思想(因为此处就类似于单链表,只有向后的节点没有向前的节点),但是还会有问题,如果删除的是一个没有下一个节点的节点,比如这里的106,那么我们此时就将0,这个位置和106进行交换,这样就可以解决,如果删除的是只有一个节点的,删除完后记得将此节点置为空。

C:使用素数表:

说明:使用素数作为除数可以减少哈希冲突

素数表的实现:

size_t GetNewSize()	{		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 i = 0; i < _PrimeSize; ++i)		{			if (_ht.size() < _PrimeList[i])			{				return _PrimeList[i];			}		}		return 0;	}

【哈希表的实现】:

1、节点的定义:

//表中存储元素的状态enum Status{	EMPTY,	DELETE,	EXIST,};template<class K, class V>struct HashNode{	K _key;	V _value;	Status _status;	HashNode(const K& key = K(),const V& value = V())		:_key(key)		,_value(value)		,_status(EMPTY)	{}};

2、哈希表的插入:

bool Insert(const K& key,const V& value)	{		//检测是否需要扩容		CheckCapacity();		size_t index = _HashFunc(key);		while(_ht[index]._status == EXIST)		{			if(_ht[index]._key == key)				return false;			//如果是线性探测的话,			++index;			if(index == _ht.size()) //这块注意				index = 0;								}			++_size;		_ht[index]._key = key;		_ht[index]._value = value;		_ht[index]._status = EXIST;		return true;	}

3、哈希表的查找:

//寻找元素的函数	Node* Find(const K& key)	{		size_t i = _HashFunc(key);		while(_ht[i]._status != EMPTY)		{			if(_ht[i]._key == key && _ht[i]._status == EXIST)				return &_ht[i];			++i;			if(i == _ht.size())				i = 0;		}				return NULL;	}	

4、哈希表的扩容:

//扩容函数	void CheckCapacity()	{	//如果哈希表是空的或者它的容量已经超过了负载因子,就要进行扩容		if(_ht.size() == 0 || (_size*10)/_ht.size() >= 7)		{			//size_t newsize = _size*2+3;			size_t newsize = GetNewSize();			HashTable temp(0);			temp._ht.resize(newsize);			for(int i = 0; i<_size; i++)			{				if(_ht[i]._status == EXIST)				temp.Insert(_ht[i]._key,_ht[i]._value);			}			this->Swap(temp);		}	}
//交换函数	void Swap(HashTable& spht)	{		_ht.swap(spht._ht);	

4、哈希表的迭代器如何实现:

引言:起初在进行实现的时候,会有两个办法:(但是这就需要我们将哈希表的结构进行改变)

*通过线索化

*通过双向循环链表

说明:这里其实我们在实现的时候问题主要是在重载++上,我们要获取到哈希表的指针,用于搜寻下一个不为空的节点,因此在设计这个迭代器的时候,我们可以给一个哈希表的指针。

A:节点的定义

template<class K,class V,class Ref,class Ptr>struct HushInterator{	typedef HushNode<K,V> Node;	typedef HushIterartor<K,V,Ref,Ptr> Self;	Node* _node;	HushTable<K,V>* _ht;

B:其他函数的实现:

//其他函数的实现	Ref Operator*(Node* node)	{		return node->_key;	}	Ptr operator->(Node* node)	{		return &(node->_key);	}	bool operator==(const Self& s)	{		return _node == s.node;	}	bool operator!=(const Self& s)	{		return _node!=s.node;	}	Self& operator++()	{		_node = Next(node);		return *this;	}

C:关于重载++的重点说明:

我们单独实现一个函数:Next

*当下一个节点不为空的时候我们就直接返回

*否则,我们就进行搜寻下一个不为空的节点

Node* Next(Node* node)	{		Node* next = node->_next;		//如果下一个节点不为空的话直接返回这个这个节点		if(next)			return next;		//如果下一个节点为空的话,去找不为空的将其返回		else		{			size_t index = _ht->_HushFunc(node->_key)+1;			for(int i = 0; i<_ht->_ht.size(); i++)			{				next = _ht->_ht[index];				if(next)					return next;			}		}		return NULL;	}

【有关哈希表实现的一些说明】:

1)说明:

我们在实现哈希表的时候不一定就是整型,也可能是字符串或者是结构体等等,那么我们这里就要进行这个方面问题的解决。这里我们就利用仿函数进行实现,然后利用函数模板的特化

2)有关代码:

//仿函数的实现template<class K>struct _HashFunc{	size_t operator()(const K& key)	{		return key;	}};template<>struct _HashFunc<string>{	//此处用静态的,在main函数执行前只生成一次	static size_t BKDRHash(const char *str)  	{  		register size_t hash = 0;  		while (size_t ch = (size_t)*str++)  		{     			//131是研究出来的数字,使用这个公式,就会避免重复			hash = hash * 131 + ch;         		}  		return hash;  	}	size_t operator ()(const string &key)	{		return BKDRHash(key.c_str());	}};

//哈希函数(散列函数)	size_t _HashFunc(const K& key)	{		HashFunc hf;		return hf(key)%(_ht.size());	}

3)此处涉及到字符串哈希算法

这个链接里有详细的解释:http://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表