C++复习笔记20

哈希结构:

        顺序结构以及平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素时,必须要经 过关键码的多次比较 顺序查找时间复杂度为 O(N) ,平衡树中为树的高度,即 O( log2N
) ,搜索的效率取决于搜索过程中元素的比较次数。
        理想的搜索方法:可以 不经过任何比较,一次直接从表中得到要搜索的元素 如果构造一种存储结构,通过某种函数 (hashFunc) 使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函 数可以很快找到该元素
当向该结构中:
        插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
        搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比 较,若关键码相等,则搜索成功。
       该方式即为哈希 ( 散列 ) 方法, 哈希方法中使用的转换函数称为哈希 ( 散列 ) 函数,构造出来的结构称为哈希表 (Hash Table)( 或者称散列表 ) .
例如:数据集合 {1 7 6 4 5 9}
哈希函数设置为: hash(key) = key % capacity ; capacity 为存储元素底层空间总的大小。
        用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快 问题:按照上述哈希方式,向集合中 插入元素 44 ,会出现什么问题?
哈希冲突:
对于两个数据元素的关键字 和 (i != j) ,有 != ,但有: Hash( ) == Hash( ) ,即: 不同关键字通过
相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞 把具有不同关键码而具有相同哈希地址的数据元素称为 同义词 发生哈希冲突该如何处理呢?
引起哈希冲突的一个原因可能是: 哈希函数设计不够合理 哈希函数设计原则
哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须在 0
m-1 之间
哈希函数计算出来的地址能均匀分布在整个空间中
哈希函数应该比较简单
常见哈希函数
1. 直接定址法--(常用)
       取关键字的某个线性函数为散列地址: Hash Key = A*Key + B 优点:简单、均匀 缺点:需要事先 知道关键字的分布情况使用场景:适合查找比较小且连续的情况 
2. 除留余数法--(常用)
       设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函 数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址
3. 平方取中法
         假设关键字为 1234 ,对它平方就是 1522756 ,抽取中间的 3 227 作为哈希地址; 再比如关键字为 4321 ,对它平方就是 18671041 ,抽取中间的 3 671( 710) 作为哈希地址 平方取中法比较适合:不知 道关键字的分布,而位数又不是很大的情况 。
4. 折叠法
         折叠法是将关键字从左到右分割成位数相等的几部分 ( 最后一部分位数可以短些 ) ,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5. 随机数法
          选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key), 其中 random 随机数函数。 通常应用于关键字长度不等时采用此法
6. 数学分析法
          设有 n d 位数,每一位可能有 r 种不同的符号,这 r 种不同的符号在各位上出现的频率不一定相同,可能 在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出 现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。
假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前 7 位都是 相同的,那么我 们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出 冲突,还可以对抽取出来的数字 进行反转 ( 1234 改成 4321) 、右环位移 ( 1234 改成 4123) 、左环移位、前两数与后两数叠加 ( 1234 12+34=46) 等方法。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布 较均匀的情况
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突 。
哈希表简单实现(链地址法)
#include<iostream>
#include<vector>
using namespace std;

template<class Type>
class HashTable;

template<class Type>
class HashNode
{
	friend class HashTable<Type>;
public:
	HashNode(Type d) :data(d), link(nullptr) {}

private:
	Type data;
	HashNode<Type>* link;
};

template<class Type>
class HashTable
{
public:
	HashTable() 
	{
		memset(hashtable, 0, sizeof(HashNode<Type>*)*HASH_DEFAULT_SIZE);
	}

	void Insert(const Type& x)
	{
		int index = Hash(x);
		HashNode<Type>* s = new HashNode<Type>(x);
		s->link = hashtable[index];
		hashtable[index]= s;
	}

	void Remove(const Type& key)
	{
		int index = Hash(key);
		HashNode<Type>* prev = nullptr;
		HashNode<Type>* p = hashtable[index];
		while (p != nullptr && p->data != key)
		{
			prev = p;
			p = p->link;
		}
		if (p == nullptr)
			return;
		
			if (prev == nullptr)
				hashtable[index] = p->link;
			else
			{
				prev->link = p->link;
			}
	}

	void Show() const
	{
		for (int i = 0; i < HASH_DEFAULT_SIZE; ++i)
		{
			cout << i << " : ";
			HashNode<Type>* p = hashtable[i];
			while (p != nullptr)
			{
				cout << p->data << "-->";
				p = p->link;
			}
			cout <<"NULL"<< endl;
		}
	}

protected:
	int Hash(const Type& key) { return key % HASH_DEFAULT_SIZE; }

private:
	enum {HASH_DEFAULT_SIZE=7};
	HashNode<Type>* hashtable[HASH_DEFAULT_SIZE];
};

