哈希表
1.为什么需要构建哈希表
现在有一组数据,我们想查找一个值(x)是否在这组数据中,通常来说,我们需要把这组数据遍历一遍,来看看有没有x这个值。
这时,我们发现这样查找数据要花费的时间复杂度为O(n),链表、顺序表都是如此。
为了降低查找数据的时间复杂度,那我们就不能去遍历所有的数据来查找,我们需要找到新的方法来查找数据,这时我们就引入了哈希表。
2.什么是哈希表
一个关键字,经过一个散列函数进行映射,得到的就是该关键字在表中的存储位置,那么符合这样的key及其映射关系后得到在表中的存储位置,那么这张表就可以称为散列表或哈希表。
哈希表的本质就是一个一维数组。将需要存取的数值通过一个映射关系存放在这个数组的相应位置。通过这种方法存储的元素在进行查找的时候查询时间复杂度为O(1),极大的提高了数据的搜索效率。
哈希表也叫做散列表,有对应的映射函数,散列函数和哈希函数
常见的哈希函数有:除留余数法
目的:
- 计算简单(复杂会降低查找的时间)
- 散列地址分布均匀(减少哈希冲突)
3.降低哈希冲突的方法
方法一:将哈希表的长度设置为素数
素数除了一和它本身就没有其他因数。所以在进行元素位置哈希的时候就可以很大程度上避免位置冲突。其次哈希表扩容的时候扩容的大小也应该是素数。和数组扩容不同的是:数组扩容后直接将元素拷贝到扩容后的数组空间就可以,哈希表在扩容后不仅需要将元素移动过去,还需要根据表的大小重新计算存储位置
方法二:根据装载因子进行哈希表扩容
4.解决哈希冲突的方法
方法一:线性探测法
线性探测哈希表实现
#include<iostream>
using namespace std;
//桶的状态
enum State
{
STATE_UNUSE,
STATE_USING,
STATE_DEL,
};
//桶的类型
struct Bucket
{
Bucket(int key = 0, State state = STATE_UNUSE)
:key_(key),
state_(state)
{}
int key_;
State state_;
};
class HashTable
{
public:
HashTable(int size = primes_[0], double loadFactor = 0.75)
:useBucketNum_(0)
,loadFactor_(loadFactor)
,primeIdx_(0)
{
//把用户传入的size调整到最近的比较大的素数上
if (size != primes_[0])
{
for (; primeIdx_ < PRIME_SIZE; primeIdx_++)
{
if (primes_[primeIdx_] > size)
{
break;
}
}
//用户传入的size值过大,已经超过最后一个素数,调整到最后一个素数
if (primeIdx_ == PRIME_SIZE)
{
primeIdx_--;
}
}
tableSize_ = primes_[primeIdx_];
table_ = new Bucket[tableSize_];
}
~HashTable()
{
delete[]table_;
table_ = nullptr;
}
public:
//插入操作
bool insert(int key)
{
//考虑扩容
double factor = useBucketNum_ * 1.0 / tableSize_;
cout << "factor:" << factor << endl;
if (factor > loadFactor_)
{
expand();
}
int idx = key % tableSize_;
//大量重复的代码,需要优化
//if (table_[idx].state_ != STATE_USING)
//{
// table_[idx].key_ = key;
// table_[idx].state_ = STATE_USING;
// return true;
//}
//for (int i = (idx + 1) % tableSize_; i != idx; i = (i + 1) % tableSize_)
//{
// table_[i].key_ = key;
// table_[i].state_ = STATE_USING;
// return true;
//}
int i = idx;
do
{
if (table_[i].state_ != STATE_USING)
{
table_[i].key_ = key;
table_[i].state_ = STATE_USING;
useBucketNum_++;
return true;
}
i = (i + 1) % tableSize_;
} while (i != idx);
return false;
}
bool erase(int key)
{
int idx = key % tableSize_;
int i = idx;
do
{
if (table_[i].state_ == STATE_USING && table_[i].key_ == key)
{
table_[i].state_ = STATE_DEL;
useBucketNum_--;
}
i = (i + 1) % tableSize_;
} while (table_[i].state_ != STATE_UNUSE && i != idx);
return true;
}
//查询
bool find(int key)
{
int idx = key % tableSize_;
int i = idx;
do
{
if (table_[i].state_ != STATE_USING)
{
return true;
}
i = (i + 1) % tableSize_;
} while (table_[i].state_ != STATE_UNUSE && i != idx);
return false;
}
void Show()
{
for (int i = 0; i < tableSize_; i++)
{
cout << "key:"<<table_[i].key_ <<"state: "<< table_[i].state_<<" ";
}
cout << endl;
}
private:
void expand()
{
++primeIdx_;
if (primeIdx_ == PRIME_SIZE)
{
throw"HashTable is too large,can not expand anymore";
}
Bucket* newTable = new Bucket[primes_[primeIdx_]];
for (int i =0; i < tableSize_; i++)
{
if (table_[i].state_ == STATE_USING)
{
int idx = table_[i].key_ % tableSize_;
int k = idx;
do
{
if (newTable[k].state_ != STATE_USING)
{
newTable[k].state_ = STATE_USING;
newTable[k].key_ = table_[i].key_;
break;
}
k = (k + 1) % primes_[primeIdx_];
} while (k != idx);
}
}
delete[]table_;
table_ = newTable;
tableSize_= primes_[primeIdx_];
}
private:
Bucket* table_;
int tableSize_;
int useBucketNum_;
double loadFactor_;//装载因子
static const int PRIME_SIZE = 10;//素数表的大小
static int primes_[PRIME_SIZE];//素数表
int primeIdx_;//当前使用的素数下标
};
int HashTable::primes_[PRIME_SIZE] = { 3,7,23,47,97,251,443, 911,1471, 42773 };
int main()
{
/*
int data = 3;
//找1-10000内的素数
for (int i = data; i < 10000; i++)
{//素数:除了1和它本身不能被其他数整除
int j = 2;
for (; j < i; j ++ )
{
if (i % j == 0)//i被j整除了
{
break;
}
}
if (j == i)//如果是break出来的j<i
{
cout << i << " ";
}
}
*/
HashTable hs;
hs.insert(12);
hs.insert(23);
hs.insert(45);
hs.Show();
hs.insert(24);
hs.Show();
return 0;
}
方法二:链地址法
链式哈希表实现
STL中无序的关联容器都是使用链式哈希表作为底层数据结构实现的;
C++中所有的容器都不是线程安全的,多线程情况下都必须使用线程的互斥操作
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
using namespace std;
// 链式哈希表
class HashTable
{
public:
HashTable(int size = primes_[0], double loadFactor = 0.75)
: useBucketNum_(0)
, loadFactor_(loadFactor)
, primeIdx_(0)
{
if (size != primes_[0])
{
for (; primeIdx_ < PRIME_SIZE; primeIdx_++)
{
if (primes_[primeIdx_] >= size)
break;
}
if (primeIdx_ == PRIME_SIZE)
{
primeIdx_--;
}
}
table_.resize(primes_[primeIdx_]);
}
public:
// 增加元素 不能重复插入key
void insert(int key)
{
// 判断扩容
double factor = useBucketNum_ * 1.0 / table_.size();
cout << "factor:" << factor << endl;
if (factor > loadFactor_)
{
expand();
}
int idx = key % table_.size(); // O(1)
if (table_[idx].empty())
{
useBucketNum_++;
table_[idx].emplace_front(key);
}
else
{
// 使用全局的::find泛型算法,而不是调用自己的成员方法find
auto it = ::find(table_[idx].begin(), table_[idx].end(), key); // O(n)
if (it == table_[idx].end())
{
// key不存在
table_[idx].emplace_front(key);
}
}
}
// 删除元素
void erase(int key)
{
int idx = key % table_.size(); // O(1)
// 如果链表节点过长:如果散列结果比较集中(散列函数有问题!!!)
// 如果散列结果比较离散,链表长度一般不会过长,因为有装载因子
auto it = ::find(table_[idx].begin(), table_[idx].end(), key); // O(n)
if (it != table_[idx].end())
{
table_[idx].erase(it);
if (table_[idx].empty())
{
useBucketNum_--;
}
}
}
// 搜索元素
bool find(int key)
{
int idx = key % table_.size(); // O(1)
auto it = ::find(table_[idx].begin(), table_[idx].end(), key); //
return it != table_[idx].end();
}
private:
// 扩容函数
void expand()
{
if (primeIdx_ + 1 == PRIME_SIZE)
{
throw "hashtable can not expand anymore!";
}
primeIdx_++;
useBucketNum_ = 0;
vector<list<int>> oldTable;
// swap会不会效率很低??? 交换了两个容器的成员变量
table_.swap(oldTable);
table_.resize(primes_[primeIdx_]);
for (auto list : oldTable)
{
for (auto key : list)
{
int idx = key % table_.size();
if (table_[idx].empty())
{
useBucketNum_++;
}
table_[idx].emplace_front(key);
}
}
}
private:
vector<list<int>> table_; // 哈希表的数据结构
int useBucketNum_; // 记录桶的个数
double loadFactor_; // 记录哈希表装载因子
static const int PRIME_SIZE = 10; // 素数表的大小
static int primes_[PRIME_SIZE]; // 素数表
int primeIdx_; // 当前使用的素数下标
};
int HashTable::primes_[PRIME_SIZE] = { 3, 7, 23, 47, 97, 251, 443, 911, 1471, 42773 };
int main()
{
HashTable htable;
htable.insert(21);
htable.insert(32);
htable.insert(14);
htable.insert(15);
htable.insert(22);
htable.insert(67);
cout << htable.find(67) << endl;
htable.erase(67);
cout << htable.find(67) << endl;
return 0;
}
5.线性探测和链地址的对比
对比一:两种方法下哈希表效率的提升上
对比二:多线程情况下锁的力度
6.哈希表的缺点
占用内存空间比较大
链式哈希表是一个链表的结构,每一个节点既需要存储数据又需要存储地址域。拿整数来说,存储100M的整数,存放到链式哈希表中就需要100*2=200M的内存。或者说有10亿个整数需要进行查重,1亿=100000000==100M;10亿=1G;10亿个整数就是4G,那么存储4G数据到链式哈希表里面就需要8G的内存