散列表(哈希表)
散列表的实现叫作散列,散列是一种按照关键字以一种常数平均时间复杂度的查找,插入,删除的一种实现方法。但是,所插入的数据无法得到有效的排序,所以查找最大最小的操作方法是不可行的,只能查找一个你需要查找的值。要消耗线性时间的操作在散列(哈希)是不支持的。散列也叫哈希。
散列表的工作原理
1.散列表概念
散列其实就是一个关键字对应一个数组表里面的某个位置,通过某种方式确定这个关键字来对应数组的某一个位置,然后映射到表的位置,其实这个数组表就是散列表(哈希表)。关键字可以是一个具体的数,字符,或字符串,也或者可以是某个结构体或类里面的一个数字,字符,或字符串。然后通过映射在表上的位置来获取这个关键字所在的结构体中其他的数据。这样就可以达到以常数的时间复杂度来查找到数据。
2.散列表现象
- 使用哈希表来存放int类型的数据(黄色得框架是我们插入哈希表得数据),这里的关键字是int类型得整数,关键字是通过它本身得值得大小去取模这个表的大小(size)。(key % table.size())。如果一个关键字取模之后所对应的位置已经有了,则我们需要把这个关键字的指针链接到之前已经链接上的数据,比如15,15 % 12 = 3(这个表的大小为12),所以我们需要把15链接到表下标为3的位置,然后27 % 12 = 3,映射的位置和15一样,我们只需要把27这个关键字链接到15的位置后面。
(这种链接的方法是一种尾插的方法,我们可以使用头插的方式链接到链表中会更加方便)。
(当有两个以上的关键字映射到同一个位置的时候,我们称为哈希冲突)。
这种方式的散列(哈希)实现的方法叫做开散列,拉链法。通过链表的方式解决了哈希冲突的问题。 - 一个表的大小是一个素数的大小时较好,因为素数的大小的表被取模的时候可以减少哈希冲突。或者说可以让链表不会过长。素数只能被1和自己整除,其他的数对素数取模的时候可以让关键字在哈希表分布更均匀,在查找的时候效率也更快。
3.哈希表存放的数据个数和哈希表的大小基本保持一致
数据个数和表的大小的比值为负载因子。负载因子是控制哈希表的大小是否需要扩容。在开放的拉链法当中,当负载因子是1的时候,我们就需要扩容,因为我们写完希望的是平均一个关键字对应一个哈希表的位置,这才可以让我们的查找,插入,删除的操作的平均时间复杂度为一个常数的时间复杂度。
哈希表的实现
关键字的映射
当我们的关键字是一个整数或单个字符的时候,我们可以很容易的找到关键字所要映射的位置,但我们的关键字是一个字符串的时候,我们就需要进一步的设计。这时候我们就可以把整个字符串的每一个字符用对应的ASCII来进行相加,但是我们有面临另一个问题,比如abc和acb和aad,如果他们都相加,最后的值是一样的,然后所映射的值是相同的,最后还是会多次哈希冲突,造成效率的下降。我们可以采取另一种措施,每当相加完一次的时候,我们就对相加的值进行一个乘法运算,乘以一个31或者另一个素数,让不同的字符串每个字符单纯相加可能会相等的情况进行大幅度的减少概率。
关键字我们使用一个结构体实现。K是关键字的模板,我们要取pair的第一个数据作为关键字,一个以自身结构为结构体的指针(哈希冲突的时候链接到表每个位置的链表当中)
template<class K,class T>
struct Hash_Date {
pair<K,T> _date;
Hash_Date<K,T>* _next = nullptr;
Hash_Date(const pair<K,T>& date) :_date(date) {}
};
tempalte<class K,class T>
class Hash
{
typedef Hash_Date<K,T> HD;
void SWAP(vector<HD*>& _tables, vector<Hash_Date<K,T>*>& newtables)
{
//扩容,我们直接把原哈希表的链表结点链接到新的表即可
//扩容的时候,我们需要对原来的值进行重新映射,不能按原来的表进行链接,扩容本质就是减少冲突
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])
{
HD* cur = _tables[i];
while (cur)
{
size_t hashi = cur->_date .first % newtables.size();
HD* next = cur->_next;
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
}
}
}
inline unsigned long __stl_next_prime(unsigned long n)
{
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, 3221225473, 4294967291
};
for (int i = 0;i < __stl_num_primes ;i++)
{
if (__stl_prime_list[i] > n)
return __stl_prime_list[i];
}
return __stl_prime_list[__stl_num_primes - 1];
}
public:
HashTables() { _tables.resize(4); }
~HashTables()
{
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])
{
HD* cur = _tables[i];
while (cur)
{
HD* next = cur->_next;
delete cur;
cur = nullptr;
cur = next;
}
}
}
}
bool Insert(const pair<K,T>& date)
{
if (Find(date))//判断插入的值之前是否存在,若存在,我们不再进行之后的操作,这一步骤也可以去重
return false;
if (_n == _tables.size())//判断负载因子,看是否需要扩容
{
size_t newsize = __stl_next_prime(unsigned long n);
vector<HD*> newtables(newsize);
SWAP(_tables, newtables);
_tables.swap(newtables);
}
size_t hashi = date.first % _tables.size();//取模,得到映射的位置
Hash_Date<K,T>* node = new Hash_Date<K,T>(date);
//这里的插入操作使用的是头插的方式,这种方式我们不在需要遍历一遍表进行尾插,提高一定的效率
Hash_Date<K,T>* next = nullptr;
node->_next = _tables[hashi];
//头插之后我们需要把表的指针改变成新插入的结点的指针
_tables[hashi] = node;
_n++;
return true;
}
bool Find(const pair<K, T>& date)
{
size_t hashi = date.first % _tables.size();
HD* cur = _tables[hashi];
//查找到关键字所对应的表的下标位置,然后遍历所对应的链表
while (cur)
{
if (cur->_date == date)
return true;
cur = cur->_next;
}
return false;
}
bool Earse(const pair<K, T>& date)
{
//如果有删除的元素,我们执行单链表的删除即可
if (!Find(date))
return false;
size_t hashi = date % _tables.size();
HD* cur = _tables[hashi];
if (cur->_date == date)
{
_tables[hashi] = cur->_next;
return true;
}
HD* prev = cur;
cur = cur->_next;
while (cur)
{
if (cur->_date == date)
{
prev->_next = cur->_next;
delete cur;
cur = nullptr;
return true;
}
prev = cur;
cur = cur->_next;
}
return true;
}
void Print()
{
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])
{
HD* cur = _tables[i];
while (cur)
{
cout << cur->_date << " ";
cur = cur->_next;
}
}
}
}
private:
//需要一个vector作为哈希表的储存容器,存放结点指针
vector<Hash_Date<K,T>*> _tables;
//记录表有多少数据
size_t _n = 0;
};