算法学习——哈希表

哈希表英文名称为Hash Table,也叫散列表。
哈希表是根据关键码的值而直接进行访问的数据结构。
简而言之,哈希表就是数组,关键码就是数组的索引下标,通过下标来访问数组中的元素。

哈希表用来解决什么问题呢?
一般用来快速判断一个元素是否出现在集合里。例如查询一个名字是否在一个学校里。我们初始化时候把这所学校里所有学生的名字都存在哈希表里,在查询的时候通过索引就可以知道一个学生是否在这个学校里。
将学生姓名映射到哈希表上涉及到了哈希函数

哈希函数

以上述例子为例,哈希函数所做的就是将学生姓名直接映射为哈希表上的索引,我们可以通过查找索引来找到对应的学生。
在这里插入图片描述
为了保证映射出来的索引数值都落在哈希表上,我们会对数值做一个取模的操作,保证了学生姓名一定可以映射到哈希表上了。
如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置,接下来哈希碰撞登场。

哈希碰撞

在这里插入图片描述
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。

拉链法

刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了。
在这里插入图片描述其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。

线性探测法

使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:
在这里插入图片描述

哈希表的数据结构

当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。

  • 数组
  • set (集合)
  • map(映射)

在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:
在这里插入图片描述
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
在这里插入图片描述
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
再来看一下map ,map 是一个<key,value> 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。

虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,std::set、std::multiset 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。

总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!

有效的字母异位词

力扣题目链接

给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
在这里插入图片描述
这道题思路如下:
这个场景中,我们需要判断的是两个字符串中字符出现的次数,并且判断是否相同。因此,我们可以将字符映射到哈希表中,通过哈希表中的次数统计来比较两个字符串是否是字母异位词。
首先,英文字母一共有26个,因此我们可以创建一个int类型的数组,包含26个元素,从0-25对应A-Z,数组中存放的数据对应该英文字母的个数。
接着,我们可以遍历第一个字符串s,将每一个字符放入相对应的整型数组中,也就是说数组中的元素个数反映了该字符串中相应英文字母出现的次数。
遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。
如何检查字符串t中是否出现了这些字符?同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。
最后检查数组, 如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。
完整代码如下:
在这里插入图片描述
在这里插入图片描述
第二次的做法:
在这里插入图片描述
在这里插入图片描述

两个数组的交集

力扣题目链接

给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
在这里插入图片描述
这道题目,主要要学会使用一种哈希数据结构:unordered_set,这个数据结构可以解决很多类似的问题。
注意题目特意说明:输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序。
思路如下:
在这里插入图片描述
我们先将nums1的数据存入unordered_set中,然后查找nums2中的元素是否在这里,如果在的话就插入result中。
在这里插入图片描述
在这里插入图片描述
注意,C++11中引入了基于范围的for循环。
语法:

for (迭代的变量 : 迭代的范围)
{
        // 循环体。
}
for(int i:vec)
{
  cout << i << endl;
}

它的原理就是从vec中取到一个内容赋值给变量i;然后在循环体中操作i。

迭代的范围不仅仅可以使用容器,也可以使用数组或者初始化列表,如下:
在这里插入图片描述
在这里插入图片描述
最后还有一点:
如果容器中的元素是结构体和类,迭代器变量应该申明为引用,加const约束表示只读。
例如

快乐数

力扣题目链接

编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
在这里插入图片描述
我的思路如下:
首先我们需要知道,怎样求一个数的各个位上的数的平方和,我们可以用取余的办法获取个位数,然后再除以10,之后再取余获取十位上的数,以此类推。
至于如何判断一个数是否是快乐数?我们需要用一个容器存储该数每次循环时的结果,并且在每次循环开始时遍历该容器查看是否出现过该结果?如果出现过说明会无限循环下去,不是快乐数。反之,继续循环,直到结果等于1。

class Solution {
public:
    int getnum(int num)
    {
        int sum = 0;
        while(num)
        {
            int a = num%10;
            sum+= a * a;
            num /=10;
        }
        return sum;
    }
    bool isHappy(int n) 
    {
        set<int>result;
        while(1)
        {
            int sum = getnum(n);
            if(result.find(sum) != result.end())
            {
                return false;
            }
            else
            {
                if(sum==1)
                {
                    return true;
                }
                else
                {
                    result.insert(sum);
                    n = sum;
                }
            }
        }
    }
};

注意:在将sum插入到result中后,一定要更新n的值为sum。
在这里插入图片描述

两数之和

力扣题目链接

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
在这里插入图片描述

什么时候使用哈希法? 当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。这道题可以理解为,我们查找能够与当前遍历的元素加起来为target的元素,也就是查找一个元素是否在集合里。
我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 <key ,value>结构来存放,key来存元素,value来存下标,那么使用map正合适。

这道题思路如下:
首先我们创建一个存放已遍历数据的map容器,接着遍历数组元素,查找target-当前元素的值是否存在于map容器中,如果存在,返回i和map容器中的value。注意map容器的查找是匹配index的,所以我们创建的时候<ind ex,value>分别为数组元素、下标。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

四数相加II

力扣题目链接

