【C++】模拟实现哈希(闭散列和开散列两种方式)

哈希

  • 前言
  • 正式开始
    • map、set 与 unordered_map、unordered_set 的不同
      • 遍历结果不同
      • 查找速度不同
    • 哈希
      • 闭散列
        • 概念介绍
        • 模拟实现
        • 字符串等自定义类型找位置
        • 字符串哈希算法
        • 二次探测
      • 开散列
        • 概念介绍
        • 模拟实现
        • 存储自定义类型
        • 哈希表大小设置为素数

【C++】模拟实现哈希(闭散列和开散列两种方式)_第1张图片

前言

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2N log2N,例如map和set。即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不是很理想。

最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器(unordered_map、unordered_set、unordered_multimap和unordered_multiset),这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本篇中只对unordered_map和unordered_set进行介绍,并对其底层进行模拟实现,下一篇用本篇的模拟实现来封装unordered_map和unordered_set。

正式开始

STL中给的unordered_map和unordered_set用法可以说和map和set一样。我就不过多介绍了,如果对于map和set的使用不了解的同学,可以看看这篇:【C++】STL map和set用法基本介绍。

map、set 与 unordered_map、unordered_set 的不同

我就直接用set来进行对比了。

遍历结果不同

【C++】模拟实现哈希(闭散列和开散列两种方式)_第2张图片

查找速度不同

下面代码为比较二者增删查的速度。

void test_op()
{
	int n = 10000;
	vector<int> v;
	v.reserve(n); // 先开到n,等会将产生的数据放到v中

	srand(time(0));
	for (int i = 0; i < n; ++i)
	{
		v.push_back(rand() + i);  // 重复少
		//v.push_back(rand());  // 重复多
	}

	// set的插入时间
	size_t begin1 = clock();
	set<int> s;
	for (auto e : v)
	{
		s.insert(e);
	}
	size_t end1 = clock();

	// unordered_set插入的时间
	size_t begin2 = clock();
	unordered_set<int> us;
	for (auto e : v)
	{
		us.insert(e);
	}
	size_t end2 = clock();

	cout << "总共插入的数据个数:" << s.size() << endl << endl;

	cout << "set insert:" << end1 - begin1 << endl;
	cout << "unordered_set insert:" << end2 - begin2 << endl << endl;

	// set的查找时间
	size_t begin3 = clock();
	for (auto e : v)
	{
		s.find(e);
	}
	size_t end3 = clock();

	// unordered_set查找的时间
	size_t begin4 = clock();
	for (auto e : v)
	{
		us.find(e);
	}
	size_t end4 = clock();
	cout << "set find:" << end3 - begin3 << endl;
	cout << "unordered_set find:" << end4 - begin4 << endl << endl;

	// set的删除时间
	size_t begin5 = clock();
	for (auto e : v)
	{
		s.erase(e);
	}
	size_t end5 = clock();

	// unordered_set删除的时间
	size_t begin6 = clock();
	for (auto e : v)
	{
		us.erase(e);
	}
	size_t end6 = clock();

	cout << "set erase:" << end5 - begin5 << endl;
	cout << "unordered_set erase:" << end6 - begin6 << endl << endl;
}

不断改变n的大小来改变插入元素的个数。下面的测试都是在debug版本下的:

n == 10000
【C++】模拟实现哈希(闭散列和开散列两种方式)_第3张图片

因为随机数可能会产生重复的数,所以去重后的数据就会少一点。

n == 100000
【C++】模拟实现哈希(闭散列和开散列两种方式)_第4张图片

n == 1000000
【C++】模拟实现哈希(闭散列和开散列两种方式)_第5张图片

n == 10000000
【C++】模拟实现哈希(闭散列和开散列两种方式)_第6张图片

可以看到,哈希还是很快的。

开始讲底层。

哈希

不知各位接触过计数排序没有。计数排序中就用到了哈希的思想。通过下标直接查找到某个数据。

这里给出1、3、0、12、5、14、2、4、7、6、9、8、11、10、15、13这个数,通过顺序表存储,我想要在O(1)的时间复杂度下查找任意一个数,各位想想有什么好办法?

