散列哈希表
散列哈希表
散列技术是在记录的存储位置和他的关键字之间建立一个确定的对应关系f,
每个关键字key对应一个存储位置f(key)。查找时,根据这个对应的关系找到给定值key的映射f(key),若查找集合中存在这个记录,则必定在f(key)的位置上。我们把这种对应关系f称为散列函数,又称为哈希(Hash)函数。
映射函数叫做散列函数,
存放记录的数组叫做散列表。
散列函数的定义:
关键字key对应一个存储位置f(key)。
查找时,根据这个对应的关系找到给定值key的映射f(key),
若查找集合中存在这个记录,则必定在f(key)的位置上。
我们把这种对应关系f成为散列函数
1.如何进行值的存放:除留余数法
把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。
增加:
用数字本身模上哈希表的长度 即为这个数字在表上的存储位置
删除:
先找到它存储的位置,然后把这个存储的位置变成0
2.哈希冲突
冲突问题:两个不同的键映射到同一个位置
[例]
设关键词序列为 {47, 7, 29, 11, 9, 84, 54, 20, 30},
散列表表长TableSizeTableSize =13 (装填因子 αα= 9/13 ≈ 0.69),
散列函数为: Unexpected text node: ’ 'h(key)=keymod11。
用线性探测法处理冲突,列出依次插入后的散列表,并估算查找性能。
所以需要注意的是–余数最好用素数,不然很容易发生冲突 所以扩容不会二倍扩容,要用素数表进行扩容 合数会容易分布到集中的地方,质数则会分散均匀一些
{3,7,23,53…}
3.哈希冲突解决方法–线性检测法
发生冲突时,往定的位置的前后去存储,但是这样有问题就是 查找的复杂度会O(1)–>O(n)
并且要注意:
查找元素时遇到空桶 要对空桶进行区分–
1.这个桶没用过,则不需要再往后找
2.这个桶的元素被删除过,要继续向后找.(这样时间复杂度可能就会变成O(n))
4.哈希冲突解决方法–装载因子法
为了减少哈希冲突且时间复杂度会增多,哈希表有一个参数加载因子/装载因子
已经使用的桶的数量/桶的总数量>0.75 此时哈希表就要进行扩容
线性探测哈希表的实现代码
// 默认的计算哈希的实现类
template<typename K>
class default_hash
{
public:
int getHash(const K &key)
{
// 通用的计算key散列码的方式 用内存地址
int size = sizeof(K);
int sum = 0;
char *p = (char*)&key;
for (int i = 0; i < size; ++i)
{
sum += (p[i] << i); // h ello olleh
}
return sum;
}
};
#if 0
template<>
class default_hash<string>
//这是错误的计算散列码的方式 是用ASCII值来计算的
{
public:
int getHash(const string &key)
{
int hash = 0;
for (int i = 0; i < key.length(); ++i)
{
hash += (key[i] << i);
}
return hash;
}
};
#endif
template<typename K, typename Hash = default_hash<K>>
class CHashTable
{
public:
CHashTable(double loadFactor = 0.75)
:mpTable(new Node[mprimeTable[0]])
,mall_bucket_num(mprimeTable[0])
,muse_bucket_num(0)
,mloadFactor(loadFactor)
{}
~CHashTable()
{
delete[]mpTable;
mpTable = nullptr;
}
void insert(const K &key) // 哈希表增加元素
{
double lf = muse_bucket_num * 1.0 / mall_bucket_num;
cout << "loadfactor:" << lf << endl;
if (lf > mloadFactor)
{
resize();
}
int index = mhash.getHash(key) % mall_bucket_num;
int index_stat = index;
do
{
if (USING != mpTable[index].mstate)
{
mpTable[index].mdata = key;
mpTable[index].mstate = USING;
muse_bucket_num++;
return;//报错用return 不要用break 避免公司其他人对代码进行添加
}
index = (index + 1) % mall_bucket_num;
} while (index_stat != index);
}
void erase(const K &key) // 哈希表删除元素
{
int index = mhash.getHash(key) % mall_bucket_num;
int index_stat = index;
do
{
if (UNUSE == mpTable[index].mstate)
{
return;
}
if (USING == mpTable[index].mstate
&& key == mpTable[index].mdata)
{
mpTable[index].mstate = USED; //析构
muse_bucket_num--;
return;
}
index = (index + 1) % mall_bucket_num;
} while (index_stat != index);
}
bool query(const K &key) // 哈希表查找元素
{
int index = mhash.getHash(key) % mall_bucket_num;
int index_stat = index;
do
{
if (USING == mpTable[index].mstate
&& key == mpTable[index].mdata)
{
return true;
}
if (UNUSE == mpTable[index].mstate)
{
return false;
}
index = (index + 1) % mall_bucket_num;
} while (index_stat != index);
return false;
}
private:
enum STATE { USING, USED, UNUSE }; // 枚举桶的状态
struct Node // 桶元素的类型
{
Node(K data = K()) // 不要写成K data = 0 不然就必须保证是整型类型了
:mstate(UNUSE)
,mdata(data) {}
STATE mstate;
K mdata;
};
Node *mpTable; // 桶的数组
int mall_bucket_num; // 桶的总数量
int muse_bucket_num; // 已使用的桶数量
double mloadFactor; // 加载因子
Hash mhash; // 计算散列码的对象
static int mprimeTable[4]; // 素数表
static int mprimeIndex; // 素数表的下标
void resize() // 哈希表扩容函数
{//扩容是遍历旧的哈希表 所以muse_bucket_num数量是没变的
mprimeIndex += 1;
Node *ptmp = new Node[mprimeTable[mprimeIndex]];
for (int i = 0; i < mall_bucket_num; i++)
{
if (USING == mpTable[i].mstate)
{
int index = mhash.getHash(mpTable[i].mdata) % mprimeTable[mprimeIndex];
int index_stat = index;
do
{
if (UNUSE == ptmp[index].mstate)
{
ptmp[index].mdata = mpTable[i].mdata;
ptmp[index].mstate = USING;
break;
}
index = (index + 1) % mprimeTable[mprimeIndex];
} while (index_stat != index);
}
}
delete []mpTable;
mpTable = ptmp;
mall_bucket_num = mprimeTable[mprimeIndex];
}
};
// {3, 7, 23, 53}
template<typename K, typename Hash> //疑问3:模板类型在类外的使用
int CHashTable<K, Hash>::mprimeTable[4] = { 3, 7, 23, 53 };
template<typename K, typename Hash>
int CHashTable<K, Hash>::mprimeIndex = 0;
int main()
{
#if 0
CHashTable<int> hashTable;
for (int i = 0; i < 20; ++i)
{
hashTable.insert(rand() % 100);
}
#endif
CHashTable<string> strHash;
strHash.insert("aaa");
strHash.insert("bbb");
strHash.insert("ccc");
strHash.insert("dda");
class Student
{
public:
Student(string name="", double score=0.0)
:_name(name), _score(score) {}
bool operator==(const Student &stu)const
{
return _name == stu._name;
}
private:
string _name;
double _score;
};
CHashTable<Student> studentHash;
studentHash.insert(Student("张三", 98.5));
studentHash.insert(Student("李四", 99.5));
studentHash.insert(Student("王五", 89.5));
cout << studentHash.query(Student("张三", 98.5)) << endl;
return 0;
}![
5.哈希冲突解决方法–链地址法
为了减少哈希冲突,哈希函数应该把数字尽可能分散的放入哈希表中。
开散列法,又叫链地址法
开散列法:对关键码集合用散列函数计算出散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个无头结点的单链表连接起来,各链表的头结点存储在哈希表中,也就相当于,此时存储各个头结点的这个数组,是一个指针数组,这个数组里面存的是每一个单链表头结点指针
散列函数要求:
1.它必须是一致的。例如,假设你输入apple时得到的是4,那么每次输入apple时,得到的都必须为4。如果不是这样,散列表将毫无用处。
2.它应将不同的输入映射到不同的数字。例如,如果一个散列函数不管输入是什么都返回1,它就不是好的散列函数。最理想的情况是,将不同的输入映射到不同的数字。
左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头(也可以理解为数组里的每个元素都是一个list),当然这个链表可能为空,也可能元素很多。 如果两个字符串在哈希表中对应的位置相同,也就是发生了哈希冲突的时候,就把元素往链表里放.所以会出现每个数组下标对应的list有长有短.
1.当链表太长
1.一个链表过长:(说明那里总是发生哈希冲突)
这个散列表中的所有元素都在这个链表中,这与一开始就将所有元素存储到一个链表中一样糟糕:散列表的速度会很慢。因为链表的时间复杂度是O(n)
说明:散列函数不合适
什么样的散列函数是良好的呢?
可研究一下SHA函数 。你可将它用作散列函数。
1.比如在版本控制git中就用到了hash:
在git中,文件内容为键值,并用SHA算法作为hash function,将文件内容对应为固定长度的字符串(hash值)。如果文件内容发生变化,那么所对应的字符串就会发生变化。git通过比较较短的hash值,就可以知道文件内容是否发生变动。
2.比如计算机的登陆密码,一般是一串字符。
然而,为了安全起见,计算机不会直接保存该字符串,而是保存该字符串的hash值(使用MD5、SHA或者其他算法作为hash函数)。当用户下次登陆的时候,输入密码字符串。如果该密码字符串的hash值与保存的hash值一致,那么就认为用户输入了正确的密码。这样,就算黑客闯入了数据库中的密码记录,他能看到的也只是密码的hash值。上面所使用的hash函数有很好的单向性:很难从hash值去推测键值。因此,黑客无法获知用户的密码。
2.所有的链表都过长
说明装载因子不合适
比如 有一个像下面这样相当满的散列表。
我们需要调整它的长度。为此,你首先创建一个更长的新数组:通常将数组增长一倍。
接下来,你需要使用函数hash将所有的元素都插入到这个新的散列表中。
填装因子越低,发生冲突的可能性越小,散列表的性能越高。一个不错的经验规则是:一旦填装因子大于0.7,就调整散列表的长度。
所以 哈希冲突则扩链 大于装载因子则扩表头
用vector以及list来写链式哈希表的好处是:
容器可以自动释放,不用手动,因为会自动调用容器底层封装的析构函数—
- 在main函数结束的时候
- 或者是在数组 – 的时候 vector减完以后会自动析构对象
用vector实现链式哈希表
#include "pch.h"
#include <iostream>
#include <vector>
#include <list>
#include <string>
#include <unordered_map> // 无序的映射表
using namespace std;
/*
顺序容器
vector:向量容器 内存2倍扩容的数组
list:链表容器 双向的循环链表
关联容器 set集合(key) map映射表(key-value)
无序的关联容器(哈希表 增删查O(1))
有序的关联容器(红黑树 增删查O(logn))
*/
//定义链式哈希表 有keyK 信息value V
//我们要用数组和链表 直接使用vector即可,在vector内放list类型
template<typename K, typename V>
struct Pair//vector<list>只需要一个参数 所以这里要把K,V封装成结构体
{
// K() 零构造 如果是整型的话,这里就直接初始化为0
Pair(const K key = K(), const V val = V())
:first(key), second(val) {}
K first;
V second;
};
// Make_Pair函数模板,封装了Pair对象的创建
//比较方便 不用在调用的时候还得写Pair<int, string>(24, "zhang san")
template<typename K, typename V>
Pair<K, V> Make_Pair(const K key = K(), const V val = V())
{
return Pair<K, V>(key, val);
}
// 不允许存储重复的key值的 34-lisi 34-zhang san
//发现重复的用新值把旧值覆盖掉
template<typename K, typename V>
class HashMap
{
public:
// 哈希表初始化长度为3 加载因子0.75
HashMap(int size = 3, double loadfactor = 0.75)
: _usedBuckets(0)
, _loadFactor(loadfactor)
{
// 要开辟长度为size的哈希表
//reserve只开空间 但是没有放链表(vector封装的数组)进去
// _buckets.reserve(size) _buckets.resize(size)
// resize不仅给vector容器开辟size个空间,还放了size个链表进去了
_buckets.resize(size);
}
// 哈希表增加操作
void insert(const Pair<K, V> &pair)
{
// 检查一下加载因子
double lf = _usedBuckets*1.0 / _buckets.size();
//默认除法得到的是整数 所以要前面乘1.0 这样能得到浮点数
cout << "lf:" << lf << endl;
if (lf >= _loadFactor)
{
expand();
}
int index = pair.first % _buckets.size();
// list<Pair<K,V>>
auto &mylist = _buckets[index];
//利用哈希表这个对象 构建list对象
if (mylist.empty()) // mylist[]
{
_usedBuckets++;//表示有个桶被占用
}
else
{
auto it = mylist.begin(); // Pair<K,V>
//vector底层封装了迭代器 所以直接用即可
for (; it != mylist.end(); ++it)
{
if (it->first == pair.first)
{
it->second = pair.second;//传进的value值给了哈希表当前这个key对应的value上
return;
}
}
}
mylist.push_back(pair);//如果没有找到对应的key键值对 就自己插进去一对
}
// 哈希表删除操作
void erase(const K &key)
{
int index = key % _buckets.size();
// list<Pair<K,V>>
auto &mylist = _buckets[index];
//注意定义引用变量是因为要改哈希表里面的元素 而不是局部变量
if (mylist.empty())
{
return;
}
auto it = mylist.begin(); // Pair<K,V>
for (; it != mylist.end(); ++it)
{
if (it->first == key)
{
mylist.erase(it);
if (mylist.empty())
{
_usedBuckets--;
}
return;
}
}
}
// 哈希表的查询操作 map[34] = value
// cout << map[34] << endl;
// map[34] = "zhang san" [34, "zhang san"]
//所以这个[ ]一共代表了查找 覆盖值 增加一个键值对的功能
V& operator[](const K &key)
{
int index = key % _buckets.size();
// list<Pair<K,V>>
auto &mylist = _buckets[index];
if (!mylist.empty())
{
auto it = mylist.begin(); // Pair<K,V>
for (; it != mylist.end(); ++it)
{
if (it->first == key)
{
return it->second;
}
}
}
//如果这个链表里没有对应的 增加一个新的key-value对
Pair<K, V> p(key);//先通过传入的先构造一个键值对
mylist.push_back(p);
return mylist.back().second;//容器里有back方法 返回哈希表里键值对的second的值
}
private:
// 哈希表的桶
vector<list<Pair<K, V>>> _buckets;
// 已占用的桶的个数
int _usedBuckets;
// 加载因子 0.75
double _loadFactor;
// 哈希表的扩容操作 2n+1进行扩容
void expand()
{//直接扩容会面临哈希表失效
// 容器的swap交换操作效率很高
vector<list<Pair<K, V>>> old;//定义一个old容器
old.swap(_buckets);//然后进行交换操作,就是成员变量交换
//此时old是原来的哈希表 现在这个bucket是个空的
_buckets.resize(old.size() * 2 + 1);//给这个哈希表扩容
_usedBuckets = 0;//已用的桶变为0
// 遍历老的哈希表,把老的哈希桶中链表的节点直接拿到新哈希表中
auto vit = old.begin();//用迭代器遍历 也可以用下标遍历
for (; vit != old.end(); ++vit) // 遍历vector的数组
{
if (!vit->empty()) // 遍历vector里面存的list
{
auto lit = vit->begin();
for (; lit != vit->end(); ++lit)
{
// #1 求相应链表的k
int index = lit->first % _buckets.size();
// #2 list的splice分片函数
if (_buckets[index].empty())
{
_usedBuckets++;
}//把这个链表拿出来放在扩容后的哈希表里
_buckets[index].push_back(*lit);
}
}
}
}
};
int main()
{
HashMap<int, string> map;
map.insert(Pair<int, string>(24, "zhang san"));//麻烦的写法
map.insert(Make_Pair(25, string("li si")));//用函数模板 自动推演出类型
map.insert({26, "wang wu"});//改版以后的写法 平时写成这样既可
map[27] = "gao yang";
map.insert({ 56, "wang wu" });
map.insert({ 89, "wang wu" });
map.insert({ 345, "wang wu" });
map.insert({ 678, "wang wu" });
map.insert({ 78, "wang wu" });
// unordered_set 集合 只存key 不存储value了
unordered_map<int, string> map1; // key-value
/*cout << map[24] << endl;
cout << map[27] << endl;
map.erase(27);
cout << map[27] << endl;*/
}
出现的问题:
当在我们写好的链式哈希表里自定义了一个类student 然后插入这个类的对象 在进行查找的时候
如果用的是内存地址的比较 就会出现问题
所以要用名字来计算散列码
............
bool find(const K &key) // 哈希表查找元素
{
int index = getHash(key) % mhashTable.size();
list<K> &mylist = mhashTable[index];
for (K &k : mylist)
{
if (k == key)
return true;
}
return false;
}
private:
vector<list<K>> mhashTable; // 链式哈希表
int muse_bucket_num; // 已使用的桶数量
double mloadFactor; // 加载因子
static int mprimeTable[4]; // 素数表
static int mprimeIndex; // 素数表的下标
int getHash(const K &key)
{
// 通用的计算key散列码的方式
int size = sizeof(K);
int sum = 0;
char *p = (char*)&key;
for (int i = 0; i < size; ++i)
{
sum += (p[i] << i); // hello olleh
}
return sum;
}
};
int main()
{ CHashTable<string> strHash;
strHash.insert("aaa");
strHash.insert("bbb");
strHash.insert("ccc");
strHash.insert("dda");
class Student
{
public:
Student(string name = "", double score = 0.0)
:_name(name), _score(score) {}
//等号运算符重载 解决内存地址不一样无法比较的问题
bool operator==(const Student &stu)const
{
return _name == stu._name;
}
//
private:
string _name;
double _score;
};
CHashTable<Student> studentHash;
studentHash.insert(Student("张三", 98.5));
studentHash.insert(Student("李四", 99.5));
studentHash.insert(Student("王五", 89.5));
cout << studentHash.find(Student("张三", 98.5)) << endl;
//当调用的是find-->gethash-->传入的是张三这个对象 gethash把这个对象的地址进行引用
//所以在find函数里我进行比较的是散列表里张三的地址和传入的这个对象张三的地址
//这两个地址大多数情况是不一样的 所以我们需要使用==运算符重载 直接比较张三这个名字
return 0;
}