void test01()
{
	vector<int>v{37,2,5,9,1,13,8,3,6,12};
	HashTable<int>ht;
	cout << "OK" << endl;

	for (const auto& e : v)
		ht.Insert(e);

	ht.Show();

	cout << "---------------------" << endl;
	ht.Remove(9);
	ht.Show();
}

void main()
{
	test01();
	system("pause");
}

桶容量为3的哈希表:

#include<iostream>
using namespace std;

/*
现在有一个用来存放整数的Hash表,
Hash表的存储单位称为桶,
每个桶能放3个整数,当一个桶中要放的元素超过3个时,
则要将新的元素存放在溢出桶中,每个溢出桶也能放3个元素,
多个溢出桶使用链表串起来。
此Hash表的基桶数目为素数P,Hash表的hash函数对P取模。代码定义如下:
现在假设hash_table已经初始化好了,
insert_new_element()函数目的是把一个新的值插入hash_table中,
元素插入成功时,函数返回0,否则返回-1,完成函数。
#define P 7
#define BUCKET_SIZE 3
#define NULL_DATA -1*/
//现在有一个用来存放整数的Hash表,
//Hash表的存储单位称为桶,
//每个桶能放3个整数,当一个桶中要放的元素超过3个时,
//则要将新的元素存放在溢出桶中,每个溢出桶也能放3个元素,
//多个溢出桶使用链表串起来。
//此Hash表的基桶数目为素数P,Hash表的hash函数对P取模。代码定义如下:

//现在假设hash_table已经初始化好了,
//insert_new_element()函数目的是把一个新的值插入hash_table中,
//元素插入成功时,函数返回0,否则返回-1,完成函数。
#define P 7
#define NULL_DATA -1
#define BUCKET_SIZE 3
struct bucket_node	
{
	int data[BUCKET_SIZE];
	struct bucket_node *next;
};
bucket_node hash_table[P];

int Hash(const int& key)
{
	return key % P;
}

void Init_hash_table()
{
	for (int i = 0; i < P; ++i)
	{
		for (int j = 0; j < BUCKET_SIZE; ++j)
		{
			hash_table[i].data[j] = NULL_DATA;
		}
	}
}
int insert_new_element(int new_element)
{
	//完成此函数
	int index = Hash(new_element);

	for (int i = 0; i < BUCKET_SIZE; ++i)
	{
		if (hash_table[index].data[i] == NULL_DATA)
		{
			hash_table[index].data[i] = new_element;
			return true;
		}
	}
	    bucket_node* p = &hash_table[index];
		while (p->next != NULL)
		{
			p = p->next;
			for (int i = 0; i < BUCKET_SIZE; ++i)
			{
				if (p->data[i] == NULL_DATA)
				{
					p->data[i] = new_element;
					return true;
				}
			}
		}

	bucket_node* new_bucket = (bucket_node*)malloc(sizeof(bucket_node));
	for (int i = 0; i < BUCKET_SIZE; ++i)
	{
		new_bucket->data[i] = NULL_DATA;
	}
	new_bucket->next = NULL;
	new_bucket->data[0] = new_element;
	p->next = new_bucket;

	return 0;
}

int main()
{
	Init_hash_table();
	int array[] = { 1,8,15,22,29,36,43 };
	//int array[] = { 15, 14, 21, 87, 96, 293, 35, 24, 149, 19, 63, 16, 103, 77, 5, 153, 145, 356, 51, 68, 705, 453 };
	for (int i = 0; i < sizeof(array) / sizeof(int); i++)
	{
		insert_new_element(array[i]);
	}
	return 0;
}

哈希冲突的解决:

1.闭散列:

       闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置中的 下一个 空位置中去。 那如何寻找下一个空位置呢?
1. 线性探测
       比如 2.1 中的场景,现在需要插入元素 44 ,先通过哈希函数计算哈希地址, hashAddr 4 ,因此 44 理论 上应该插在该位置,但是该位置已经放了值为 4 的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
       插入
