问题引入:

我们在网站上注册账号时,当填好用户名后,系统都会判断用户名是否已被使用,如果已被使用,系统就会提示该用户名已被注册。充满好奇心的小明想知道系统是如何检测用户名是否被使用的。他能想到的最简单的方法就是逐个比较,但是如果用户名有很多,查找效率就显得很低;还有一种方法就是把用户名按字典序排序,二分查找,这个方法的效率的确是高了很多,可是前提是用户名是有序的,有些时候我们并不能将用户名进行排序。小明想知道有没有更好的方法呢?


我们可以用哈希表来解决这个问题。哈希表又叫散列表,关键值通过哈希函数映射到数组上,查找时通过关键值直接访问数组。在上面的例子里,我们将用户名通过哈希函数映射成一个整数,也就是数组的存储位置,在检测时用同样方法计算出存储位置,如果位置上已有元素则表示用户名已经被注册。

哈希函数指的是关键值和存储位置建立的对应关系,查找时只要根据这个关系就能找到目标位置。一般我们只要通过一次查找就能找到目标位置,但有些关键字需要多次比较和查找才能找到,这是为什么呢?因为哈希表里,可能存在关键字不同但是哈希地址相同的情况,也就是冲突。一般情况下,冲突是不可避免的,因为关键字集合往往比哈希地址集合大很多。


lua table hash 随机 随机hash表的填入_hash


哈希表的一些性质:

1.优秀的处理冲突方法可以减少比较次数,提高查找效率

2.哈希函数构造的好坏会影响查找效率

3.哈希表里的冲突只能减少,是不可避免的

常见的哈希函数构造方法:

10,这种情况下就不能用直接寻址法了。

除留余数法。我们将关键字对整数 p 取的余数直接做为存储地址,整数 p 一般取小于等于哈希表长度 size 的质数,如果关键字不是整数,比如是个字符串,可以先将其做个转换,然后再对 p 取余。选择优秀的 p 可以避免冲突。

其他的构造方法还有分析数字法,随机数法等等。

在这里我们运用直接寻址法构造一个哈希函数:

#include<iostream>
#include<string>
using namespace std;
class HashTable {
private:
    string *elem;       //表明是动态开辟存放用户名的内存
    int size;           //哈希表的表长
public:
    HashTable() {
        size = 2000;    //表长
        elem = new string[size];    //动态开辟内存
        for (int i = 0; i < size; i++) {
            elem[i] = "#";          //用来标记该位置是否已经存放用户名,"#"标记当前位置为空
        }
    }
    ~HashTable() {
        delete[] elem;      //析构函数,删除动态开辟的内存
    }
    int hash(string &index){        //哈希函数构造方法,使用for循环中的构造方法为index字符串找到其在哈希表中的对应位置
        int code = 0;
        for(size_t i = 0;i < index.length();i++){
            code = (code * 256 + index[i] + 128) % size;     
        }
        return code;
    }
};
int main() {
    HashTable hashtable;
    return 0;
}

上述哈希函数的构造方法是利用这样的一个等式:


for(size_t i = 0;i < index.length();i++){
            code = (code * 256 + index[i] + 128) % size;     
        }

那么问题来了,是否所有的哈希函数都有同样的哈希构造法?很明显不是的,对应不同的问题,我们要采取不同的哈希函数构造方法,那么我们要构造怎样的哈希函数才能有效的利用哈希表的空间呢?


所以下面是设计哈希函数要达到的两个要求

设计哈希函数时要达到两个要求:

1.计算简单,计算复杂的哈希函数会增加查询的时间;

2.关键字尽可能地均分到存储地址上,这样可以减少冲突。

常见的处理哈希冲突的方法:


开放地址法。如果发生冲突,那么就使用某种策略寻找下一存储地址,直到找到一个不冲突的地址或者找到关键字,否则一直按这种策略继续寻找。如果冲突次数达到了上限则终止程序,表示关键字不存在哈希表里。一般常见的策略有这么几种:

1. 线性探测法,如果当前的冲突位置为 d,那么接下来几个探测地址为 d + 1,d + 2,d + 3 等,也就是从冲突地址往后面一个一个探测;

2. 线性补偿探测法,它形成的探测地址为 d + m,d + 2 * m,d + 3 * m 等,与线性探测法不同,这里的查找单位不是 1,而是 m,为了能遍历到哈希表里所有位置,我们设置 m 和表长 size 互质;

3. 随机探测法,这种方法和前两种方法类似,这里的查找单位不是一个固定值,而是一个随机序列。

2,d + (-1)2,d + 22,d + (-2)2