可以直接通过下标产生对应的映射,什么意思呢?
就是下标0位置存放的数可以直接存储0,下标1位置存放的数可以直接存储1,下标2存储2,下标3存储3,下标4存储4,下标5存储5……一直到下标15。

那么假如顺序表名称为v,我们用v[5]就可以直接找到5,用v[13]就可以直接找到13……等等。找的时候根本不需要像红黑树那样不断对比才能找到,这样就是O(1)的时间复杂度。

那么哈希(也可叫散列)就是这样的思想,让每一个数据与其位置建立映射关系,通过位置直接查找出来所需的数据,这样查找起来就会非常的快。

但是有一个问题,就是当上面数据分布不是那么均匀(大的大,小的小)时该怎么办呢?

比如:3、7、19、25、36、300、7000
我们如果光对这7个数开7000个int,怕是有点不值得。得改一改思路。

我们可以将数进行取模运算,得到的余数即放到对应的顺序表中,比如这里把顺序表开到10个int。上面各个数对10进行取模就分别得到了3、7、9、5、6、0、0。

这样取模的方式就叫做除留余数法,是一种哈希函数(哈希函数传一个值能够产生对应的下标,哈希函数不止这里的除留余数法,还有其他的,不过除留余数法更常用,对于其他方法感兴趣的同学可以自己查查)。

那么存储下来就是这样:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第7张图片
虽然说解决了,但是还出现了一个问题,就是300和7000占用了同一个位置。

这个问题就叫做哈希冲突,也叫哈希碰撞。
有两个方法可以解决这个问题。分别为 闭散列 和 开散列。

闭散列 ===》 开放地址法。
开散列 ===》 拉链法/哈希桶。

那么我来挨个介绍,并进行模拟实现。

闭散列

概念介绍

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。

就拿上面的例子来说,本来是这样的:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第8张图片

用闭散列的方式就是这样的:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第9张图片
将7000放到算出来的0下标的下一个位置1下标处。

如果再来一个20的话,余数还是0,此时还是继续往后寻找空位置,也就是2:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第10张图片

那么再来一个40呢?
余数还是0,被占用了,1、2、3也被占了,那就放到4处:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第11张图片

这里光一个0下标位置就几乎占了一半的下标了。

当空间快满的时候就需要进行扩容,这里可以搞一个负载因子来决定扩容时机。
负载因子 = 实际存储数据个数 / 当前顺序表的大小。这样的话负载因子取值范围就是[0, 1]。
我们可以控制当负载因子为0.7时就进行扩容。

对于负载因子:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第12张图片

【C++】模拟实现哈希(闭散列和开散列两种方式)_第13张图片

接着上面的,如果我们此时查找20的话,先算出对应下标0,下标0处不是20,继续往后找,为7000,继续找,为20,此时即找到。

但如果找一个不存在的数呢?比如说66,余数为6,下标6不是,继续下标7,不是下标8找到空,此时就可以说明不存在了,因为我们存放的时候如果位置被占了,就会沿着继续往后找,直到找到一个空位置放进去就行。此时我们找一个数,位置被占了,沿着找,到了空就是没找到。

但是如果20被删除了呢?
找40,中间20是空的,就会出现找不到的情况。
我们可以将每一个位置设置一个标志位,EMPTY表示这个位置是空的,曾经没有数据。EXIST表示这个位置中有数据。DELETE表示这个位置没数据,但曾经是有数据的,只是被删除了,我们在查找的时候就不能跳过这个位置。其中EMPTY和DELETE能够插入数据。当想要删除数据的时候,只需要将标志位置为DELETE就行了。

如果说数据一直插入删除导致所有的位置的标志位都变为了DELETE。这样的话查找时就要先找到初始位置,然后从一个位置往后找到顺序表的末尾,再循环到顺序表的最开始找,直到又重新找到原始位置。但一般是不会出现这种情况的,因为

可以看到,这个方法并不是那么好,但是我们还是要学一学的。