通过哈希函数获取待插入元素在哈希表中的位置 。
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探 测找到下一个空位置,插入新元素。
        删除
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他
元素的搜索 。比如删除元素 4 ,如果直接删除掉, 44 查找起来可能会受影响。因此 线性探测采用标
记的伪删除法来删除一个元素
线性探测法:
	#include<iostream>
	#include<vector>
	using namespace std;
	 //注意:假如实现的哈希表中元素唯一,即key相同的元素不再进行插入
	 //为了实现简单,此哈希表中我们将比较直接与元素绑定在一起

	//用于空间状态标记
	enum State { EMPTY, EXIST, DELETE };
	template<class K, class V>
	class HashTable
	{
		struct Elem
		{
			pair<K, V> _val;
			State _state;
		};

	public:
		HashTable(size_t capacity = 3)
			: _ht(capacity), _size(0)
		{
			for (size_t i = 0; i < capacity; ++i)
				_ht[i]._state = EMPTY;
		}

		bool Insert(const pair<K, V>& val)
		{
			// 检测哈希表底层空间是否充足
			 //_CheckCapacity();
			size_t hashAddr = HashFunc(val.first);
			 size_t startAddr = hashAddr;
			while (_ht[hashAddr]._state != EMPTY)
			{
				if(_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first == val.first)
				return false;
 
				hashAddr++;
				if(hashAddr == _ht.capacity())
				 hashAddr = 0;
				/*
	  转一圈也没有找到,注意:动态哈希表,该种情况可以不用考虑,哈希表中元素个数
	到达一定的数量,哈希冲突概率会增大,需要扩容来降低哈希冲突,因此哈希表中元素是不会存满的
	 if(hashAddr == startAddr)
	 return false;
	 */
			 }
 
			   //插入元素
			  _ht[hashAddr]._state = EXIST;
			  _ht[hashAddr]._val = val;
			  _size++;
			  return true;
			 }
	 int Find(const K& key)
	 {
	 size_t hashAddr = HashFunc(key);
	 while(_ht[hashAddr]._state != EMPTY)
	 {
	 if(_ht[hashAddr]._state == EXIST && _ht[hashAddr]._val.first == key)
	 return hashAddr;
 
	 hashAddr++;
	 }
	 return hashAddr;
	 }
	 bool Erase(const K& key)
	 {
	 int index = Find(key);
	 if(-1 != index)
	 {
	 _ht[index]._state = DELETE;
	 _size++;
	 return true;
	 }
	 return false;
	 }
	 size_t Size()const;
	 bool Empty() const; 
	 //void Swap(HashTable<K, V, HF>& ht);
	private:
	 size_t HashFunc(const K& key)
	 {
	 return key % _ht.capacity();
	 }
	private:
	 vector<Elem> _ht;
	 size_t _size;
	};

	void test01()
	{
		pair<int, string>v[]{ {1,"abc"},{2,"xyz"},{3,"opq"},{4,"fff"},{5,"qwe"} };
		HashTable<int,string>ht;
		ht.Insert(v[0]);
		ht.Insert(v[1]);
		ht.Insert(v[2]);
		ht.Insert(v[4]);
		cout << "OK" << endl;
	}

	void main()
	{
		test01();
		system("pause");
	}

闭散列扩容:

void CheckCapacity()
{
 if(_size * 10 / _ht.capacity() >= 7)
 {
 HashTable<K, V, HF> newHt(GetNextPrime(ht.capacity));
 for(size_t i = 0; i < _ht.capacity(); ++i)
 {
 if(_ht[i]._state == EXIST)
 newHt.Insert(_ht[i]._val);
 }
 
 Swap(newHt);
 }
}
线性探测优点:实现非常简单、
线性探测缺点: 一旦发生哈希冲突,所有的冲突连在一起,容易产生数据 堆积 ,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低 。如何缓解呢?
2. 二次探测

 

        研究表明: 当表的长度为质数且表装载因子 a 不超过 0.5 时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装 满的情况,但在插入时必须确保表的装载因子 a 不超过 0.5 如果超出必须考虑增容
因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
1. 开散列概念
       开散列法又叫链地址法 ( 开链法 ) ,首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结 点存储在哈希表中。
       从下图可以看出, 开散列中每个桶中放的都是发生哈希冲突的元素。sgissdda
SGI版本底层哈希表(开散列)模拟实现:
#include<iostream>
#include<vector>
using namespace std;

template<class Value>
class _hashtable_node
{
public:
	Value val;
	_hashtable_node* next;
};

struct MyHash
{
	int operator()(int key)
	{
		return key;
	}
};

struct MyExtractKey
{
	int operator()(int key)
	{
		return key;
	}
};

