数据结构:哈希表

一、哈希表(Hash Table)

也叫做散列表。是根据关键码值(Key Value)直接进行访问的数据结构。哈希表通过「 key 」和「映射函数 Hash(key) 」计算出对应的「 value」,把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做「哈希函数(散列函数)」,存放记录的数组叫做「哈希表(散列表)」。

通过哈希表,我们输入相同的值会得到相同的返回值,输入不同的值会得到不同的返回值。在哈希表的实际应用中,关键字的类型除了数字类,还有可能是字符串类型、浮点数类型、大整数类型,甚至还有可能是几种类型的组合。一般我们会将各种类型的关键字先转换为整数类型,再通过哈希函数,将其映射到哈希表中。

二、实现哈希表可以分成两部分:

  • 向哈希表中插入一个关键码值:哈希函数决定该关键字的对应值应该存放到表中的哪个区块,并将对应值存放到该区块中。

  • 在哈希表中搜索一个关键码值:使用相同的哈希函数从哈希表中查找对应的区块,并在特定的区块搜索该关键字对应的值。

哈希函数方法有:直接定址法、除留余数法、平方取中法、基数转换法

三、哈希函数的常用定义方法 

 1.直接定址法:即:Hash(key) = key 或者 Hash(key) = a * key + b,其中 a 和 b 为常数。

适合于关键字分布基本连续的情况,如果关键字分布不连续,空位较多,则会造成存储空间的浪费。

2. 除留余数法:

假设哈希表的表长为 m,取一个不大于 m 但接近或等于 m 的质数 p,利用取模运算,将关键字转换为哈希地址。即:Hash(key) = key % p,其中 p 为不大于 m 的质数。(p一般取素数、或者m)。

3平方取中法:

Hash(key) = (key * key) // 100 % 1000先计算平方,去除末尾的 2 位数,再取中间 3 位数作为哈希地址。

这种方法因为关键字平方值的中间几位数和原关键字的每一位数都相关,所以产生的哈希地址也比较均匀,有利于减少冲突的发生。

4.基数转换法:

将关键字看成另一种进制的数再转换成原来进制的数,然后选其中几位作为哈希地址。

比如,将关键字看做是 13 进制的数,再将其转变为 10 进制的数,将其作为哈希地址。

四、哈希表满足以下条件:

  • 哈希函数应该易于计算,并且尽量使计算出来的索引值均匀分布。

  • 哈希函数计算得到的哈希值是一个固定长度的输出值。

  • 如果 Hash(key1) 不等于 Hash(key2),那么 key1、key2 一定不相等。

  • 如果 Hash(key1) 等于 Hash(key2),那么 key1、key2 可能相等,也可能不相等(会发生哈希碰撞)。

五、哈希碰撞(哈希冲突):


哈希冲突(Hash Collision):不个同的关键字通过同一个哈希函数可能得到同一哈希地址,即 key1 ≠ key2,而 Hash(key1) = Hash(key2),这种现象称为哈希冲突。

如何解决哈希冲突:
常用的哈希冲突解决方法主要是两类:「开放地址法(Open Addressing)」 和 「链地址法(Chaining)」

开放地址法(Open Addressing)

指的是将哈希表中的「空地址」向处理冲突开放。当哈希表未满时,处理冲突时需要尝试另外的单元,直到找到空的单元为止。

按照下面方法求解:H(i) = (Hash(key) + F(i)) % m,i = 1, 2, 3, ..., n (n ≤ m - 1)。

H(i) 是在处理冲突中得到的地址序列。即在第 1 次冲突(i = 1)时经过处理得到一个新地址 H(1),如果在 H(1) 处仍然发生冲突(i = 2)时经过处理时得到另一个新地址 H(2) …… 如此下去,直到求得的 H(n) 不再发生冲突.这就是线性探测法、此外还有二次探测法、伪随机探测法。

链地址法(Chaining)

将具有相同哈希地址的元素(或记录)存储在同一个线性链表中。

