字典
字典的定义
- 有序表
- 能实现随机访问和顺序访问
- 重要的操作方式:按关键字访问
- 需要注意的问题:重复关键字
线性表描述
template<class E, class K> class SortedChain {
public:
SortedChain() {first = 0;}
~SortedChain();
bool IsEmpty() const {
return first == 0;
}
int Length() const;
bool Search(const K& k, E& e) const;
SortedChain<E,K>& Delete(const K& k, E& e);
SortedChain<E,K>& Insert(const E& e);
SortedChain<E,K>& DistinctInsert(const E& e);
void Output(ostream& out) const;
private:
SortedChainNode<E,K> *first;
};
template<class E, class K>
bool SortedChain<E,K>::Search(const K& k, E& e) const { //返回是否搜索到元素
SortedChainNode<E,K> *p = first;
while (p && p->data < k) //有序存储的链表
p = p->link; //找到不比k小的元素
if (p && p->data == k) { //如果链表尚不为空且数据匹配
e = p->data;
return true;
}
return false;
}
template<class E, class K> SortedChain<E,K>& SortedChain<E,K>
::Delete(const K& k, E& e) {
SortedChainNode<E,K> *p = first, *tp = 0; //p用于移动,tp用于记录p的前一位置
while (p && p->data < k) { //找到匹配元素
tp = p;
p = p->link;
}
if (p && p->data == k) { //找到了要删的节点
e = p->data;
if (tp) //p是普通节点
tp->link = p->link;
else //p是首节点
first = p->link;
delete p;
return *this;
}
throw BadInput(); //没有可删的节点
return *this;
}
template<class E, class K>
SortedChain<E,K>& SortedChain<E,K>::Insert(const E& e) { //插入元素
SortedChainNode<E,K> *p = first, *tp = 0;
while (p && p->data < e) { //找到应该插入的位置
tp = p;
tp p
p = p->link;
}
SortedChainNode<E,K> *q = new SortedChainNode<E,K>;
q->data = e; //用节点存储插入的元素
q->link = p;
if (tp) //p是普通节点
tp->link = q;
else //p是首节点
first = q;
return *this;
}
//不允许重复关键字的插入
template<class E, class K> SortedChain<E,K>& SortedChain<E,K>
::DistinctInsert(const E& e){
SortedChainNode<E,K> *p = first, *tp = 0;
while (p && p->data < e) { //找到应该插入的位置
tp = p;
p = p->link;
}
if (p && p->data == e) //如果是重复关键字
throw BadInput();
//如果不是重复关键字
SortedChainNode<E,K> *q = new SortedChainNode<E,K>;
q->data = e; //用节点存储插入的元素
q->link = p;
if (tp) //p是普通节点
tp->link = q;
else //p是首节点
first = q;
return *this;
}
散列表
定义
- 在表项的存储位置与表项关键字之间建立一个确定的对应函数关系Hash(),使每个关键字值与结构中的一个唯一的存储位置相对应 ➡️ 一对一映射
- 插入时,依此函数计算储存位置并按此位置存放
- 查找时,对元素的关键字进行同样的函数计算,把求得的函数值当作元素的存储位置,在结构中按此位置取元素比较,若关键字相等,则查找成功
- 适用范围
(1)key的取值范围比较宽泛,但待处理的值不多
(2)存储空间有限
(3)快速查找 - 对于不同的关键字,可能得到同一哈希地址,这被称为冲突
散列函数
直接定址法
- 对关键字做一个线性计算,把计算结果当作散列地址
- 特点:简单,没有冲突发生,但很少能应用到生活中
数字分析法
- 提取信息量大(冲突较少)的局部关键字
平方取中法
- 取关键字平方后的中间几位为哈希地址
折叠法
- 将关键字分割成位数相同的几部分,其叠加和作为哈希地址
- 一般分割出的位数与散列表地址位数相同
- 原数:1234567890 总共不超过10000个数据 ➡️ 用四位数字即可表示
移位叠加:0987+6543+21=7551
间界叠加:0987+3456+21=4464 - 适用于关键字位数很多的情况
除留余数法(最常用)
- 取关键字被某个不大于哈希表长度的数,取余后得到的数位哈希地址
- 除数一般取接近表长的质数,或不包含小于20的质因数的合数
解决冲突的方法
线性开型寻址法
- 可存放新表项的空闲位置,既向它的同义词表项开放,又向它的非同义词表项开放
- 一旦发生冲突,在表中依次向后寻找下一个空闲位置
- 查找数据时按照同样的方法,如果找到一个位置为空,则说明查找失败
- 删除数据时先把数据本身删掉,然后检查后面的位置,如果那个元素按照严格映射不应该在那个位置,则把它往前挪
//采取除数取余法 template<class E, class K> class HashTable { public: HashTable(int divisor = 11); //默认设置除数 ~HashTable() { delete [] ht; delete [] empty; } bool Search(const K& k, E& e) const; HashTable<E,K>& Insert(const E& e); void Output(); private: int hSearch(const K& k) const; int m; //哈希函数的除数 E *ht; //哈希数组NeverUsed bool *empty; //一维数组,和哈希数组对应,记录该位置是否有数据 }; template<class E, class K> HashTable<E,K>::HashTable(int divisor) { m = divisor; ht = new E [m]; //分配哈希数组存储空间 empty = new bool [m]; for (int i = 0; i < m; i++) empty[i] = true; //默认设置哈希数组是空 } //寻找数据是否存在,如果存在返回k的位置,如果不存在返回插入的位置 template<class E, class K> int HashTable<E,K>::hSearch(const K& k) const { int i = k % m; //取余算出数据在哈希表中应该在的位置 int j = i; do { if (empty[j] || ht[j] == k) //如果找到k return j; j = (j + 1) % m; //如果没找到,往后移一格 } while (j != i); //如果循环一圈还没找到,跳出循环 return j; //返回应该插入的位置 } template<class E, class K> bool HashTable<E,K>::Search(const K& k, E& e) const{ int b = hSearch(k); //返回应该插入的位置 if (empty[b] || ht[b] != k) //如果该位置没有数据或该位置的数据不是k return false; //插入失败 e = ht[b]; //如果可以插入,改变哈希数组 return true; //插入成功 } template<class E, class K> HashTable<E,K>& HashTable<E,K>::Insert(const E& e) { K k = e; //提取关键字 int b = hSearch(k); //获取应该插入的位置 if (empty[b]) { //如果该位置为空,则可以插入 empty[b] = false; //更新该位置的信息 ht[b] = e; return *this; } if (ht[b] == k) //如果该位置已经放着这个数据了 throw BadInput(); //重复数据 throw NoMem(); //表满了 return *this; }
- 特点:
(1)优点:简单,只要表不满总能找到空位插入成功
(2)缺点:如果关键字本身很有规律,可能大多数值都集中在同一位置上,在局部造成严重的聚集,性能急剧下降
平方探测法
- 如果发生冲突,则往后
存储
- 优点:一定程度上解决了局部聚集的问题
负载因子:表示哈希表满的程度
双散列法
- 需要两个散列函数
- 第一个散列函数计算关键字的首选地址
- 一旦发生冲突,用第二个散列函数计算到下一地址的增量,或者直接计算下一个地址
- 优点:随机性较强,解决了局部聚集的问题
链表法
- 将具有相同哈希地址的元素串在一个链表当中
- 优点:解决冲突和溢出问题
- 改进:每个链表结尾增加一个关键字极大的节点(哨兵),链表搜索的循环条件:i && (i->data<k) ➡️ i->data < k
template<class E, class K> class ChainHashTable {
public:
ChainHashTable(int divisor = 11){
m = divisor; //设置除数
ht = new SortedChain<E,K> [m]; //哈希数组,每个元素都是一个链表
}
~ChainHashTable() {
delete [] ht;
}
bool Search(const K& k, E& e) const{
return ht[k % m].Search(k, e);
}
ChainHashTable<E,K>& Insert(const E& e){
ht[e % m].DistinctInsert(e);
return *this;
}
ChainHashTable<E,K>& Delete(const K& k, E& e){
ht[k % m].Delete(k, e);
return *this;
}
void Output() const;
private:
int m; //除数
SortedChain<E,K> *ht; //哈希链表的数组
};
平均查找长度:平均计算的操作次数
作业5
基于线性探查的散列表(neverUsed思想)
// 12 13 14 25 80 81 11 75 76 74
template<typename T>
class HashTable{
public:
HashTable(int d=11){
divisor=d;
neverUsed=new bool[divisor];
ht=new int[divisor];
for(int i=0;i<divisor;i++){
neverUsed[i]=true;
ht[i]=-1;
}
}
int search(T t);
double falseEmpty(); //返回实际是空桶,但neverUsed是false所占空桶的百分比
HashTable<T>& insert(T t);
HashTable<T>& deleted(T t);
HashTable<T>& reorganize();
void output();
private:
int divisor; //除数
bool *neverUsed; //记录是否存储过数据
int *ht; //哈希数组
};
template<typename T>
double HashTable<T>::falseEmpty(){
double empty=0; //总的空桶数
double falseEmp=0; //没有数据但neverUsed为false的空桶数
for(int i=0;i<divisor;i++)
if(ht[i]==-1){
empty++;
if(!neverUsed[i])
falseEmp++;
}
return falseEmp/empty; //返回占比
}
template<typename T>
int HashTable<T>::search(T t){ //返回应该插入的位置
int mark=t%divisor;
int index=mark;
do{
if(neverUsed[index]||ht[index]==t) //该位置为空或者该数据已存在
return index;
index=(index+1)%divisor;
} while(index!=mark);
return -1;
}
template<typename T>
HashTable<T>& HashTable<T>::insert(T t){
int mark=search(t);
if(mark==-1||ht[mark]==t) //如果没有找到或者该数据已存在
cout<<"Insert Failure!"<<endl;
else{
ht[mark]=t;
neverUsed[mark]=false;
}
return *this;
}
template<typename T>
HashTable<T>& HashTable<T>::deleted(T t){ //删除数据
int mark=t%divisor;
int index=mark;
do{
if(ht[index]==t) //找到数据
break;
index=(index+1)%divisor;
} while(index!=mark);
if(ht[index]!=t)
cout<<"Delete Failure!"<<endl;
else
ht[index]=-1;
return *this;
}
template<typename T>
HashTable<T>& HashTable<T>::reorganize(){ //重新组织散列表
if(falseEmpty()<0.6) //60%以上的空桶域为false时重组
return *this;
int *temp=new int[divisor]; //用来储存哈希表中现有的数据
int mark=0;
for(int i=0;i<divisor;i++){
if(ht[i]!=-1){
temp[mark]=ht[i];
mark++;
ht[i]=-1;
}
neverUsed[i]=true;
}
for(int i=0;i<mark;i++) //重新插入
insert(temp[i]);
cout<<"Reorganized hash table:"<<endl;
output();
return *this;
}
template<typename T>
void HashTable<T>::output(){ //输出散列表
cout<<"Hash table:"<<endl;
for(int i=0;i<divisor;i++){
if(!neverUsed[i])
cout<<ht[i]<<" ";
else //没有元素输出0
cout<<"0"<<" ";
}
cout<<endl;
}
int main(){
HashTable<int> ht;
int t;
cout<<"Please input 10 numbers for your hash table!"<<endl;
for(int i=0;i<10;i++){
cin>>t;
ht.insert(t);
}
ht.output();
cout<<"Please input 5 numbers you want to delete!"<<endl;
for(int i=0;i<5;i++){
cin>>t;
ht.deleted(t);
}
ht.reorganize();
return 0;
}
HashChainsWithTails
//69 36 11 55 33 66 49 20 23
template<typename T>
class chain;
template<typename T>
class hashChainsWithTails;
template<typename T>
class node{ //节点类
friend chain<T>; //声明友元
friend hashChainsWithTails<T>;
private:
T data;
node *next;
};
template<typename T>
class chain{ //链表类
friend hashChainsWithTails<T>;
public:
chain(){
head=NULL;
}
private:
node<T> *head; //表头
};
template<typename T>
class hashChainsWithTails{
public:
hashChainsWithTails(int d=11){
divisor=d;
table=new chain<T>[divisor];
tail=new node<T>;
tail->data=2147483647; //尾节点的关键字比其他都大
tail->next=NULL;
for(int i=0;i<divisor;i++){
table[i].head=new node<T>;
table[i].head->next=tail; //每个链表共有一个尾节点
}
}
node<T>* find(T t){ //返回该关键字所应该在的位置的前一个节点
int mark=t%divisor;
node<T> *pre=table[mark].head->next;
if(pre==tail) //如果该链表还没有存放任何关键字
return pre;
if(pre->data>=t) //如果该链表的第一个关键字就比该关键字大
return table[mark].head;
while(pre->next->data<t) //找到该关键字应该在的位置
pre=pre->next;
return pre;
}
void insert(T t){ //插入关键字
int mark=t%divisor;
node<T> *p=new node<T>;
p->data=t;
p->next=NULL;
node<T> *pre=find(t);
if(pre==tail){
table[mark].head->next=p;
p->next=tail;
}
else{
p->next=pre->next;
pre->next=p;
}
}
void deleted(T t){ //删除关键字
node<T> *pre=find(t);
if(pre==tail||pre->next->data!=t)
cout<<"Delete failure!"<<endl;
else
pre->next=pre->next->next;
}
void output(){ //输出哈希表
for(int i=0;i<divisor;i++){
cout<<"Hash table["<<i<<"]: ";
node<T> *p=table[i].head->next;
while(p!=tail){
cout<<p->data<<" ";
p=p->next;
}
cout<<endl;
}
}
private:
node<T> *tail; //共有的尾节点
chain<T> *table; //哈希表
int divisor; //除数
};
int main(){
hashChainsWithTails<int> h;
int t;
for(int i=0;i<9;i++){
cin>>t;
h.insert(t);
}
h.output();
for(int i=0;i<5;i++){
cin>>t;
h.deleted(t);
}
h.output();
return 0;
}