数据结构——散列表

定义:

散列表是一种以常数平均时间执行插入、删除和查找的技术。它可以根据给定的关键字来计算关键字在表中的地址的数据结构,它建立了关键字和存储地址之间的一种直接映射关系。

散列函数就是可以将每个关键字映射到从0到 散列表长度-1 这个范围中的某个数,并且放到适当的单元中的一种函数。

冲突就是当散列函数把两个或者两个以上的不同关键字映射到同一地址的时候,我们把这种情况的发生称为冲突,这些发生碰撞的不同关键字称为同义词。

散列函数的构造方法:

1.直接寻址法:取关键字的某个线性函数为散列地址,散列函数为a*key+b。a和b是常数,这种方法最简单不会产生冲突,但是空间消耗特别大。

const int a=1;
const int b=0;
int Hash::direct_difine(int key)
{
   return a*key+b;
}

2.除留余数法:m是散列表的长度,取一个不大于m但是最接近或等于m的质数p,利用key%p求出散列的地址下标,通常一个不太接近 2的整数幂 的素数是一个不错的选择。——这是使用最多的一种方法。

const int p=51;
int Hash::division(int key)
{
    return key%p;
}

 3.数字分析法

设关键字是r进制数,而r个数码在各位上出现的频率不一定相同,可能在某些位上分布的均匀些,每种数码出现的机会均等;而在某些位上分布的不均匀,只有某几种数码经常出现,则应选取数码分布较为均匀的若干位作为散列地址。这种方法适用于已知的关键字集合:如电话号码,电话号码前面的数字分布不均匀,而后面的四位数分布均匀,取后面四位数为下标。

int Hash::analyse_number(int key)
{
    int n=key;
    int res=0;
    for(int i=0;i<4;i++)
    {
        res=res*10+(n%10);
        n=n/10;
    }
    return res;
}

4.平方取中法

顾名思义,取关键字的平方值的中间几位作为散列地址,具体取多少按实际情况而定。例子:取倒数从第二位往前3位。

int Hash::pow_use_mid(int key)
{
    int res=0;
    int pow_number=key*key;
    for(int i=0;i<4;i++)
    {
        if(i>0)
            res=res*10+(n%10);
        n=n/10;
    }
    return res;
}

5.折叠法

将关键字分割位位数相同的几部分(最后一部分的位数可以稍微短一些),然后取这几部分的叠加和作为散列地址,这种方法被称为折叠法。当关键字位数很多,而且关键字中每一位上的数字分布大致均匀是,可以采用折叠法得到散列地址。这个属实不好演示!!!

6.乘法散列法

先使用关键字key乘以常数A(0<A<1),并提取A*key的小数部分的值,用m乘以这个值,再向下取整的最终值为下标。m是和上面除留余数法的m是一个概念,但是这种方法中的m取值不影响散列分布的情况,一般取为2的某个整数幂(应该最好小于散列的长度)

const double A=0.3;
const int m=64;
int Hash::chain(int key)
{
    double num=key*A;
    while(num>1)//取小数部分
    {
        num--;
    }
    return floor(num*m);//向下取整
}

7.全域散列法——不是很懂!!!!!!

解决冲突的方法:

一.分离链接法——可以结合list类来写,但是我对list操作不熟练!!!所以定义的链表节点来手动连接操作……

我演示这个方法使用的是一个存储字符串的散列表,主要操作有散列函数,插入函数,删除函数,查找函数(查询包含与否的函数于此函数一样,只有返回值不同)。还有更加高端的一种操作是我在别的博主哪里看到的,再散列函数,这个操作可以令散列分布的更加均匀,使得程序的查找时间大大的缩短,当插入元素的个数大于表的长度的时候就会扩充表的长度为最接近 2倍的原长度 的一个素数。而求最接近并且大于n的一个素数的函数nextPrime操作我写不来!!!!

大佬博主的文章链接:https://blog.csdn.net/Linux_ever/article/details/51133708?utm_source=app

可以去学习一下他的文章,再散列真的很厉害,可惜我不是很能看懂那个nextPrime的操作,以后有时间再去继续思考那篇文章,回头把我的散列表再修改一下!!!!

插入函数

//插入
void detach_link::insert(const string&s)
{
    int index=hash(s,length);

    if(containes(s))//如果要添加的项已经在集合中就不需要执行添加操作了
        return;
    if(theLists[index]==nullptr)
        theLists[index]=new node(s,nullptr);
    else
    {
        node*cur=new node(s,theLists[index]->next);
        theLists[index]->next=cur;
    }
    number++;
}