static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
  53,         97,           193,         389,       769,
  1543,       3079,         6151,        12289,     24593,
  49157,      98317,        196613,      393241,    786433,
  1572869,    3145739,      6291469,     12582917,  25165843,
  50331653,   100663319,    201326611,   402653189, 805306457,
  1610612741, 3221225473ul, 4294967291ul
};

inline unsigned long __stl_next_prime(unsigned long n)
{
	const unsigned long* first = __stl_prime_list;
	const unsigned long* last = __stl_prime_list + __stl_num_primes;

	while (first != last)
	{
		if (*first >= n)
			break;
		++first;
	}
	return first == last ? *(last - 1) : *first;
}

template<class Key,class Value,class HashFcn=MyHash,class ExtrectKey=MyExtractKey>
class hashtable
{
public:
	typedef Key  key_type;
	typedef Value value_type;
	typedef HashFcn hasher;
	typedef size_t size_type;
public:
	typedef _hashtable_node<Value> node;

public: hashtable(size_t n) :hash(HashFcn()),get_key(ExtrectKey()), num_elements(0) 
    {
	  initialize_buckets(n);
    }

	  size_type bkt_num_key(const key_type& key)
	  {
		  return bkt_num_key(key, buckets.size());
	  }

	  size_type bkt_num(const value_type& obj)
	  {
		  return bkt_num_key(get_key(obj));
	  }

	  size_type bkt_num_key(const key_type& key, size_t n)
	  {
		  return hash(key) % n; //
	  }

	  size_type bkt_num(const value_type& obj, size_t n)
	  {
		  return bkt_num_key(get_key(obj), n);
	  }
	  node* new_node(const value_type& obj)
	  {
		  node* n = (node*)malloc(sizeof(node));
		  n->next = 0;
		  n->val = obj;
		  return n;
	  }

	  bool insert_unique(const value_type& obj)
	  {
		  resize(num_elements + 1);
		  return insert_unique_noresize(obj);
	  }

	  bool insert_unique_noresize(const value_type& obj)
	  {
		  const size_type n = bkt_num(obj);
		  node* first = buckets[n];

		  for (node* cur = first; cur; cur = cur->next)
			  if (obj == cur->val)
				  return false;

		  node* tmp = new_node(obj);
		  tmp->next = first;
		  buckets[n] = tmp;
		  ++num_elements;
		  return true;
	  }

	  void resize(size_type num_elements_hint)
	  {
		  const size_type old_n = buckets.size();
		  if (num_elements_hint > old_n)
		  {
			  const size_type n = next_size(num_elements_hint);
			  if (n > old_n)
			  {
				  vector<node*> tmp(n, (node*)0);

				  for (size_type bucket = 0; bucket < old_n; ++bucket)
				  {
					  node* first = buckets[bucket];
					  while (first)
					  {
						  size_type new_bucket = bkt_num(first->val, n);
						  buckets[bucket] = first->next;
						  first->next = tmp[new_bucket];
						  tmp[new_bucket] = first;
						  first = buckets[bucket];
					  }
				  }
				  buckets.swap(tmp);
			  }
		  }
	  }

	  void print_hash_table()
	  {
		  for (int i = 0; i < buckets.size(); ++i)
		  {
			  cout << i;
			  cout << " : ";
			  node* first = buckets[i];
			  while (first != NULL)
			  {
				  cout << first->val << "-->";
				  first = first->next;
			  }
			  cout << "Nil." << endl;
		  }
	  }

private:

	size_type next_size(size_type n) const
	{
		return __stl_next_prime(n);
	}

	void initialize_buckets(size_type n)
	{
		const size_type n_buckets = next_size(n);
		buckets.reserve(n_buckets);
		buckets.insert(buckets.end(), n_buckets, (node*)0);
		num_elements = 0;
	}
private:
	hasher hash;
	ExtrectKey get_key;
	vector<node*> buckets; //桶节点
	size_t num_elements;   //
};

void test01()
{
	hashtable<int, int, MyHash, MyExtractKey> ht(53);
	ht.insert_unique(1);
	ht.insert_unique(54);
	cout << "OK" << endl;
}

void test02()
{
	hashtable<int, int, MyHash, MyExtractKey> ht(53);
	for (int i = 0; i < 53; ++i)
		ht.insert_unique(i+2);

	ht.print_hash_table();
	cout << "=================" << endl;
	ht.insert_unique(100);
	ht.print_hash_table();
}

void main()
{
	//hashtable<>
	test02();
	system("pause");
} 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值