数据结构笔记5: 散列

字典

字典的定义

  • 有序表
  • 能实现随机访问和顺序访问
  • 重要的操作方式:按关键字访问
  • 需要注意的问题:重复关键字

线性表描述

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(),使每个关键字值与结构中的一个唯一的存储位置相对应 ➡️ 一对一映射

Address=Hash(key)

  • 插入时,依此函数计算储存位置并按此位置存放
  • 查找时,对元素的关键字进行同样的函数计算,把求得的函数值当作元素的存储位置,在结构中按此位置取元素比较,若关键字相等,则查找成功
  • 适用范围
    (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^{2}存储
  • 优点:一定程度上解决了局部聚集的问题
  • \alpha = \frac{n}{m} 负载因子:表示哈希表满的程度

双散列法

  • 需要两个散列函数
  • 第一个散列函数计算关键字的首选地址
  • 一旦发生冲突,用第二个散列函数计算到下一地址的增量,或者直接计算下一个地址
  • 优点:随机性较强,解决了局部聚集的问题

链表法

  • 将具有相同哈希地址的元素串在一个链表当中
  • 优点:解决冲突和溢出问题
  • 改进:每个链表结尾增加一个关键字极大的节点(哨兵),链表搜索的循环条件: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;
}

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值