开放地址法计算简单快捷,处理起来方便,但是也存在不少缺点。线性探测法容易形成“堆聚”的情况,即很多记录就连在一块,而且一旦形成“堆聚”,记录会越聚越多。另外,开放地址法都有一个缺点,删除操作显得十分复杂,我们不能直接删除关键字所在的记录,否则在查找删除位置后面的元素时,可能会出现找不到的情况,因为删除位置上已经成了空地址,查找到这里时会终止查找。

链地址法。该方法将所有哈希地址相同的结点构成一个单链表,单链表的头结点存在哈希数组里。链地址法常出现在经常插入和删除的情况下。

相比开放地址法,链地址法有以下优点:不会出现“堆聚”现象,哈希地址不同的关键字不会发生冲突不需要重建哈希表,在开放地址法中,如果哈希表里存满关键字了就需要扩充哈希表然后重建哈希表,而在链地址法里,因为结点都是动态申请的,所以不会出现哈希表里存满关键字的情况;相比开放地址法,关键字删除更方便,只需要找到指定结点,删除该结点即可。

开放地址法和链地址法各有千秋,适用于不同情况。

当关键字规模少的时候,开放地址法比链地址法更节省空间,因为用链地址法可能会存在哈希数组出现大量空地址的情况,而在关键字规模大的情况下,链地址法就比开放地址法更节省空间,链表产生的指针域可以忽略不计,关键字多,哈希数组里产生的空地址就少了。

下面是使用开放地址法中的线性探测法解决哈希冲突:

//函数作用,用于检验每次运用hash函数产生的哈希值的有效性
    bool search(string &index,int &pos,int ×){
        pos = hash(index);   //用于记录string类字符串index使用hash函数产生的初始哈希值
        times = 0;          //记录冲突次数
        //如果哈希值的标记处为空的话,那么就不必检验该处位置是否被占用等问题
        //而如果哈希值的标记处的值与index相等的话,则说明在处理index之前,hash表中已经处理过了与index字符串相等
        //的字符串,并且将其存放在pos处,因此此时可提示该字符串在hash表中已经存在
        //所以当该处不为空,并且占有的元素不是index时,则说明产生了"哈希冲突",因此则需要重新为index字符串寻找位置
        while(elem[pos]!="#" && elem[pos]!=index){    
            ++times;                           
            if(times < size){
                pos = (pos + 1) % size;    //pos + 1的值可能会大于size的值,使用线性探测法解决哈希冲突
            }
            else{
                return false;        //冲突次数达到了哈希表的长度,表示用线性探测法找不到能够容纳index字符串的位置
            }
        }
        if(elem[pos]==index){
            return true;        //表示hash表中已经处理过了与index字符串相等的字符串
         }
        else{
            return false;       //哈希冲突未解决
        }
    }




那么如果元素找到了自己通过哈希函数计算得的在哈希表中的位置,则此时元素需要插入哈希表中,下面是哈希表插入函数的实现:

int insert(string& index) {
        int pos, times;
        if (search(index, pos, times)) {        //首先检测一下待插入的字符串是否在hash表中已经存在,若存在则返回2
            return 2;
        } else if (times < size / 2) {          //冲突次数少于表长一半(或者可另外规定)的情况下,将index字符串插入表中,因为如果冲突次数过多,说明表中
                                                //含有的元素已经很多了,再添加可能导致表溢出等问题
            elem[pos] = index;
            return 1;
        } else {
            recreate();         //冲突次数过多,表明表中元素也过多了,无法再容纳新的元素,因此需要重建一个更大的哈希表
            return 0;
        }
    }



然后如果hash表中的已经存放的数据元素过多的话,表面现有的hash表已经无法再存放更多的元素了,因此需要重建一个更大的hash表,然后再继续加入新的数据元素:

void recreate(){
        string *temp_elem;
        temp_elem = new string[size];
        for(int i = 0;i < size;i++){
            temp_elem[i] = elem[i];       //首先申请临时空间来存放原来哈希表中已经存放好的数据
        }
        int copy_size = size;           //然后记录好原来哈希表的长度
        size = size * 2;              //扩大哈希表的长度,使其成为原来的两倍
        delete [] elem;                //删除原来申请的哈希表的空间,防止内存泄漏
        elem = new string[size];        //申请新的空间
        for (int i = 0; i < size; i++) {
            elem[i] = "#";              //标记好原来已经存放元素的位置
        }
        for (int i = 0; i < copy_size; i++) {
            if(temp_elem[i]!="#"){
                insert(temp_elem[i]);       //将原来已经有存放元素的位置重新插入表中
            }
        }
        delete [] temp_elem;    //删除临时申请的空间,防止内存泄漏
    }