下面我们就模拟实现一下。

模拟实现

基本框架:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第14张图片

然后写一个插入:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第15张图片

Find函数:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第16张图片

测试一下:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第17张图片

再填一个扩一下容:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第18张图片

然后再来写一下删除:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第19张图片

再写一个遍历打印的:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第20张图片

测试:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第21张图片

字符串等自定义类型找位置

还有问题。
库中是可以存放字符串的。而我们这里实现的可不行。
【C++】模拟实现哈希(闭散列和开散列两种方式)_第22张图片

因为字符串可没有取模的功能。

想要改改的话,就得搞仿函数。当传一个string的时候,能够返回一个数就行。
那么上面的代码就要改改。在CloseHash的模版参数中添加一个参数hash用来传仿函数,然后在用到%的地方将pair的first套上仿函数。

【C++】模拟实现哈希(闭散列和开散列两种方式)_第23张图片
【C++】模拟实现哈希(闭散列和开散列两种方式)_第24张图片

【C++】模拟实现哈希(闭散列和开散列两种方式)_第25张图片

对于像int、float、double、long等等这样的类型,直接返回其本身强转的值就够了:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第26张图片

而对于我们的string需要再写一个仿函数:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第27张图片
但是模版有特化,我们还可以这样写:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第28张图片

如果是第一种方式的话,就要这样用:
在这里插入图片描述

如果是第二种方式的话,就不需要传第三个了。
测试:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第29张图片

这样的话,自定义类型再多传一个模版参数就行了。

字符串哈希算法

还有问题。
字符串冲突的概率还是比数字大不少的,比如说 “abcd” “bcad” “aadd” 还有 “eat” "ate"等等。
【C++】模拟实现哈希(闭散列和开散列两种方式)_第30张图片

那么怎么解决呢?
有一篇博客详细介绍了:字符串哈希算法

里面有一种方法:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第31张图片
库中就用的是这个,我们也用:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第32张图片
这样产生的数重复就会少一点,而且相同字符串每次产生的树都是相同的。

二次探测

上面就是对于闭散列进行的简单实现。实现的时候当数据重复了,就从余数下标位置开始找,一个一个的找空位置,这种一个一个找的方式就叫做线性探测。还有种查找空位置的方式叫做二次探测,可不要听名字就觉得是两个两个位置的找,是平方平方的找。

先说一下线性查找的优缺点:
线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降
低。
线性查找是 h a s h i = h a s h i + x ( x 取 1 , 2 , 3 … … ) hashi = hashi + x(x取1,2,3……) hashi=hashi+xx1,2,3……

再来说二次探测。查找的方式稍微变了点: h a s h i = h a s h i + x 2 hashi = hashi + x^2 hashi=hashi+x2,x取值同上。
那么这样找的话每次加的就是 1、4、9、16……

比如说下面的数据:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第33张图片

其中插入一个44,hash(44) = 4,4下标处有数据,4加上1的平方为5,5再加2的平方为9,9加上3的平方为18,再模上10得8,此时8处为空,那么就把44放到8下标处。

【C++】模拟实现哈希(闭散列和开散列两种方式)_第34张图片

那么我们也可模拟实现一下二次探测,主要是改一下插入就行:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第35张图片

测试:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第36张图片

闭散列就讲到这里,没什么太难的地方,下面说更为重要的开散列。

开散列

概念介绍

开散列法又叫链地址法(开链法),首先对关键码集合用哈希函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中。

上面的专业术语看起来比较模糊,看图:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第37张图片
仍然是上面的几个数,不过插入发生哈希冲突的时候只需要搞成链表,把冲突的数据挂到链表上就行了。对于闭散列来说冲突时不会占用其他数据的位置了,而且这样的话查找起来会更加方便,更快。

大概的意思就讲完了,下面来模拟实现。

模拟实现

首先,开散列中存放的是链表,那么用该用八种链表中的哪种呢?