我们假设哈希函数产生的哈希地址区间为 [0, m - 1],哈希表的表长为 m。则可以将哈希表定义为一个有 m 个头节点组成的链表指针数组 T。

  • 这样在插入关键字的时候,我们只需要通过哈希函数 Hash(key) 计算出对应的哈希地址 i,然后将其以链表节点的形式插入到以 T[i] 为头节点的单链表中。在链表中插入位置可以在表头或表尾,也可以在中间。如果每次插入位置为表头,则插入操作的时间复杂度为O(1)

  • 而在在查询关键字的时候,我们只需要通过哈希函数 Hash(key) 计算出对应的哈希地址 i,然后将对应位置上的链表整个扫描一遍,比较链表中每个链节点的键值与查询的键值是否一致。查询操作的时间复杂度跟链表的长度 k 成正比,也就是 O(k)。对于哈希地址比较均匀的哈希函数来说,理论上讲,k = n // m,其中 n 为关键字的个数,m 为哈希表的表长。

简单来说我们定义一个链表数字,通过哈希函数得到一个值作为地址,用这个地址当成头文件来存储各种数据,从而避免了哈希冲突。


六、总结:

  • 哈希表(Hash Table):通过键 key 和一个映射函数 Hash(key) 计算出对应的值 value,把关键码值映射到表中一个位置来访问记录,以加快查找的速度。

  • 哈希函数(Hash Function):将哈希表中元素的关键键值映射为元素存储位置的函数。

  • 哈希冲突(Hash Collision):不同的关键字通过同一个哈希函数可能得到同一哈希地址。

哈希表的两个核心问题是:「哈希函数的构建」 和 「哈希冲突的解决方法」

  • 常用的哈希函数方法有:直接定址法、除留余数法、平方取中法、基数转换法、数字分析法、折叠法、随机数法、乘积法、点积法等。

  • 常用的哈希冲突的解决方法有两种:开放地址法和链地址法。

C++ 中的哈希集合为 unordered_set,可以查找元素是否在集合中。如果需要同时存储键和值,则需要用 unordered_map,可以用来统计频率,记录内容等等。如果元素有穷,并且范围不大,那么可以用一个固定大小的数组来存储或统计元素。例如我们需要统计一个字符串中所有字母的出现次数,则可以用一个长度为 26 的数组来进行统计,其哈希函数即为字母在字母表的位置,这样空间复杂度就可以降低为常数。


七、C++中的哈希表:

 1.map

  1. 头文件:#include<map>

  1. 定义 :map<string,int> maps;

3.访问:

    ​普通访问 maps['c']=5;

    ​迭代器:

for(map<char,int>::iterator it=mp.begin();it!=mp.end();it++)
   {
       cout<<it->first<<" "<<it->second<<endl;
   }

4.常用函数:

    ​maps.insert() 插入、maps.find() 查找一个元素、maps.clear()清空、maps.erase()删除一个元素、maps.szie()长度、maps.begin()返回指向map头部的迭代器、maps.end()返回指向map末尾的迭代器、maps.rbegin()返回指向map尾部的逆向迭代器、maps.rend()返回指向map头部的逆向迭代器、maps.empty()判断其是否为空、maps.swap()交换两个map

2.unordered_map

1.头文件:#include<unordered_map>

​2.声明:unordered_map<elemType_1, elemType_2> var_name; //声明一个没有任何元素的哈希表,

//其中elemType_1和elemType_2是模板允许定义的类型,如要定义一个键值对都为Int的哈希表:

unordered_map<int, int> map;

  1. 初始化:

    unordered_map<int, int> hmap{ {1,10},{2,12},{3,13} };
    

                覆盖:

hmap[4] = 14;如果令hmap[4] = 15;则会发生覆盖

                 插入:

hmap.insert({ 5,15 }); //insert()函数在同一个key中插入两次,第二次插入会失败

                复制:

unordered_map<int, int> hmap{ {1,10},{2,12},{3,13} };
                            unordered_map<int, int> hmap1(hmap);    

4.遍历:

unordered_map<int, int> hmap{ {1,10},{2,12},{3,13} };
unordered_map<int, int>::iterator iter = hmap.begin();
for( ; iter != hmap.end(); iter++){
 cout << "key: " <<  iter->first  << "value: " <<  iter->second <<endl;
}

5.常用函数:除了:begin( )、end( )、其中 cbegin() 和 cend()是面向常数的,empty()、size()、erase()、clear()。

at()函数:查找key对应的值。

find()函数:以key作为参数寻找哈希表中的元素,如果哈希表中存在该key值则返回该位置上的迭代器,否则返回哈希表最后一个元素下一位置上的迭代器