给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
在这里插入图片描述
这道题的思路如下:
为什么这道题可以用哈希表的方法来做?因为这道题还是可以转化为查找某个值是否在map容器中存储。
首先我们需要创建一个map容器,然后存储数组1和数组2之和以及该和出现的次数。
接着,设置一个count变量为0。然后我们遍历数组3和数组4,假设遍历数据为i和j,我们查找0-(i+j)是否在map中出现,如果出现那么count就等于加上该值对应的value(也就是该和出现的次数),最后返回count。
在这里插入图片描述
为什么map容器可以写成map[i+j]++来统计和以及出现次数?
在使用Map中我们可以使用map[key]来访问相应的key值,若不存在该key值,则会自动创建该key值,并且value赋值为0。
若使用map[key]++的方式则是创建key值(从1开始)或者在原存在的key上加1。
例如:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

赎金信

力扣题目链接

给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
在这里插入图片描述
我的做法如下所示:
我们可以用空间换时间的方法来解这道题。
哈希解法:
因为题目里只有小写字母,我们采用空间换取时间的哈希策略,用一个长度为26的数组来记录magazine里字母出现的次数。
然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。
在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效。
代码如下:
在这里插入图片描述
注意:最后的判断如果小于零说明ransomNote里出现的字符,magazine没有。

三数之和

力扣题目链接

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
在这里插入图片描述

“三数之和”问题的核心思路是固定一个数,然后使用两个指针(一个从左开始,另一个从右开始)在剩余的数组中寻找另外两个数,使得这三个数的和等于零。在实现过程中需要注意避免重复的三元组。
解决步骤如下:

  1. 排序:首先对数组进行排序。排序有助于后续跳过重复元素,并且使得双指针方法成为可能。
  2. 遍历数组:遍历排序后的数组,对于每一个元素nums[i],我们将在它后面的部分寻找两个数,使得这三个数的和为零。
  3. 双指针寻找:对于每个固定的nums[i],设置两个指针:left = i + 1 和 right = nums.size() -1。移动这两个指针,寻找符合条件的两个数。如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
  4. 跳过重复元素:在每次找到一个有效的三元组后,跳过所有重复的元素。
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums)
    {
        sort(nums.begin(), nums.end());
        vector<vector<int>>result;
        for (int i = 0; i < nums.size() - 2; i++)
        {
            int left = i + 1;
            int right = nums.size() - 1;
            if (nums[i] > 0)return result; //剪枝
            if (i > 0 && nums[i] == nums[i - 1])continue; //第一个元素去重
            while (left < right)
            {
                if (nums[i] + nums[left] + nums[right] < 0)
                {
                    left++;
                }
                else if (nums[i] + nums[left] + nums[right] == 0)
                {
                    result.push_back(vector<int>{nums[i], nums[left], nums[right]});
                    //后两个元素去重
                    while (left < right && nums[left] == nums[left + 1])
                    {
                        left++;
                    }
                    while (left < right && nums[right] == nums[right - 1])
                    {
                        right--;
                    }
                    left++;
                    right--;
                }
                else
                {
                    right--;
                }
            }

        }
        return result;
    }
};

四数之和

力扣题目链接

题意:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

注意:
答案中不可以包含重复的四元组。

示例: 给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 满足要求的四元组集合为: [ [-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2] ]

在这里插入图片描述
这道题和上一题是一个套路,就是多套一层for循环。但是有几个地方需要注意:

  1. 不要判断nums[k] > target 就返回了,比如target是-10,nums[0]是-4,但是nums[1]、nums[2]、nums[3]是-3,-2,-1。
  2. 四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况。
  3. 二级循环的去重。 if(j> i+1 && nums[j]==nums[j-1])continue;
  4. nums[k] + nums[i] + nums[left] + nums[right] > target 会溢出。
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) 
    {
        vector<vector<int>> result;
        if(nums.size()<4)return result;
        sort(nums.begin(),nums.end());
        for(int i = 0 ; i < nums.size()-3;i++)
        {
            // 剪枝处理
            if (nums[i] > target && nums[i] >= 0) 
            {
            	break; // 这里使用break,统一通过最后的return返回
            }
            if(i > 0 &&nums[i]==nums[i-1]) continue;
            for(int j = i+1 ; j < nums.size()-2;j++)
            {
                // 2级剪枝处理
                if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0) {
                    break;
                }
                if(j> i+1 && nums[j]==nums[j-1])continue;
                int left = j+1;
                int right = nums.size()-1;
                while(left < right)
                {
                    long long sum = (long)nums[i] + nums[j] + nums[left] + nums[right];
                    if(sum==target)
                    {
                        result.push_back(vector<int>{nums[i],nums[j],nums[left],nums[right]});
                        while(left<right && nums[left]==nums[left+1])left++;
                        while(left<right && nums[right] == nums[right-1])right--;
                        left++;
                        right--;
                    }
                    else if(sum > target) right--;
                    else left++;
                }
            } 
        }
        return result;
    }
};

总结

一般来说哈希表都是用来快速判断一个元素是否出现集合里。

对于哈希表,要知道哈希函数和哈希碰撞在哈希表中的作用。
哈希函数是把传入的key映射到符号表的索引上。
哈希碰撞处理有多个key映射到相同索引上时的情景,处理碰撞的普遍方式是拉链法和线性探测法。

接下来是常见的三种哈希结构:
数组
set(集合)
map(映射)
在C++语言中,set 和 map 都分别提供了三种数据结构,每种数据结构的底层实现和用途都有所不同。

  • 18
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值