散列函数——记得初始化,我忘记初始化出了好多麻烦!!!!!!

//处理字符串的一种散列函数——除留余数法
int detach_link::hash(const string&key,int tableSize)
{
    int hashVal=0;
    for(int i=0; i<key.size(); i++)
        hashVal=(hashVal*32)+key[i];
    //cout<<hashVal<<endl;
    hashVal=hashVal%tableSize;
    if(hashVal<0)
        hashVal=hashVal+tableSize;
    return hashVal;
}

删除函数

//删除
void detach_link::remove(const string&s)
{
    if(!containes(s))
    {
        cout<<"没有要删除的内容"<<endl;
        return;
    }
    int index=hash(s,length);
    if(theLists[index]->name==s)
    {
        node*q=theLists[index];
        theLists[index]=theLists[index]->next;
        delete q;
        return;
    }
    for(node*p=theLists[index]; p->next!=nullptr; p=p->next)
    {
        if(p->next->name==s)
        {
            node*q=p->next;
            p->next=p->next->next;
            delete q;
            break;
        }
    }
}

查找函数

//在链表中查找节点
node*detach_link::find(const string&s)
{
    int index=hash(s,length);
    for(node*p=theLists[index]; p!=nullptr; p=p->next)
    {
        if((p->name)==s)
            return p;
    }
    return nullptr;
}

整体代码

#include<iostream>
#include<vector>
#include<list>
#include<cstdbool>
using namespace std;
typedef struct node
{
    string name=" ";
    node*next=nullptr;
    node(const string&s,node*p):name(s),next(p) {}
} node;
class detach_link
{
public:
    detach_link();
    void print();//查看散列表中的内容
    bool isEmpty();//判断散列表是否为空
    bool containes(const string&s);//判断是否包含此字符串
    void insert(const string&s);//插入项
    void remove(const string&s);//删除项
    node*find(const string&s);//根据名字查找数据项
    ~detach_link();
private:
    vector<node*>theLists;//存放链表的数组,初始长度为100
    int length;//数组长度
    int number;//集合中元素的个数
    int hash(const string&key,int tableSize);//散列函数
    void makeEmpty();//清空销毁散列表
    //void rehash();再散列——这里可能需要查找距离n最近的一个素数来再散列,而且分离链接再散列和寻找距离n最近的一个素数的目的是为了缩短程序的查找时间,有点高端操作,我看一个博主的操作学不来^_^!!
    //int nextPrime(int n);求距离n最近的一个素数
};
//初始化数组的长度为100,并且全部置为空指针
detach_link::detach_link()
{
    theLists.resize(100,nullptr);
    length=100;
    number=0;
}
//销毁空间
detach_link::~detach_link()
{
    //makeEmpty();
}
//检查某个元素是否在数组中
bool detach_link::containes(const string&s)
{
    int index=hash(s,length);
    //cout<<index<<"  "<<s<<endl;
    for(node*p=theLists[index]; p!=nullptr; p=p->next)
    {
        if((p->name)==s)
            return true;
    }
    return false;
}
//在链表中查找节点
node*detach_link::find(const string&s)
{
    int index=hash(s,length);
    for(node*p=theLists[index]; p!=nullptr; p=p->next)
    {
        if((p->name)==s)
            return p;
    }
    return nullptr;
}
//判断散列表是否为空
bool detach_link::isEmpty()
{
    if(number==0)
        return true;
    return false;
}
//查看散列表中的内容
void detach_link::print()
{
    for(int i=0; i<length; i++)
    {
        for(node*cur=theLists[i]; cur!=nullptr; cur=cur->next)
        {
            cout<<cur->name<<endl;
        }
    }
}
//处理字符串的一种散列函数——除留余数法
int detach_link::hash(const string&key,int tableSize)
{
    int hashVal=0;
    for(int i=0; i<key.size(); i++)
        hashVal=(hashVal*32)+key[i];
    //cout<<hashVal<<endl;
    hashVal=hashVal%tableSize;
    if(hashVal<0)
        hashVal=hashVal+tableSize;
    return hashVal;
}
//插入
void detach_link::insert(const string&s)
{
    int index=hash(s,length);

    if(containes(s))//如果要添加的项已经在集合中就不需要执行添加操作了
        return;
    if(theLists[index]==nullptr)
        theLists[index]=new node(s,nullptr);
    else
    {
        node*cur=new node(s,theLists[index]->next);
        theLists[index]->next=cur;
    }
    number++;
}
//删除
void detach_link::remove(const string&s)
{
    if(!containes(s))
    {
        cout<<"没有要删除的内容"<<endl;
        return;
    }
    int index=hash(s,length);
    if(theLists[index]->name==s)
    {
        node*q=theLists[index];
        theLists[index]=theLists[index]->next;
        delete q;
        return;
    }
    for(node*p=theLists[index]; p->next!=nullptr; p=p->next)
    {
        if(p->next->name==s)
        {
            node*q=p->next;
            p->next=p->next->next;
            delete q;
            break;
        }
    }
}
//清空销毁散列表
void detach_link::makeEmpty()
{
    for(int i=0; i<length; i++)
    {
        node*cur=theLists[i];
        while(cur!=nullptr)
        {
            node*p=cur->next;
            delete cur;
            cur=p;
        }
    }
}
int main()
{
    detach_link use;
    if(use.isEmpty())
        cout<<"散列表为空"<<endl;
    use.insert(string("TuBoWen"));
    use.insert(string("Iverson"));
    use.insert(string("Curry"));
    use.insert(string("Kid"));
    use.insert(string("Irving"));
    use.insert(string("Paul"));
    use.print();
    cout<<"插入成功!!"<<endl;
    use.remove("Kid");
    use.remove("TuBoWen");
    use.print();
    cout<<"删除成功!!!"<<endl;
    if(use.isEmpty())
        cout<<"散列表为空"<<endl;
    cout<<"查找数据:Iverson"<<endl;
    if(use.find("Iverson")!=nullptr)
        cout<<use.find("Iverson")->name<<endl;
    return 0;
}

