小谈哈希表

小谈哈希表

本文共分为四个部分,哈希表的实现、哈希冲突、哈希扩容以及自定义数据是否可以做哈希表的键值。

哈希表的实现

STL中的散列表采用链表法解决哈希冲突,初始化一个数组,将数组的每一个元素位置称为桶,每个桶上存放的是映射为同一下标的不同关键值的链表的链头元素。
因此散列表的底层数据结构为数组+链表,但是当链表元素长度过长时为避免查询事件复杂度过大,会转化成红黑树结构。

/*
  1)什么是桶?
		STL中散列表采用链接法解决冲突。结构中维护了一个vector,vector中每一个元素称为一个桶(bucket),它包含的是一个链表的第一个节点
		在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。

		简单点说,这个桶就是链表/树

*/
#include <iostream>
#include <vector>
#include <ctime>

#define MAXTABLESIZE 10000 //允许开辟的最大散列表长度
using namespace std;

class hashtable {
public:
	//先定义一个链表
	struct listNode
	{
		int val;
		listNode *next;

		listNode() : val(0), next(nullptr) {}
		listNode(int x) : val(x), next(nullptr) {}
		listNode(int x, listNode *next) : val(x), next(next) {}
	};
	//记录元素数量
	int count = 0;
	//记录当前数组的容量大小
	size_t cursize;
	//存放数据
	vector<listNode*> vec_Node;

	//初始化数组容量
	size_t size;
	//查询
	listNode* find(const int key) {

		size_t pos = fun_index(key);
		listNode *cur = vec_Node[pos];

		while (cur != nullptr) {

			if (key == cur->val)
				return cur;
			else
				cur = cur->next;
		}

		return nullptr;
	}
	//插入
	void insert(const int key) {
	
		if (find(key) != nullptr)
			return;

	
		listNode *tmp = new listNode(key);
		//计算位置
		size_t index = fun_index(key);

		listNode *cur = vec_Node[index];
		if (cur == nullptr) {
			vec_Node[index] = tmp;
		}
		else {
			tmp->next = cur->next;
			vec_Node[index]->next = tmp;
		}
		//cout << vec_Node[index]->val << endl;
		count++;

		if (count == cursize / 2) {
			cout << "扩容" << endl;
			cout << vec_Node.capacity() << endl;
			increaseCapicity();
			cout << vec_Node.capacity() << endl;
		}
	}
	//删除----链表的删除操作
	void remove(const int key) {

		listNode *node = find(key);
		if (node == nullptr)
			return;

		size_t pos = fun_index(key);
		listNode *cur = vec_Node[pos];

		//判断删除的是否是头结点
		if (cur == node) {

			vec_Node[pos] = cur->next;
			cur->next = nullptr;
			free(cur);

			count--;
			return;
		}
		while (cur != nullptr && cur->next != node) {
			cur = cur->next;
		}
		
		cur->next = node->next;
		free(node);
		count--;
		return;
	}
	
	//打印
	void show() {
		for (listNode *cur : vec_Node) {
			while(cur != nullptr) {
				cout << cur->val << "  ";

				cur = cur->next;
			}
		}
		cout << endl;
	}
	size_t getNum() {

		return count;
	}
	hashtable(size_t sz=0):size(sz) {
		init_hash_table();
	}
	~hashtable(){}

private:

	//扩容
	void increaseCapicity() {
		size = get_size() * 2;
		size_t NewSize = get_size();
		cursize = NewSize;
		//当扩容时我们需要初始化一个新的容器,然后将原有的元素进行重新映射到新的容器
		vector<listNode*> newVec(NewSize);

		
		for (listNode *node : vec_Node) {

			while (node != nullptr) {
				size_t pos = fun_index(node->val);

				listNode *cur = new listNode(node->val);
				if (newVec[pos] != nullptr) {

					cur->next = newVec[pos]->next;
					newVec[pos]->next = cur;
				}
				else {
					newVec[pos] = cur;
				}

				node = node->next;
			}
		}
		vec_Node.swap(newVec);

	}
	size_t get_size() {
			size_t n = size;
			int p = (n % 2) ? n + 2 : n + 1; //从大于n的下一个奇数开始
			int i;
			while (p <= MAXTABLESIZE)
			{
				for (i = (int)sqrt(p); i > 2; i--)
				{
					if ((p % i) == 0)
						break;
				}
				if (i == 2)
					break; //说明是素数,结束
				else
					p += 2;
			}
			return p;
		}