单链表就行了,因为现实中就算有哈希冲突也不会很多的,等会我们模拟实现完后存放一下随机数各位就知道了,每个哈希桶的长度大多都是1,极个别的才是2、3,4往上的可以说几乎就没有。

那么存放单链表的话,就要写出链表的节点:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第38张图片

然后再写哈希表的框架:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第39张图片

还是插入:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第40张图片

查找
【C++】模拟实现哈希(闭散列和开散列两种方式)_第41张图片

测试:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第42张图片

再来写删除:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第43张图片

测试:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第44张图片

存储自定义类型

和上面闭散列同样的问题,当前实现的开散列的方式没法存储string等自定义类型,所以还
要搞仿函数。

仿函数和上面闭散列中用到的一模一样,这里就不给了。

加上模版参数:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第45张图片

用到取模的地方套上仿函数:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第46张图片
【C++】模拟实现哈希(闭散列和开散列两种方式)_第47张图片

在这里插入图片描述

【C++】模拟实现哈希(闭散列和开散列两种方式)_第48张图片

测试:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第49张图片

哈希表大小设置为素数

哈希表大小设置为素数能够减少哈希冲突。这句话是大佬说的,人家有科学依据,而且STL库中也是这么干的。

虽然我不知道为啥,但是咱们照做就行。

搞素数的函数如下:

inline size_t __stl_next_prime(size_t n)
{
	static const size_t __stl_num_primes = 28;
	static const size_t __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, 3221225473, 4294967291
	};

	for (size_t i = 0; i < __stl_num_primes; ++i)
	{
		if (__stl_prime_list[i] > n)
		{
			return __stl_prime_list[i];
		}
	}

	return -1;
}

扩容的时候用一下就行了。
【C++】模拟实现哈希(闭散列和开散列两种方式)_第50张图片

到这里开散列和闭散列就讲的差不多了,我们来看看插入随机数每个链有多长。

如下几个接口:

// 表的长度
size_t TablesSize()
{
	return _tables.size();
}

// 桶的个数
size_t BucketNum()
{
	size_t num = 0;
	for (size_t i = 0; i < _tables.size(); ++i)
	{
		if (_tables[i])
		{
			++num;
		}
	}

	return num;
}

// 最长桶的长度
size_t MaxBucketLenth()
{
	size_t maxLen = 0;
	for (size_t i = 0; i < _tables.size(); ++i)
	{
		size_t len = 0;
		Node* cur = _tables[i];
		while (cur)
		{
			++len;
			cur = cur->_next;
		}

		//if (len > 0)
			//printf("[%d]号桶长度:%d\n", i, len);

		if (len > maxLen)
		{
			maxLen = len;
		}
	}

	return maxLen;
}

size_t Size()
{
	return _size;
}

测试代码:

void TestHT3()
{

	int n = 10000;
	vector<int> v;
	v.reserve(n);
	srand(time(0));
	for (int i = 0; i < n; ++i)
	{
		//v.push_back(i);
		v.push_back(rand() + i);  // 重复少
		//v.push_back(rand());  // 重复多
	}

	size_t begin1 = clock();
	HashTable<int, int> ht;
	for (auto e : v)
	{
		ht.Insert(make_pair(e, e));
	}
	size_t end1 = clock();

	cout << "数据个数:" << ht.Size() << endl;
	cout << "表的长度:" << ht.TablesSize() << endl;
	cout << "桶的个数:" << ht.BucketNum() << endl;
	cout << "平均每个桶的长度:" << (double)ht.Size() / (double)ht.BucketNum() << endl;
	cout << "最长的桶的长度:" << ht.MaxBucketLenth() << endl;
	cout << "负载因子:" << (double)ht.Size() / (double)ht.TablesSize() << endl;
}

然后测试:

n == 10000
【C++】模拟实现哈希(闭散列和开散列两种方式)_第51张图片

n == 100000
【C++】模拟实现哈希(闭散列和开散列两种方式)_第52张图片

n == 1000000
【C++】模拟实现哈希(闭散列和开散列两种方式)_第53张图片

n == 10000000
【C++】模拟实现哈希(闭散列和开散列两种方式)_第54张图片