运行:

二.开放定址法

1)线性探测法

当冲突发生时,舒徐查看表中下一个单元(当探测到表尾地址时,下一个探测地址是首地址0),直到找出一个空闲单元(当表未填满时一定能找到一个空闲单元)或查遍全表。但是线性探测法会造成大量元素再相邻的散列地址上聚集起来,这样就大大降低了查找效率。

int Open_Adress::findPos(const string&x)
{
    int index=hash(x,data.size());

    //线性探测
    while(data[index].status!=0&&data[index]->name!=x)
    {
        index=(index+1)%data.size();
    }

    return index;
}

2)平方探测法

假设发生冲突的地址为d,平方探测法的到的新的地址序列为 d+(i的平方) i是发生冲突的次数,平方探测法是一种较好的处理冲突的方法,可以避免出现“堆积”的问题,它的缺点是不能探测到散列表上的所有单元,但至少能探测到一半单元。

int Open_Adress::findPos(const string&x)
{
    int index=hash(x,data.size());
    //平方探测,当插入元素个数超过列表长度一半的时候需要再散列操作
    //如果找到了空位置就返回该元素的位置编号
    //如果找到了该元素x,则返回该元素的位置编号
    int clash=1;//在找位置时发生的冲突的次数
    while(data[index]->status!=0&&data[index]->name!=x)
    {
        index=(index+2*clash-1)%data.size();
        clash++;
    }
    return index;
}

3)再散列法——又称双散列法,目前实现不了

需要使用两个散列函数,当通过第一个散列函数Hash_1(key)得到的地址发生冲突时,则利用第二个散列函数Hash_2(key)计算关键字的地址增量。Hash_2函数的设计十分需要注意,否则就会出现预选位置提前使用完毕这种非常糟糕的情况!!!

m是表长,i是冲突次数,初值为0.

index=[Hash_1(key)+i*Hash_2(key)]%m

或index=(index+(R-hash(x,m)%R))%m

R=prePrime(m)

prePrime函数是寻找最接近n的并且小于m的素数

int Open_Adress::findPos(const string&x)
{
    int index=hash(x,data.size());
    //!!双散列法,双散列法对R的选择比较苛刻,R必须是一个最接近data.size()的素数,否则表的预选位置就有可能提前用完,但是prePrime我不会写!!
    while(data[index].status!=0&&data[index]->name!=x)
    {
        index=(index+(prePrime(data.size())-hash(x,data.size())%prePrime(data.size()))  )%data.size();
    }

    return index;
}

很不好意思的说上面的代码是抄的……

4)伪随机序列法:当地址发生冲突的时候,地址增量为伪随机数序列,被称为伪随机序列法。了解即可

演示这个方法使用的是一个存储字符串的散列表,主要操作有散列函数(与上面的一样),插入函数,删除函数,查找是否包含函数,寻找元素位置函数,扩充散列长度的再散列函数。但是双散列法我目前演示不出来,因为双散列法对于R的选择比较苛刻,R必须是一个最接近 散列表长度的素数 否则表的预选位置就有可能提前使用完毕,造成一种非常糟糕的情况!!!其次再散列的操作和上面一样,由于求最接近并且大于n的一个素数的函数nextPrime操作我写不来,但是开放定址法的长度是在必要的时候需要延长的,所以我写了一个阉割版的,直接扩充长度为原长度的两倍,但是我这样的写会导致整个散列表插入,查找,定位等函数运行效率低下。整段程序还是主要使用的是平方探测法。

