Leetcode练习题:哈希表
哈希表在之前的专题里面也是常常用到,是根据关键字的值直接进行访问,十分快速。虽然在学习数据结构的时候,哈希表的内容还是学的比较多的,包括各种解决地址冲突的方法,但都没有编码实现过,接下来的题目也是直接stl中的map来实现的,还是学习一下map在题目中的一些常用方法和套路。
30:串联所有单词的子串
问题描述
给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。
注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。
示例 1:
输入:
s = “barfoothefoobarman”,
words = [“foo”,“bar”]
输出:0 9
解释:
从索引 0 和 9 开始的子串分别是 “barfoo” 和 “foobar” 。输出时,按照索引由小到大顺序输出。
示例 2:
输入:
s = “wordgoodgoodgoodbestword”,
words = [“word”,“good”,“best”,“word”]
输出:-1
s中的子串无法由words串联得到,所以输出-1
解题思路
每个单词都要求出现,但不考虑顺序,因此可以建立一个哈希表,来记录每一个单词需要出现的次数。
从s的下标0来时向后扫描,因为单词长度是相同的,每一次切割下该长度的单词与所需哈希表比较,存在且map[string[>0,则将map[string]-1并继续扫描,如果都符合要求了,则记录该下标,这里使用count++来记录,不需要判断哈希表中每个字符的值是否都为0
代码实现
vector<int> findSubstring(string s, vector<string>& words) {
if (s == "" || words.size() == 0)
{
return {};
}
unordered_map<string, int> need;
vector<int>ans;
int word_len = words[0].size();
int len = s.length();
//记录所需字符以及出现次数
for (auto& s: words)
{
need[s]++;
}
unordered_map<string,int> now;
now=need;
string temp = "";
for (int i = 0; i < len; i ++)
{
//如果从该下标开始的后续长度已经不满足了话 直接退出
if (len - i + 1 < word_len* words.size())
{
break;
}
//每次都重置now哈希表
now=need;
temp= s.substr(i, word_len);
int j = i, count = 0;
while (now[temp] > 0)
{
now[temp]--;
count ++;
j += word_len;
temp = s.substr(j, word_len);
}
//利用count来实现所有字符串都完成
if (count == words.size()) {
ans.push_back(i);
}
}
return ans;
}
反思与收获
每个元素都需要出现,且不考虑出场顺序,这个时候利用哈希表十分方便,巧妙利用总数计数,免去了判断哈希表中每个值是否都为空的步骤
36:有效的数独
问题描述
判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
上图是一个部分填充的有效的数独。
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。
示例 1:
输入:
53…7…
6…195…
.98…6.
8…6…3
4…8.3…1
7…2…6
.6…28.
…419…5
…8…79
输出: true
示例 2:
输入:
83…7…
6…195…
.98…6.
8…6…3
4…8.3…1
7…2…6
.6…28.
…419…5
…8…79
输出: false
解释: 除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。
但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。
说明:
一个有效的数独(部分已被填充)不一定是可解的。
只需要根据以上规则,验证已经填入的数字是否有效即可。
给定数独序列只包含数字 1-9 和字符 ‘.’ 。
给定数独永远是 9x9 形式的。
解题思路
每行设置一个哈希表,记录每个数字是否出现过
每列设置一个哈希表,记录每个数字是否出现过
每个大格设置一个哈希表,记录每个数字是否出现过
但由于只有9个元素,因此无需用到哈希表,直接数组解决了。
计算所在大格的标号为,i/3*3+j/3
代码实现
bool isValidSudoku(vector<vector<char>>& board) {
int row[9][9];
int column[9][9];
int grid[9][9];
for(int i=0;i<9;i++)
{
for(int j=0;j<9;j++)
{
row[i][j]=0;
column[i][j]=0;
grid[i][j]=0;
}
}
int val,num;
for(int i=0;i<9;i++)
{
for(int j=0;j<9;j++)
{
if(board[i][j]!='.')
{
val=board[i][j]-'0'-1;
num=i/3*3+j/3;
if(row[i][val])
{
return false;
}else
{
row[i][val]=1;
}
if(column[j][val])
{
return false;
}else
{
column[j][val]=1;
}
if(grid[num][val])
{
return false;
}else
{
grid[num][val]=1;
}
}
}
}
return true;
}
反思与收获
元素十分少的情况下,其实也可以直接用数组来实现哈希表的作用。
438:找到字符串中所有字母异位词
问题描述
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
说明:
字母异位词指字母相同,但排列不同的字符串。
示例 1:
输入:
s: “cbaebabacd” p: “abc”
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的字母异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的字母异位词。
示例 2:
输入:
s: “abab” p: “ab”
输出:
[0, 1, 2]
解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的字母异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的字母异位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的字母异位词。
解题思路
一次扫描遍历,有点像滑动窗口的感觉,控制窗口的左右边界。
首先利用哈希表来记录 所需字符以及对应出现的次数。
不断向右遍历,如果map[s[right]]>0的话,则说明该字符还是所需的,need++(这里need与上题count是相同作用)
当need的大小等于字符串p的大小时,说明每一个所需元素都扫描到了
再判断这一段扫描区间,因为left那可能含有多余的元素,如果map[s[left]]==0,则说明这个元素是所需,现在left往前走的话,need要减1了,因为如果不是所需的话,map[s[left]]应该是<0的
代码实现
vector<int> findAnagrams(string s, string p) {
vector<int> res;
int m[128] = {0};
int left = 0, right = 0, need = 0;
for(char c : p)
++m[c];
while(right < s.size())
{
if(m[s[right]] > 0) ++need;
--m[s[right]];
++right;
while(need == p.size())
{
if(right - left == p.size()) res.push_back(left); //通过长度判断异位词,加入res中
if(m[s[left]] == 0) --need;
++m[s[left]];
++left;
}
}
return res;
}
反思与收获
滑动窗口结合哈希表,要想清楚left和right是在框柱考虑的字符区间,但是need才是真正在记录是否哈希表中每个元素都到位了的情况。
451:根据字符出现频率排序
问题描述
给定一个字符串,请将字符串里的字符按照出现的频率降序排列,如果频率相同,则按照字符的ASCII码升序排列。
示例 1:
输入:
“tree”
输出:
“eert”
解释:
'e’出现两次,'r’和’t’都只出现一次。
因此’e’必须出现在’r’和’t’之前,而且’r’比’t’的ASCII码小。
示例 2:
输入:
“cccaaa”
输出:
“aaaccc”
解释:
'c’和’a’都出现三次。因此按照字符升序排列,'a’在’c’前。
示例 3:
输入:
“Aabb”
输出:
“bbAa”
解释:
'A’和’a’被认为是两种不同的字符,并且’A’的ASCII码比’a’小
解题思路
建立哈希表,记录每个字符出现的次数,然后再根据次数进行排序,但是map没有排序这个操作,所以这边使用pair来实现对应的关系,vector来存储所有的pair,重写cmp函数
代码实现
static bool cmp(pair<char,int> p1,pair<char,int>p2)
{
return p1.second==p2.second ? p1.first<p2.first:p1.second>p2.second;
}
string frequencySort(string s) {
int count[123]={0};
for(int i=0;i<s.length();i++)
{
count[s[i]-'A'+65]++;
}
vector<pair<char,int>> m;
for(int i=0;i<123;i++)
{
if(count[i]!=0)
{
//cout<<char(i)<<" "<<count[i]<<endl;
m.push_back({(char)i,count[i]});
}
}
sort(m.begin(),m.end(),cmp);
string ans;
for(auto v:m)
{
ans+=string(v.second,v.first);
}
return ans;
}
反思与收获
想要对map里面继续排序,后面似乎有道题目会做到的692,但是利用vector<pair<string,int>>这样也能实现,适用于情况比较简单的场景
525:
问题描述
给定一个二进制数组, 找到含有相同数量的 0 和 1 的最长连续子数组(的长度)。
示例 1:
输入: [0,1]
输出: 2
说明: [0, 1] 是具有相同数量0和1的最长连续子数组。
示例 2:
输入: [0,1,0]
输出: 2
说明: [0, 1] (或 [1, 0]) 是具有相同数量0和1的最长连续子数组。
注意: 给定的二进制数组的长度不会超过50000。
解题思路
一开始想,搞个map记录0,1的数量,再判断数量是否相同,但这样找不到最长连续子数组的长度,因此下标我们肯定是要记录的,才能记录长度。
这里很巧妙的将,0看成-1,1看成+1,这样一来如果某个区间段和=0的话,则说明该区间0,1个数是相同。
既然如此,可以再想到类似前缀和的概念,一次遍历完成,利用哈希表记录每种前缀和出现的情况,如果之前就存在过,则说明这两段之间和为0,并且哈希表只要记录每种前缀和出现的第一次下标即可,才能计算最长长度
代码实现
int findMaxLength(vector<int>& nums) {
unordered_map<int,int> m;
m[0]=-1;
int ans=0,count=0;
for(int i=0;i<nums.size();i++)
{
count+=(nums[i]==1)?1:-1;
if(m.find(count)!=m.end())
{
ans=max(ans,i-m[count]);
}else
{
m[count]=i;
}
}
return ans;
}
反思与收获
利用哈希表记录前缀和,将0,1情况转换为-1和+1,这两种处理方法十分巧妙。
54:砖墙
问题描述
你的面前有一堵矩形的、由多行砖块组成的砖墙。 这些砖块高度相同但是宽度不同。你现在要画一条自顶向下的、穿过最少砖块的垂线。
砖墙由行的列表表示。 每一行都是一个代表从左至右每块砖的宽度的整数列表。
如果你画的线只是从砖块的边缘经过,就不算穿过这块砖。你需要找出怎样画才能使这条线穿过的砖块数量最少,并且返回穿过的砖块数量。
你不能沿着墙的两个垂直边缘之一画线,这样显然是没有穿过一块砖的。
示例:
输入:
[[1,2,2,1], [3,1,2], [1,3,2], [2,4], [3,1,2], [1,3,1,1]]
输出: 2
解释:
砖墙如下:
解题思路
这题跟以前接触的区间题目还挺不一样,计算穿过这些区间,但是边缘不算。
如果直接去考虑穿过区间的线,是很难的因为有太多太多取值了,因此我们反着思考,来考虑恰好穿过边缘的线。
利用哈希表,将每一个右边缘长度出现的情况统计出来,原墙行数-出现情况最多次则为答案了。
代码实现
int leastBricks(vector<vector<int>>& wall) {
unordered_map<int,int> m;
int temp;
for(int i=0;i<wall.size();i++)
{
temp=0;
for(int j=0;j<wall[i].size()-1;j++)
{
temp+=wall[i][j];
if(m[temp])
{
m[temp]++;
}
else
{
m[temp]=1;
}
}
}
int ans=0;
for(auto i=m.begin();i!=m.end();i++)
{
ans=max(ans,i->second);
}
return wall.size()-ans;
}
反思与收获
这题目还巧妙的,主要是利用哈希表,我们将什么设置为关键字值呢,什么是重要的需要记录并且有对应值的,在解决题目的时候要思考这些问题
560:和为K的子数组
问题描述
给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
解题思路
经典前缀和和哈希表结合的题目。
用哈希表记录出现的前缀和情况,
后-前=K转换为前=后-K,如果哈希表中有后-K的元素,则将其出现的次数加到ans中
代码实现
int subarraySum(vector<int>& nums, int k) {
unordered_map<int,int> m;
m[0]=1;
int ans=0,pre=0;
for(int i=0;i<nums.size();i++)
{
pre+=nums[i];
if(m.find(pre-k)!=m.end()) {ans+=m[pre-k];}
m[pre]++;
}
return ans;
}
反思与收获
前缀和与哈希表结合,还有前缀和的经典计算公式转换,真的很常用,要牢记,熟练掌握。
594:最长和谐子序列
问题描述
和谐数组是指一个数组(长度>=2)里元素的最大值和最小值之间的差别正好是1。
现在,给定一个整数数组,你需要在所有可能的子序列中找到最长的和谐子序列的长度。
如果找不到这样的数组,则输出0。
示例 :
输入: [1,3,2,2,5,2,3,7]
输出: 5
原因: 最长的和谐数组是:[3,2,2,2,3].
说明: 输入的数组长度最大不超过20,000.
解题思路
显然我们需要一个哈希表来记录每一个数出现的次数,那和谐数组是什么意思呢,注意这个子序列不要求连续,和谐数组,就是要么考虑当前元素和当前元素-1的元素个数和,要么考虑当前元素和当前元素+1的元素个数和。
代码实现
int findLHS(vector<int>& nums) {
map <int,int> m;
int ans=0;
for(int i=0;i<nums.size();i++)
{
m[nums[i]]++;
//如果前后元素都为0的话,就直接
if(m[nums[i]-1]==0&&m[nums[i]+1]==0)
{
continue;
}
ans=max(ans,m[nums[i]]+max(m[nums[i]+1],m[nums[i]-1]));
}
return ans;
}
反思与收获
这道题用哈希表解一下子就变得很简单了,了解和谐数列,最大于最小相差1,其实简单来理解,就是要么前面要么后面。
692:前K个高频单词
问题描述
给一非空的单词列表,返回前 k 个出现次数最多的单词。
返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率,按字母顺序排序。
示例 1:
输入: [“i”, “love”, “leetcode”, “i”, “love”, “coding”], k = 2
输出: [“i”, “love”]
解析: “i” 和 “love” 为出现次数最多的两个单词,均为2次。
注意,按字母顺序 “i” 在 “love” 之前。
示例 2:
输入: [“the”, “day”, “is”, “sunny”, “the”, “the”, “the”, “sunny”, “is”,
“is”], k = 4输出: [“the”, “is”, “sunny”, “day”]
解析: “the”, “is”, “sunny” 和 “day” 是出现次数最多的四个单词,
出现次数依次为 4, 3, 2 和 1 次。
解题思路
这题哈希表来记录每个单词出现的次数,然后根据次数排序,可以利用451的方式来实现,这边是直接对map进行排序顺序更改,注意cmp函数的写法
代码实现
vector<string> topKFrequent(vector<string>& words, int k) {
unordered_map<string,int> m;
for(int i=0;i<words.size();i++)
{
m[words[i]]++;
}
sort(words.begin(),words.end(),[&](const string &s1,const string &s2){
return (m[s1]>m[s2])||(m[s1]==m[s2]&&s1<s2);
});
unique(words.begin(),words.end());
return vector<string>(words.begin(),words.begin()+k);
}
反思与收获
直接在map中根据关键字的值进行排序,注意cmp函数重写,要添加[&]符号,const 参数
**sort(words.begin(),words.end(),[&](const string &s1,const string &s2){
return (m[s1]>m[s2])||(m[s1]==m[s2]&&s1<s2);}
);**
781:森林中的兔子
问题描述
森林中,每个兔子都有颜色。其中一些兔子(可能是全部)告诉你还有多少其他的兔子和自己有相同的颜色。我们将这些回答放在 answers 数组里。
返回森林中兔子的最少数量。
示例:
输入: answers = [1, 1, 2]
输出: 5
解释:
两只回答了 “1” 的兔子可能有相同的颜色,设为红色。
之后回答了 “2” 的兔子不会是红色,否则他们的回答会相互矛盾。
设回答了 “2” 的兔子为蓝色。
此外,森林中还应有另外 2 只蓝色兔子的回答没有包含在数组中。
因此森林中兔子的最少数量是 5: 3 只回答的和 2 只没有回答的。
示例:
输入: answers = [10, 10, 10]
输出: 11
示例:
输入: answers = []
输出: 0
解题思路
一开始给人感觉竟然有点像逻辑题似的,但确实里面包含了一个隐藏信息那就是,只要是颜色相同的兔子它们报出来的数字都是一样的。
因此我们用哈希表来记录,同一种颜色报数过了的兔子个数。
之后我们才来分析,每一个颜色中,报数兔子个数,报的数,和该颜色兔子总数之间的关系。
注意:报出来数字相同的兔子其实也不一定颜色是相同的 比如 3 3 3 3 3,显然这里不止一种颜色的,那就再计算当中实现。
报的数3,则说明该颜色的兔子实际用3+它本身1=4只
现在有map[3]=5只兔子喊出了这样的话,celi[5/4]说明其实有两种颜色,两组这样的情况,则实际有4*2=8只
代码实现
int numRabbits(vector<int>& answers) {
unordered_map<int,int> colors;
int ans=0;
for(int i=0;i<answers.size();i++)
{
colors[answers[i]]++;
}
for(auto i=colors.begin();i!=colors.end();i++)
{
int all=i->second;
int mul=i->first+1;
int time=ceil(all*1.0/mul);
ans+=time*mul;
}
return ans;
}
反思与收获
要挖掘题目隐藏的信息,和包含的逻辑关系,这个兔子题目还挺有趣的,利用哈希表来记录喊出相同数字的兔子数,再分析计算出总数。
——————————————————————————————————
哈希表真的运用很广泛,无论是专门的哈希表的题目还是其他的题目,在解决一对一映射问题,关键字记录的时候要考虑到哈希。还有简单情况时用数组来代替哈希,哈希的排序两种方法的解决,哈希结合前缀和,哈希结合滑动窗口的方法等。ヾ(๑╹◡╹)ノ"