	void init_hash_table() {
		size_t Newsize = get_size();
		cursize = Newsize;
		vec_Node.resize(Newsize);
	}

	size_t fun_index(const int key)
	     {
	          return key % cursize;
	     }
};


vector<int> nums;
void produceRandNumbers(vector<int> &nums, int start, int end, const int amount);
int main() {
	hashtable hash(10);
	
	produceRandNumbers(nums, 0, 30, 20);
	for (int x : nums) {
		hash.insert(x);
	}
    
	hash.show();

	return 0;
}


void produceRandNumbers(vector<int> &nums, int start, int end, const int amount) {
	
	//随机种子
	srand((unsigned)time(NULL));

	for (int i = 0; i < amount; i++) {
		nums.push_back(start+ (rand() %(end - start)));
	}
}

为什么采用数组?
常数级别复杂度访问哈希桶。

哈希冲突

1、哈希冲突是否可以避免?
哈希冲突无法避免,只能减少。
2、STL如何解决哈希冲突?
1)开放寻址法,即当出现哈希冲突时,寻找下一个空的地址存储该元素。
2)链表法,即当出现冲突时,将所有冲突的元素使用链表进行组织起来,进行查表操作。并且当链表节点的长度大于8时升级为红黑树,小于6时退化成链表。
2、常用哈希函数?
除留余数法,即对键值直接进行取模操作。C++中提供了hash类,并且重载了可调用函数,可以直接使用。关于hash类,stl源码中对于基本数据类型做了特化处理,使得可以返回一个可以取模的size_t类型。同时对于字符数组(char*)和字符串(string)类型做了特殊处理,使得可以返回一个可以取模的size_t类型。
3、针对string 数据类型的哈希函数特化?

size_t hash = 0;
for (auto ch : key)
    hash = hash * 131 + ch;
return hash;
size_t hash = 0;
size_t i = 0;
for (auto ch : key)
	hash += i * ch;
return hash;

哈希扩容

1、扩容大小
哈希表将容量扩展为原来的两倍,并将原数组中的数据进行重新映射,放到新的数组
2、为什么扩容后需要重新映射?
哈希函数是与容量相关的,当容量发生变化后,相应的哈希函数会发生变化,因为原数组 中的所有元素需要使用新的哈希函数进行重新映射

自定义数据类型是否可以当作哈希表的键值?

可以。但是对自定义数据有要求。

unordered_set<typename _Kty,  typename _Hasher=hash<_Kty>,   typename _Keyeq=equal_to<_Kty>,typename _Allocator<_Kty>>

可见哈希表中有四个模板参数,第一个为插入元素的类型,第二个为hash函数(即重载可调用作用符,设计一个可用于返回直接取模的哈希值),第三个为判断两个对象是否相等的条件(在map set hashset hashmap 中都是不能重复的,因为需要重载 == 运算符(采用全局函数做友元)),第四个是分配器; 并且后三个都是提供默认值的。

因此,对于自定义数据我们需要满足两点。
1、重载可调用作用符,用于判断是否两个键值发生哈希冲突。
2、自定义哈希函数,完成键值的映射。对于哈希函数的映射我们有三种方式进行定义。
1)使用仿函数

class Hasher {//hash函数,得到hash码
public:
size_t operator()(const Person& p)const{
return hash<string>()(p.firstname) + hash<string>()(p.lastname) + hash<int>()(p.age);
}
};

unordered_set<Person,Hasher> uset;

2)模板特化(直接对hash 函数进行偏特化处理,重载其 () 运算符)

template<>
class hash<Person> {//偏特化(这里使用了标准库已经提供的hash偏特化类hash<string>,hash<int>())
public:
size_t operator()(const Person& p)const {
return hash<string>()(p.firstname)+ hash<string>()(p.lastname)+ hash<int>()(p.age);
}
};

3)自定义哈希函数

提供代码如下:

//hashkey
struct T
{
	int a;
	
	string b;
	char c;

	//重载 == 相等的运算操作符
	T(int _a, char _c, string _s):a(_a),c(_c),b(_s) {

	}
	friend bool operator==(T a, T b);
};
bool operator==(T a, T b) {

	return (a.a == b.a) && (a.b == b.b) && (a.c == b.c);
}

size_t hasher(const T& p) {//hash函数,得到hash码
	return hash<int>()(p.a) + hash<char>()(p.c) + hash<string>()(p.b);
}

template<>
class hash<T> {
public:
	size_t operator()(const T &t){
		return hash<int>()(t.a) + hash<char>()(t.c) + hash<string>()(t.b);
	}
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值