同样还是上文提到的那个大佬的另一篇博客写的真的很详细:https://blog.csdn.net/Linux_ever/article/details/51143042?utm_source=app

插入函数

当findPos函数使用线性探测法时,每当散列表中元素填满散列表时采用再散列,不过由于聚集情况的发生会使效率低下,当然在散列表中元素超过长度一般时就再散列也没什么问题。当findPos函数使用平方探测法时,就如下所示在散列表中元素超过长度一般时就再散列。至于双散列方法目前还不是很懂,应该也是在散列表中元素超过长度一般时就再散列,以后有时间再想想!!

void Open_Adress::insert(const string&x)
{
    int index=findPos(x);
    if(data[index]->status==1)//说明x已经在散列表中插入过了
        return;
    data[index]->name=x;
    data[index]->status=1;
    value++;
    if(value>(data.size()/2))//如果数组元素的个数超过了长度的一般
        rehash();//再散列
}

删除函数

void Open_Adress::remove(const string&x)
{
    int index=findPos(x);
    if(data[index]->status==0)
    {
        cout<<"该元素不存在于散列表中!!"<<endl;
        return;
    }
    data[index]->name="";
    data[index]->status=0;
    value--;
    return;
}

查找元素是否包含函数

bool Open_Adress::contain(const string&x)
{
    int index=findPos(x);
    if(data[index]->status==1)
        return true;
    return false;
}

再散列函数(扩充散列表长度)

void Open_Adress::rehash()
{
    int oldLen=data.size();
    //复制信息
    vector<string>oldName;
    for(int i=0;i<oldLen;i++)
    {
        if(data[i]->status==1)
            oldName.push_back(data[i]->name);
    }
    //扩充空间为原来的2倍
    for(int i=0;i<oldLen;i++)
    {
        Hybrid*p=new Hybrid("",0);
        data.push_back(p);
    }
    makeEmpty();//清空原散列表
    //复制散列表
    for(int i=0;i<oldName.size();i++)
    {
        insert(oldName[i]);
    }
}

寻找元素位置函数

这里是开放定址法的各个方法区分的关键函数,各种算法的不同会导致不同的返回值,线性探测的方法和再散列的方法(抄其他博主的,自己不是很会写)都写了,但主要使用的是平方探测法。

int Open_Adress::findPos(const string&x)
{
    int index=hash(x,data.size());

    //线性探测
    /*while(data[index].status!=0&&data[index]->name!=x)
    {
        index=(index+1)%data.size();
    }*/


    /*//!!双散列法,双散列法对R的选择比较苛刻,R必须是一个最接近data.size()的素数,否则表的预选位置就有可能提前用完,但是prePrime我不会写!!
    while(data[index].status!=0&&data[index]->name!=x)
    {
        index=(index+(prePrime(data.size())-hash(x,data.size())%prePrime(data.size()))  )%data.size();
    }*/


    //平方探测,当插入元素个数超过列表长度一半的时候需要再散列操作
    //如果找到了空位置就返回该元素的位置编号
    //如果找到了该元素x,则返回该元素的位置编号
    int clash=1;//在找位置时发生的冲突的次数
    while(data[index]->status!=0&&data[index]->name!=x)
    {
        index=(index+2*clash-1)%data.size();
        clash++;
    }
    return index;
}

整段代码

#include<iostream>
#include<vector>
#include<cstdbool>
using namespace std;
typedef struct Hybrid
{
    string name="";
    int status=0;//数组中位置的2种状态
    Hybrid(string x,int s):name(x),status(s) {}
} Hybrid;
class Open_Adress
{
private:
    vector<Hybrid*>data;//存储数据,初始长度为10
    int value;//插入散列表的数值的个数
    void makeEmpty();//清空散列表
    int hash(const string&key,int tableSize);//散列函数
    int findPos(const string&x);//为插入元素寻找位置
    void destroy();
    void rehash();//再散列操作,当散列表空间不够的时候扩充散列表空间——理应扩充至 最接近oldSize*2的素数 长度的新长度,但是能力有限!!!函数写不出来
public:
    Open_Adress();
    void insert(const string&x);//向散列表中插入x
    bool contain(const string&x);//检查散列表中是否包含x
    void remove(const string&x);//从散列表中删除x
    void print();//打印散列表中内容
    bool isEmpty();//判断散列表是否为空
    ~Open_Adress();
};