结果也是显而易见了。桶长度一般都是1,2的都少,3、4就更不用说了。
所以说哈希冲突不会那么多的。

上面的接口,STL库中也提供了。
【C++】模拟实现哈希(闭散列和开散列两种方式)_第55张图片
像bucket_count就是桶个数,max_bucket_count就是最多能有多少个桶,bucket就是第几个桶的大小,bucket是返回某个关键字所在哪一个桶中。

【C++】模拟实现哈希(闭散列和开散列两种方式)_第56张图片
load_factor就是负载因子。rehash和reserve就是扩容的东西。

最后写一下析构:
【C++】模拟实现哈希(闭散列和开散列两种方式)_第57张图片

这篇就到这里,下一篇讲解用本篇的代码封装unordered_map和unordered_set。

本篇的所有代码如下:

#pragma once

template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

template<>
struct HashFunc<string>
{
	size_t operator()(const string& str)
	{
		size_t res = 0;
		for (auto c : str)
		{
			res += c;
			res *= 131;
		}
		return res;
	}
};

/*struct HashFuncString
{
	size_t operator()(const string& str)
	{
		size_t res = 0;
		for (auto c : str)
			res += c;

		return res;
	}
};*/

namespace FangZhang_CloseHash
{
	

	// 位置的状态
	enum State
	{
		EMPTY, // 位置为空
		EXIST, // 存在数据
		DELETE // 数据被删除
	};

	// 顺序表中存放的数据 此处是k/v模型
	// 等到讲哈希的封装的时候会改
	template<class K, class V>
	struct HashData
	{
		HashData(const pair<K, V>& kv = make_pair(K(), V()))
			:_kv(kv)
			, _state(EMPTY)
		{}

		pair<K, V> _kv;
		State _state;
	};

	// 闭散列
	template<class K, class V, class Hash = HashFunc<K>>
	class CloseHash
	{
		typedef HashData<K, V> Data;
	public:
		bool Insert(const pair<K, V>& kv)
		{
			// 去重
			if (Find(kv.first))
				return false;

			// 扩容                         _size / _tables.size()就是负载因子
			if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7)
			{
				// 新搞一个哈希表
				CloseHash newCH;
				// 新空间大小
				size_t newSize = _tables.size() == 0 ? 10 : 2 * _tables.size();
				// 给新的哈希表开空间
				newCH._tables.resize(newSize);

				// 扩容后,将原表中的数据重新映射到新表中
				// 改变原来的映射关系,重新映射
				// 能够将原表中重复占位的数减少一些
				for (int i = 0; i < _tables.size(); ++i)
				{
					if (_tables[i]._state == EXIST)
						newCH.Insert(_tables[i]._kv);
				}

				// 将新表中的数据换到原表中
				_tables.swap(newCH._tables);
			}

			// 正式插入
			//Hash hash;
			//size_t hashi = hash(kv.first) % _tables.size();
			//while (_tables[hashi]._state == EXIST)
			//{ // DELETE 和 EMPTY都可以插入

			//	// 先寻找插入位置
			//	++hashi;

			//	// 循环找空位置
			//	hashi %= _tables.size();
			//}

			//_tables[hashi]._kv = kv;
			//_tables[hashi]._state = EXIST;
			//++_size;

			// 正式插入
			Hash hash;
			size_t hashi = hash(kv.first) % _tables.size();
			int i = 0;
			while (_tables[hashi]._state == EXIST)
			{
				++i;
				hashi += i * i;
				hashi %= _tables.size();
			}

			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_size;

			return true;
		}

		Data* Find(const K& key)
		{
			if (_tables.size() == 0)
				return nullptr;

			Hash hash;
			// 先找位置
			size_t hashi = hash(key) % _tables.size();
			// 记录初始位置
			size_t start = hashi;

			// 标志位为delete或tmpty都要继续找
			while (_tables[hashi]._state != EMPTY)
			{
				// 如果找到了那个数并且标志位不为delete就返回对应的地址
				if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
					return &_tables[hashi];

				++hashi;
				// 循环找
				hashi %= _tables.size();

				// 绕了一圈都没找到
				if (hashi == start)
					break;
			}

			return nullptr;
		}