hash表的基本操作完整代码实现:

#include <iostream>
#include <string>
using namespace std;
class HashTable {
private:
    string *elem;       //表明是动态开辟存放用户名的内存
    int size;           //哈希表的表长
public:
    HashTable() {
        size = 2000;    //表长
        elem = new string[size];    //动态开辟内存
        for (int i = 0; i < size; i++) {
            elem[i] = "#";          //用来标记该位置是否已经存放用户名,"#"标记当前位置为空
        }
    }
    ~HashTable() {
        delete[] elem;      //析构函数,删除动态开辟的内存
    }
    int hash(string &index){        //哈希函数构造方法,使用for循环中的构造方法为index字符串找到其在哈希表中的对应位置
        int code = 0;
        for(size_t i = 0;i < index.length();i++){
            code = (code * 256 + index[i] + 128) % size;     //构造方法可任选
        }
        return code;
    }
    //函数作用,用于检验每次运用hash函数产生的哈希值的有效性
    bool search(string &index,int &pos,int ×){
        pos = hash(index);   //用于记录string类字符串index使用hash函数产生的初始哈希值
        times = 0;          //记录冲突次数
        //如果哈希值的标记处为空的话,那么就不必检验该处位置是否被占用等问题
        //而如果哈希值的标记处的值与index相等的话,则说明在处理index之前,hash表中已经处理过了与index字符串相等
        //的字符串,并且将其存放在pos处,因此此时可提示该字符串在hash表中已经存在
        //所以当该处不为空,并且占有的元素不是index时,则说明产生了"哈希冲突",因此则需要重新为index字符串寻找位置
        while(elem[pos]!="#" && elem[pos]!=index){    
            ++times;                           
            if(times < size){
                pos = (pos + 1) % size;    //pos + 1的值可能会大于size的值,使用线性探测法解决哈希冲突
            }
            else{
                return false;        //冲突次数达到了哈希表的长度,表示用线性探测法找不到能够容纳index字符串的位置
            }
        }
        if(elem[pos]==index){
            return true;        //表示hash表中已经处理过了与index字符串相等的字符串
         }
        else{
            return false;       //哈希冲突未解决
        }
    }
    int insert(string& index) {
        int pos, times;
        if (search(index, pos, times)) {        //首先检测一下待插入的字符串是否在hash表中已经存在,若存在则返回2
            return 2;
        } else if (times < size / 2) {          //冲突次数少于表长一半(或者可另外规定)的情况下,将index字符串插入表中,因为如果冲突次数过多,说明表中
                                                //含有的元素已经很多了,再添加可能导致表溢出等问题
            elem[pos] = index;
            return 1;
        } else {
            recreate();         //冲突次数过多,表明表中元素也过多了,无法再容纳新的元素,因此需要重建一个更大的哈希表
            return 0;
        }
    }
    void recreate(){
        string *temp_elem;
        temp_elem = new string[size];
        for(int i = 0;i < size;i++){
            temp_elem[i] = elem[i];       //首先申请临时空间来存放原来哈希表中已经存放好的数据
        }
        int copy_size = size;           //然后记录好原来哈希表的长度
        size = size * 2;              //扩大哈希表的长度,使其成为原来的两倍
        delete [] elem;                //删除原来申请的哈希表的空间,防止内存泄漏
        elem = new string[size];        //申请新的空间
        for (int i = 0; i < size; i++) {
            elem[i] = "#";              //标记好原来已经存放元素的位置
        }
        for (int i = 0; i < copy_size; i++) {
            if(temp_elem[i]!="#"){
                insert(temp_elem[i]);       //将原来已经有存放元素的位置重新插入表中
            }
        }
        delete [] temp_elem;    //删除临时申请的空间,防止内存泄漏
    }

};
int main() {
    HashTable hashtable;
    string buffer;
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> buffer;
        int ans = hashtable.insert(buffer);
        if (ans == 0) {
            cout << "insert failed!" << endl;
        } else if (ans == 1) {
            cout << "insert success!" << endl;
        } else if (ans == 2) {
            cout << "It already exists!" << endl;
        }
    }
    int temp_pos, temp_times;       //记录在哈希表中插入的位置和冲突次数
    cin >> buffer;
    if (hashtable.search(buffer, temp_pos, temp_times)) {
        cout << "search success!" << endl;            //寻找该元素是否在哈希表中
    } else {
        cout << "search failed!" << endl;
    }
    return 0;
}



hash表是非常有用的工具,刚刚接触,以后要多多的学习并运用起来。