Open_Adress::Open_Adress()
{
    for(int i=0;i<10;i++)
    {
        Hybrid*p=new Hybrid("",0);
        data.push_back(p);
    }
    value=0;
}
int Open_Adress::hash(const string&key,int tableSize)
{
    int hashVal=0;
    for(int i=0; i<key.size(); i++)
        hashVal=(hashVal*32)+key[i];
    hashVal=hashVal%tableSize;
    if(hashVal<0)
        hashVal=hashVal+tableSize;
    return hashVal;
}
void Open_Adress::print()
{
    for(int i=0;i<data.size();i++)
    {
        if(data[i]->status==1)
            cout<<i<<"——"<<data[i]->name<<endl;
    }
}
bool Open_Adress::contain(const string&x)
{
    int index=findPos(x);
    if(data[index]->status==1)
        return true;
    return false;
}
void Open_Adress::rehash()
{
    int oldLen=data.size();
    //复制信息
    vector<string>oldName;
    for(int i=0;i<oldLen;i++)
    {
        if(data[i]->status==1)
            oldName.push_back(data[i]->name);
    }
    //扩充空间为原来的2倍
    for(int i=0;i<oldLen;i++)
    {
        Hybrid*p=new Hybrid("",0);
        data.push_back(p);
    }
    makeEmpty();//清空原散列表
    //复制散列表
    for(int i=0;i<oldName.size();i++)
    {
        insert(oldName[i]);
    }
}
void Open_Adress::remove(const string&x)
{
    int index=findPos(x);
    if(data[index]->status==0)
    {
        cout<<"该元素不存在于散列表中!!"<<endl;
        return;
    }
    data[index]->name="";
    data[index]->status=0;
    value--;
    return;
}
bool Open_Adress::isEmpty()
{
    return value==0;
}
int Open_Adress::findPos(const string&x)
{
    int index=hash(x,data.size());

    //线性探测
    /*while(data[index].status!=0&&data[index]->name!=x)
    {
        index=(index+1)%data.size();
    }*/


    //!!双散列法,双散列法对R的选择比较苛刻,R必须是一个最接近data,size()的素数,否则表的预选位置就有可能提前用完,但是prePrime我不会写!!
    /*while(data[index].status!=0&&data[index]->name!=x)
    {
        index=(index+(prePrime(data.size())-hash(x,data.size())%prePrime(data.size())))%data.size();
    }*/


    //平方探测,当插入元素个数超过列表长度一半的时候需要再散列操作
    //如果找到了空位置就返回该元素的位置编号
    //如果找到了该元素x,则返回该元素的位置编号
    int clash=1;//在找位置时发生的冲突的次数
    while(data[index]->status!=0&&data[index]->name!=x)
    {
        index=(index+2*clash-1)%data.size();
        clash++;
    }
    return index;
}
void Open_Adress::insert(const string&x)
{
    int index=findPos(x);
    if(data[index]->status==1)//说明x已经在散列表中插入过了
        return;
    data[index]->name=x;
    data[index]->status=1;
    value++;
    if(value>(data.size()/2))//如果数组元素的个数超过了长度的一般
        rehash();//再散列
}
void Open_Adress::makeEmpty()
{
    for(int i=0;i<data.size();i++)
    {
        data[i]->name="";
        data[i]->status=0;
    }
    value=0;
}
void Open_Adress::destroy()
{
    for(int i=0;i<data.size();i++)
    {
        delete data[i];
    }
}
Open_Adress::~Open_Adress()
{
    this->destroy();
}
int main()
{
    Open_Adress oad;
    if(oad.isEmpty())
        cout<<"散列表为空!!"<<endl;
    oad.insert("TuBowen");
    oad.insert("Nash");
    oad.insert("Curry");
    oad.insert("Iverson");
    oad.print();
    cout<<"插入成功!!"<<endl;
    oad.insert("Kid");
    oad.insert("Morant");
    oad.print();
    cout<<"再散列成功!!不过是阉割版的再散列操作,毕竟不会找素数!!"<<endl;
    if(oad.isEmpty())
        cout<<"散列表为空!!"<<endl;
    if(oad.contain("TuBowen"))
        cout<<"TuBowen"<<endl;
    cout<<"查找操作成功!!"<<endl;
    oad.remove("TuBowen");
    oad.print();
    cout<<"删除操作成功!!"<<endl;
    if(oad.contain("TuBowen"))
        cout<<"TuBowen"<<endl;
    return 0;
}

运行:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值