		bool Erase(const K& key)
		{
			Data* pd = Find(key);
			if (!pd)
			{
				return false;
			}
			else
			{
				pd->_state = DELETE;
				--_size;
				return true;
			}
		}

		void Print()
		{
			for (int i = 0; i < _tables.size(); ++i)
			{
				if (_tables[i]._state == EXIST)
				{
					printf("[%d::%d] ", i, _tables[i]._kv.first);
				}
				else
				{
					printf("[%d::*] ", i);
				}
			}
		}

	private:
		vector<Data> _tables;
		size_t _size;
	};

	void testCH1()
	{
		/*CloseHash ch;
		int arr[] = { 3,7,19,25,36,300,7000,22 };
		for (auto i : arr)
		{
			ch.Insert(make_pair(i, i));
		}
		ch.Print();


		ch.Insert(make_pair(2, 2));
		ch.Insert(make_pair(95, 95));
		ch.Insert(make_pair(20, 20));

		ch.Erase(300);
		ch.Print();

		HashData* pd = ch.Find(7000);
		cout << pd->_kv.first << endl;
		pd = ch.Find(20);
		cout << pd->_kv.first << endl;
		pd = ch.Find(22);
		cout << pd->_kv.first << endl;
		pd = ch.Find(300);
		cout << pd->_kv.first << endl;*/

		int a[] = { 1, 9, 4, 5, 6, 7};
		CloseHash<int, int> ht;
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		ht.Insert(make_pair(44,44));

		ht.Print();

		ht.Erase(4);
		cout << ht.Find(44)->_kv.first << endl;
		cout << ht.Find(4) << endl;
		ht.Print();

		ht.Insert(make_pair(-2, -2));
		ht.Print();

		cout << ht.Find(-2)->_kv.first << endl;
	}

	void testCH2()
	{
		string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

		CloseHash<string, int> countHT;
		for (auto& str : arr)
		{
			auto ptr = countHT.Find(str);
			if (ptr)
			{
				ptr->_kv.second++;
			}
			else
			{
				countHT.Insert(make_pair(str, 1));
			}
		}

	}


	void TestCH3()
	{
		HashFunc<string> hash;
		cout << hash("abcd") << endl;
		cout << hash("bcad") << endl;
		cout << hash("eat") << endl;
		cout << hash("ate") << endl;
		cout << hash("abcd") << endl;
		cout << hash("aadd") << endl << endl;

		cout << hash("abcd") << endl;
		cout << hash("bcad") << endl;
		cout << hash("eat") << endl;
		cout << hash("ate") << endl;
		cout << hash("abcd") << endl;
		cout << hash("aadd") << endl << endl;
	}
}

namespace FangZhang_OpenHash
{
	template<class K, class V>
	struct HashNode
	{
		HashNode(const pair<K, V>& kv = make_pair(K(), V()))
			:_kv(kv)
			, _next(nullptr)
		{}

		pair<K, V> _kv;
		HashNode<K, V>* _next;
	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;

	public:
		bool Insert(const pair<K, V>& kv)
		{
			// 去重
			if (Find(kv.first))
			{
				return false;
			}
			
			// 扩容
			// 此处扩容就不需要让负载因子为0.7了
			// 直接_size == _tables.size()就行
			// 因为不会出现占用其他数据位置的情况
			if (_size == _tables.size())
			{
				size_t newSize = __stl_next_prime(_size);
				// 这里可以利用前面开辟好的节点
				// 所以就不需要像闭散列那样再整一个哈希表了
				// 复用Insert的话开销比较大,还要重新给节点开空间
				// 所以直接给一个vector利用前面开的节点就好
				vector<Node*> newTables;
				newTables.resize(newSize);
				for (auto& node : _tables)
				{
					Node* cur = node;
					Hash hash;
					while (cur)
					{
						Node* next = cur->_next;

						size_t hashi = hash(cur->_kv.first) % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;

						cur = next;
					}

					node = nullptr;
				}

				_tables.swap(newTables);
			}

			// 真正插入
			Hash hash;
			size_t hashi = hash(kv.first) % _tables.size();

			// 头插
			Node* newNode = new Node(kv);
			newNode->_next = _tables[hashi];
			_tables[hashi] = newNode;

			++_size;

			return true;
		}