unordered_map<int, int> hmap{ {1,10},{2,12},{3,13} };
unordered_map<int, int>::iterator iter;
iter = hmap.find(2); //返回key==2的迭代器,可以通过iter->second访问该key对应的元素
if(iter != hmap.end())  cout << iter->second;

 count()函数: 统计某个key值对应的元素个数, 因为unordered_map不允许重复元素,所以返回值为0或1。

3.set(有有序集合)

  1. 头文件#include <set>

  1. 定义:set<int> s;//底层逻辑为红黑树

  1. 基本函数有:

int x;
int* pos;
s.insert(x);//插入元素x
clear();//清除所有元素
s.erase(x);//删除元素x
erase(pos);//删除pos迭代器所指的元素,返回下一个元素的迭代器
s.size(x);//返回s的大小
s.count(x);//返回x元素出现次数
s.begin();//返回第一个元素的迭代器
s.end();//返回最后一个元素的迭代器的下一位
s.find(x);//查找x,若存在,返回该键的元素的迭代器;若不存在,返回s.end();
s.empty();//返回set是否为空

4.unordered_set此外还有无序集合

1.头文件# include<unordered_set>

2.定义unordered_set<int> s;//实现基于哈希表

3.函数基本与set相同。

八、下面是我的做题记录:

1.题目:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class MyHashSet {

    //要定义一个hash表我们得有容器和哈希函数。

    vector<list<int>> data;//定义一个hash表的容器:链表向量

    static const int base = 769;//定义一个常量,它是一个质数,也是数据容器的元素个数

    static int hash(int key){

        return key % base;

    }

public:

    MyHashSet():data(base){}//初始化数据容器,大小为769.

   

    void add(int key) {

        int n =hash(key);//得到一个值,它会作为地址(索引)放入元素

         for (auto it = data[n].begin(); it != data[n].end(); it++){//创建一个迭代器it指向从n开头的向量,遍历后面的向量

        if((*it) == key){//当it范指针指向我们得到的值的元素存在时,我们返回,此时里面有对应的元素,不做任何操作

            return;

        }}

        data[n].push_back(key);}

    void remove(int key) {

        int n = hash(key);

        for (auto it = data[n].begin(); it != data[n].end(); it++) {//创建一个迭代器it,遍历向量,找到对应的值,删除即可

            if ((*it) == key) {

                data[n].erase(it);

                return;

            }}

    }

   

    bool contains(int key) {

        int n = hash(key);

        for (auto it = data[n].begin(); it != data[n].end(); it++) {//创建一个迭代器it,遍历向量,找到对应的值,存在就返回true否则返回false;

            if ((*it) == key) {

                return true;

            }

        }

        return false;

    }

};

//思路:利用直接定址法构造hash函数,用一个链表向量存放数据,通过hash函数映射得到值,在对应的值上存入数据.注意:在add里面我们要遍历从得到的映射值开始来查找是否有相同元素,如果有就不存入数据。在remove里面我们要遍历到元素在的位置,把它删除,在contains里面要遍历它是否存在。

//核心内容:主要是定义hash函数的过程、选取对应的容器、以及用循环解决问题。

2.题目:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class MyHashMap {

private:

     vector<list<pair<int, int>>> data;//定义一个pair的链表向量

     static const int base = 769;

     static int hash(int key){//定义一个hash函数

         return key%base;

     }

public:

    MyHashMap():data(base) {}//实现类的对象,大小为769

   

    void put(int key, int value) {

        int n = hash(key);

        for(auto it = data[n].begin();it != data[n].end();it++){//查找向量里面是否有pair对应的key,如果有就覆盖value,否则新建一个pair。

            if((*it).first==key){

                (*it).second =value;

                return;

            }

        }

        data[n].push_back(make_pair(key,value));

    }

   

    int get(int key) {

        int n = hash(key);

        for(auto it = data[n].begin();it != data[n].end();it++){//用for循环查找相应的key,并返回对应的value

            if((*it).first==key){

                return (*it).second;

            }

        }

        return -1;

    }

   

    void remove(int key) {

        int n = hash(key);

        for(auto it = data[n].begin();it != data[n].end();it++){//用for循环查找相应的key,并删除

            if((*it).first==key){

                data[n].erase(it);

                return;

            }

        }

    }

};

