哈希结构:
顺序结构以及平衡树
中,元素关键码与其存储位置之间没有对应的关系,因此在
查找一个元素时,必须要经
过关键码的多次比较
。
顺序查找时间复杂度为
O(N)
,平衡树中为树的高度,即
O( log2N
)
,搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以
不经过任何比较,一次直接从表中得到要搜索的元素
。
如果构造一种存储结构,通过某种函数
(hashFunc)
使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函
数可以很快找到该元素
。
当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比
较,若关键码相等,则搜索成功。
该方式即为哈希
(
散列
)
方法,
哈希方法中使用的转换函数称为哈希
(
散列
)
函数,构造出来的结构称为哈希表
(Hash Table)(
或者称散列表
) .
例如:数据集合
{1
,
7
,
6
,
4
,
5
,
9}
;
哈希函数设置为:
hash(key) = key % capacity
; capacity
为存储元素底层空间总的大小。
![](https://img-blog.csdnimg.cn/16a3f9fa3648410a9abcaa753a5a38af.png)
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
问题:按照上述哈希方式,向集合中
插入元素
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
的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
。
插入
通过哈希函数获取待插入元素在哈希表中的位置 。
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探
测找到下一个空位置,插入新元素。
![](https://img-blog.csdnimg.cn/17ea9705549441c0b1b4812cd4da2f82.png)
删除
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他
元素的搜索
。比如删除元素
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.
二次探测
![](https://img-blog.csdnimg.cn/2be6fe4578b5460081c65b9b11fd0d8d.png)
研究表明:
当表的长度为质数且表装载因子
a
不超过
0.5
时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装
满的情况,但在插入时必须确保表的装载因子
a
不超过
0.5
,
如果超出必须考虑增容
因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
1.
开散列概念
开散列法又叫链地址法
(
开链法
)
,首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结
点存储在哈希表中。
从下图可以看出,
开散列中每个桶中放的都是发生哈希冲突的元素。sgissdda
![](https://img-blog.csdnimg.cn/e50cbca5f3414ca48b7835be1affaa57.png)
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");
}