		Node* Find(const K& key)
		{
			if (_tables.size() == 0)
				return nullptr;

			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
					return cur;

				cur = cur->_next;
			}

			return nullptr;
		}

		bool Erase(const K& key)
		{
			if (!Find(key))
			{
				return false;
			}

			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur && cur->_kv.first != key)
			{
				prev = cur;
				cur = cur->_next;
			}

			if (prev)
			{
				prev->_next = cur->_next;
			}
			else
			{
				_tables[hashi] = cur->_next;
			}
				
			delete cur;

			return true;
		}

		// 表的长度
		size_t TablesSize()
		{
			return _tables.size();
		}

		// 桶的个数
		size_t BucketNum()
		{
			size_t num = 0;
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				if (_tables[i])
				{
					++num;
				}
			}

			return num;
		}

		// 最长桶的长度
		size_t MaxBucketLenth()
		{
			size_t maxLen = 0;
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				size_t len = 0;
				Node* cur = _tables[i];
				while (cur)
				{
					++len;
					cur = cur->_next;
				}

				//if (len > 0)
					//printf("[%d]号桶长度:%d\n", i, len);

				if (len > maxLen)
				{
					maxLen = len;
				}
			}

			return maxLen;
		}

		size_t Size()
		{
			return _size;
		}

	private:
		inline size_t __stl_next_prime(size_t n)
		{
			static const size_t __stl_num_primes = 28;
			static const size_t __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, 3221225473, 4294967291
			};

			for (size_t i = 0; i < __stl_num_primes; ++i)
			{
				if (__stl_prime_list[i] > n)
				{
					return __stl_prime_list[i];
				}
			}

			return -1;
		}

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

	void test_HB1()
	{
		int a[] = { 1, 11, 4, 15, 26, 7, 44, 55, 99, 78, 32, 23, 30, 13};
		HashTable<int, int> ht;
		for (auto i : a)
		{
			ht.Insert(make_pair(i, i));
		}

		cout << ht.Erase(4) << endl;
		cout << ht.Erase(44) << endl;
		cout << ht.Erase(4) << endl;
		cout << ht.Erase(15) << endl;
	}

	void test_HB2()
	{
		string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };

		HashTable<string, int> countHT;
		for (auto& str : arr)
		{
			auto ptr = countHT.Find(str);
			if (ptr)
			{
				ptr->_kv.second++;
			}
			else
			{
				countHT.Insert(make_pair(str, 1));
			}
		}

		cout << endl;
	}

	void TestHT3()
	{
		int n = 19000000;
		vector<int> v;
		v.reserve(n);
		srand(time(0));
		for (int i = 0; i < n; ++i)
		{
			//v.push_back(i);
			v.push_back(rand() + i);  // 重复少
			//v.push_back(rand());  // 重复多
		}

		size_t begin1 = clock();
		HashTable<int, int> ht;
		for (auto e : v)
		{
			ht.Insert(make_pair(e, e));
		}
		size_t end1 = clock();

		cout << "数据个数:" << ht.Size() << endl;
		cout << "表的长度:" << ht.TablesSize() << endl;
		cout << "桶的个数:" << ht.BucketNum() << endl;
		cout << "平均每个桶的长度:" << (double)ht.Size() / (double)ht.BucketNum() << endl;
		cout << "最长的桶的长度:" << ht.MaxBucketLenth() << endl;
		cout << "负载因子:" << (double)ht.Size() / (double)ht.TablesSize() << endl;
	}
}

到此结束。

你可能感兴趣的:(C++,c++,哈希算法,开发语言,算法,数据结构,哈希)