//思路:跟上一个实现hash表一样,不过这个是实现哈希映射,因此我们要建立一个pair的链表向量,它可以存储key和value;最后我们在pue、get、remove函数通过循环实现就可以了,但是要注意在想pair的链表向量插入值的时候应该是push_back(make_pair(key,value));同时还要注意访问我们这次在向量中查找的是关键值key了,同时要覆盖value.

3.题目:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

1.排序

class Solution {

public:

    bool containsDuplicate(vector<int>& nums) {

        sort(nums.begin(),nums.end());

        for(int i=0;i<nums.size()-1;i++){

            if(nums[i]==nums[i+1]){

                return true;

            }//找到重复元素返回ture;

        }

        return false;//如果遍历一遍向量,还是没有重复元素就返回false

    }

};

//思路:通过sort函数排序整个数组,然后在用for循环遍历,在遍历的过程中如果发现当前值等于它的下一个值,就说明有重复元素,返回true,否则返回false。

2.使用集合(哈希表)

class Solution {

public:

    bool containsDuplicate(vector<int>& nums) {

        unordered_set<int> s;//我们使用无重复值的元素集合

        for (int x: nums) {

            if (s.find(x) != s.end()) {//查找集合中是否有对应元素,如果没有,那么find就会返回集合最后一个元素的下一个迭代器,即s.end()

                return true;//如果不等于s.end()就说明有重复元素

            }

            s.insert(x);//没有就插入数据

        }

        return false;

    }

};

//思路:使用没有重复值的集合当做数据容器,遍历一遍数组,并插入数组元素,如果在集合中有重复值,就返回true,否则返回false,这里要注意我们查找集合里面的元素,由于它是无序的我们可以使用find()查找,find()会返回该元素在集合的地址,如果没有find()函数就会返回最后一位元素的下一位迭代器。

4.题目:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {

public:

    bool areOccurrencesEqual(string s) {

        unordered_map<char,int> hash;

        for(char ch : s){//仅仅是查看迭代的值,不改变ch

            if(!hash.count(ch)){//如果元素不在哈希表中,那么我们就把value值变成0

                hash[ch] = 0;

            }

            ++hash[ch];//读到对应字符,该字符的value增加

        }

        //由于在hash表中,元素是不重复的,因此我们可以得到一个元素最大可以有多少个,这个值就是每个元素相同出现的次数

        int n = s.size()/hash.size();

        for(auto& x : hash){//可以修改迭代的值x

            if(x.second != n){

                return false;

            }

        }

        return true;

    }

};

//思路:我们要查看整个字符串它所有的字符是否相同,那我们可以定义一个没有重复值的映射,我们每当遍历到字符串的一个字符,先看看它是否存在,如果不存在我们给它赋初值为0,并+1,后面遇到就加1.这样我们就得到一个记录字符串的每个字符出现次数的哈希表了,最后我们遍历整个哈希映射,查看它们是否元素出现次数相同,元素次数=总字符数/哈希映射key的个数

5.题目:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {

public:

    bool canConstruct(string ransomNote, string magazine) {

        vector<int> hash(26);//最后一个元素是z,它的ascall码对应的是122,a对应的是97,由于122-97 = 25;因此数组最后一个索引至少为25,即数组的元素要大于等于26

        if(ransomNote.size()>magazine.size()){

            return false;

        }//可加可不加,加了提高效率

        for(auto& x : magazine){//遍历字符串,把字符串的对应的字符作为索引,记录字符个数

            hash[x-'a']++;//相同字符减去'a'得到的值相同

        }

        for(auto& x : ransomNote){

            if(--hash[x-'a'] < 0){//如有一个元素不存在,那么它减去1,就变成负数了,则说明在magazine中不存在足够的那个元素,返回false,--hash[x-'a']为先减后得返回值

                return false;

            }

        }

        return true;

    }

};

//判断 ransomNote 能不能由 magazine 里面的字符构成。即查找一个字符串的元素在另外一个字符串是否全部存在,我们只需要用一个数组记录每个字符出现的次数,通过ascall码来计算索引,因此数组最小为26个空间,最后我们遍历另外一个字符串,如果有没有出现的元素,那么数组对应的0就会变成负数,则返回false;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

years_GG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值