力扣(leetcode)题目总结——哈希表篇

leetcode 经典题分类

  • 链表
  • 数组
  • 字符串
  • 哈希表
  • 二分法
  • 双指针
  • 滑动窗口
  • 递归/回溯
  • 动态规划
  • 二叉树
  • 辅助栈

本系列专栏:点击进入 leetcode题目分类 关注走一波


前言:本系列文章初衷是为了按类别整理出力扣(leetcode)最经典题目,一共100多道题,每道题都给出了非常详细的解题思路算法步骤代码实现。很多同学刚开始刷题都是按照力扣顺序刷题,其实这样对新手不太适用,刷题效果也很不好。因为力扣题目顺序是随机的,并没有按照算法分类,导致同一类型的算法强化训练不够,最后刷完也是迷迷糊糊的。所以本系列文章就是来帮你完成算法分类,针对每种算法做强化训练,保证让你以后遇到题目直接秒杀!

文章目录

  • leetcode 经典题分类
    • 两数之和
    • 整数转罗马数字
    • 罗马数字转整数
    • 串联所有单词的子串
    • 有效的数独
    • 解数独
    • 缺失的第一个正数
    • 字母异位词分组
    • 最小覆盖子串


两数之和

【题目描述】

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target的那两个整数,并返回它们的数组下标。

【输入输出实例】

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]

示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]

【算法思路】

只需进行一次for循环,在每一次循环过程中,在哈希表(map容器,以对组的形式存放数据)中查找是否存在key == target - nums[i]的值(哈希表的查找效率高)。

如果查找到,则返回{ value, i };如果未查找到,将nums[i]作为key值,下标i作为value值存放到哈希表中,继续下一次查找。

【算法描述】

vector<int> twoSum(vector<int>& nums, int target)  //用哈希表查找 
{
	unordered_map<int,int> hashtable;  //创建哈希表(map容器)
    for(int i = 0;i < nums.size(); i++)
    {
		// auto为unordered_map<int,int>::iterator
		//查找哈希表中 key == target-nums[i]的元素。若查找到返回该元素的迭代器,未查找返回.end()
		auto it = hashtable.find(target - nums[i]);      
		if(it != hashtable.end())
		{
			return {it->second, i};  //查找成功
        }
        hashtable[nums[i]] = i;  //更新哈希表中数据,key和value互换
    }
    return {-1, -1};  //查找失败
}

整数转罗马数字

【题目描述】

给你一个整数,将其转为罗马数字。罗马数字包含以下七种字符:I(1),V(5),X(10),L(50),C(100),D(500)和M(1000)。

例如,罗马数字2写做II,即为两个并列的1。12写做 XII,即为X+II。 27 写做 XXVII, 即为 XX + V + II。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如4不写做IIII,而是IV。数字1在数字5的左边,所表示的数等于大数5减小数1得到的数值4。同样地,数字9表示为IX。这个特殊的规则只适用于以下六种情况:

  • I可以放在V(5)和X(10)的左边,来表示4和9。
  • X可以放在L(50)和C (100)的左边,来表示40和90。
  • C可以放在D(500)和M(1000)的左边,来表示 400和900。

【输入输出实例】

示例 1:

输入:num = 3
输出:“III”

示例 2:

输入:num = 4
输出:“IV”

示例 3:

输入:num = 9
输出:“IX”

示例 4:

输入:num = 58
输出:“LVIII” (L = 50, V = 5, III = 3)

示例 5:

输入:num = 1994
输出:“MCMXCIV” (M = 1000, CM = 900, XC = 90, IV = 4)

【算法思路】

总共13个独特的符号:

在这里插入图片描述

确定罗马数字的规则是:对于罗马数字从左到右的每一位,选择尽可能大的符号值。

根据罗马数字的唯一表示法,为了表示一个给定的整数num,我们寻找不超过num的最大符号值,将num减去该符号值,然后继续寻找不超过num的最大符号值,将该符号拼接在上一个找到的符号之后,循环直至num为0。最后得到的字符串即为num的罗马数字表示。

编程时,可以建立一个数值-符号对(一一对应)的列表,按数值从小到大排列。从列表尾部开始往前遍历每个数值-符号对,若当前列表数值不超过num,则从num中不断减去该数值,直至num小于当前值时,然后遍历下一个次小的数值-符号对。若遍历中num为0则跳出循环。

【算法描述】

string intToRoman(int num) 
{
    const pair<int,string> list[13] =   //建立 数值-符号 映射的列表
    {
        {1, "I"},
        {4, "IV"},
        {5, "V"},
        {9, "IX"},
        {10, "X"},
        {40, "XL"},
        {50, "L"},
        {90, "XC"},
        {100, "C"},
        {400, "CD"},
        {500, "D"},
        {900, "CM"},
        {1000, "M"},
    };
    string str;    //存放要输出的罗马数字
    int i = 12;    //表示映射列表的下标
    //不断寻找不超过num的最大符号值,找到后减去,再继续寻找
    while(num) 
    {
        if(num >= list[i].first)  //找到不超过num的最大符号值
        {
            num -= list[i].first;    //减去
            str += list[i].second;   //保存对应的罗马字符
        }
        else
        {
            i--;   //继续向下寻找
        }
    }
    return str;
}

罗马数字转整数

【题目描述】

给你一个罗马数字,将其转为整数。罗马数字包含以下七种字符:I(1),V(5),X(10),L(50),C(100),D(500)和M(1000)。

例如,罗马数字2写做II,即为两个并列的1。12写做 XII,即为X+II。 27 写做 XXVII, 即为 XX + V + II。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如4不写做IIII,而是IV。数字1在数字5的左边,所表示的数等于大数5减小数1得到的数值4。同样地,数字9表示为IX。这个特殊的规则只适用于以下六种情况:

  • I可以放在V(5)和X(10)的左边,来表示4和9。
  • X可以放在L(50)和C (100)的左边,来表示40和90。
  • C可以放在D(500)和M(1000)的左边,来表示 400和900。

【输入输出实例】

示例 1:

输入: s = “III”
输出: 3

示例 2:

输入: s = “IV”
输出: 4

示例 3:

输入: s = “IX”
输出: 9

示例 4:

输入: s = “LVIII”
输出: 58 (L = 50, V= 5, III = 3)

示例 5:

输入: s = “MCMXCIV”
输出: 1994 (M = 1000, CM = 900, XC = 90, IV = 4)

【算法思路】

按照题目的描述,可以总结如下规则:

  • 当小值在大值的左边,则减小值,如 IV = 5 - 1 = 4;

  • 当小值在大值的右边,则加小值,如 VI = 5 + 1 =6;

  • 由上可知,右值永远为正,因此最后一位必然为正。

总之,把一个小值放在大值的左边,就是做减法,否则为加法。在代码实现上,可以往后看多一位,对比当前位与后一位的大小关系,从而确定当前位是加还是减法。当没有下一位时,做加法即可。

如下图举例所示:

在这里插入图片描述

【算法描述】

int romanToInt(string s) 
{
    int sum = 0;    //记录最后整数结果
    int prenum = getnum(s[0]);  //前一个罗马字符表示的数字
    for(int i = 1; i < s.size(); i++)  //遍历罗马字符串s
    {
        int num = getnum(s[i]);  //当前罗马字符表示的数字
        //通过比较当前罗马数字和前一个罗马数字的大小来判断是加prenum还是减prenum
        if(num > prenum)
        {
            sum -= prenum;
        }
        else
        {
            sum += prenum;
        } 
        prenum = num;    //更新prenum
    }
    sum += prenum;   //右值永远为正,因此最后一位必然为正,直接加
    return sum;
}
//实现 罗马字符-数字 的映射函数
int getnum(char ch)
{
    switch(ch)
    {
        case 'I':
            return 1;
        case 'V':
            return 5;
        case 'X':
            return 10;
        case 'L':
            return 50;
        case 'C':
            return 100;
        case 'D':
            return 500;
        case 'M':
            return 1000;
        default:
            return 0;
    }
}

串联所有单词的子串

【题目描述】

给定一个字符串s和一个字符串数组words。words中所有字符串长度相同。 s 中的串联子串是指一个包含words中所有字符串以任意顺序排列连接起来的子串。

例如,如果 words = [“ab”,“cd”,“ef”], 那么 “abcdef”, “abefcd”,“cdabef”, “cdefab”,“efabcd”, 和 “efcdab” 都是串联子串。 “acdbef” 不是串联子串,因为他不是任何 words 排列的连接。

返回所有串联字串在 s 中的开始索引。可以以任意顺序返回答案。

【输入输出实例】

示例 1:

输入:s = “barfoothefoobarman”, words = [“foo”,“bar”]
输出:[0,9]
解释:子串"barfoo"开始位置是0。它是words中以 [“bar”,“foo”] 顺序排列的连接。
子串"foobar"开始位置是9。它是words中以 [“foo”,“bar”] 顺序排列的连接。

示例 2:

输入:s = “wordgoodgoodgoodbestword”, words = [“word”,“good”,“best”,“word”]
输出:[]

示例 3:

输入:s = “barfoofoobarthefoobarman”, words = [“bar”,“foo”,“the”]
输出:[6,9,12]

【算法思路】

因为单词长度是固定的,我们可以计算出截取字符串的单词个数是否和words里相等,所以我们可以借用哈希表。

一个哈希表是word,用来存放words中的单词及出现的次数;一个哈希表hash是截取的字符串,即从 s 中不断划分单词,存放匹配成功的单词及出现的个数,再比较两个哈希是否相等。

先考虑如何能找到 s 划分的所有指定长度 n 的单词?从下标 i 开始把 s 划分为字母为 n 的单词,i 的取值为 0 ~ n-1,则只需要循环 n次就可以划分出所有长度为 n 的单词,因为从 i = n+1 开始划分与 i = 0 开始划分的单词是一样的。

从下标 i 开始划分 s,通过移动右指针right以间隔 n 来遍历 s。若此单词不在word中,表示匹配不成功,则要清除之前hash中记录的单词,并且移动窗口左指针到下一个单词继续匹配。若此单词在word中,表示匹配成功,则将该单词加入到hash中,并检查该单词是否匹配多次,即检查该单词在hash中出现的次数是否比word中高,若是则需要缩小窗口,也就是left右移,将移出窗口的单词在hash中–,直到hash中出现的次数小于word中次数。

最后只要成功匹配的单词数达到 m 时,则表示找到了一个串联子串,其left为该字串的起始下标,放入result即可。

【算法描述】

//滑动窗口
vector<int> findSubstring(string s, vector<string>& words) 
{
    vector<int> result;    //存放结果
    int m = words.size();    //单词数
    int n = words[0].size();    //每个单词的字母数
    int len = s.size();
    unordered_map<string, int> word;    //存放words中的单词及出现的个数
    for(string str : words)
    {
        word[str]++;
    }
    //从下标i开始把s划分为字母为n的单词
    //只需要循环n次就可以划分出所有长度为n的单词,因为从i = n+1开始划分与i = 0开始划分的单词是一样的
    for(int i = 0; i < n; i++)  
    {
        int left = i;   //滑窗左指针
        int right = i;  //滑窗右指针
        int count = 0;   //记录已成功匹配的单词数
        unordered_map<string, int> hash;   //存放匹配成功的单词及出现的个数
        while(right + n <= len)
        {
            string str(s.begin() + right, s.begin() + right + n);  //左闭右开:划分单词
            right += n;   //窗口右边界右移一个单词的长度
            if(word.find(str) == word.end())  //此单词不在words中,表示匹配不成功
            {
                count = 0;   //重置,清除之前记录的单词
                hash.clear();
                left = right;  //移动窗口左指针
            }
            else  //此单词在words中,表示匹配成功
            {
                hash[str]++;  //将单词加到hash中
                count++;
                while(hash[str] > word[str]) //一个单词匹配多次,需要缩小窗口,也就是left右移
                {
                    string temp(s.begin() + left, s.begin() + left + n);
                    hash[temp]--;
                    count--;
                    left += n;
                }
            }
            if(count == m)  //成功匹配的单词数达到m时,表示找到了一个串联子串
            {
                result.push_back(left);
            }
        }
    }
    return result;
}

有效的数独

【题目描述】

请你判断一个 9 x 9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。

  • 数字 1-9 在每一行只能出现一次。
  • 数字 1-9 在每一列只能出现一次。
  • 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

注意:

  • 一个有效的数独(部分已被填充)不一定是可解的。
  • 只需要根据以上规则,验证已经填入的数字是否有效即可。
  • 空白格用 '.' 表示。

【输入输出实例】

示例 1:

在这里插入图片描述

输入:board =
[[“5”,“3”,“.”,“.”,“7”,“.”,“.”,“.”,“.”]
,[“6”,“.”,“.”,“1”,“9”,“5”,“.”,“.”,“.”]
,[“.”,“9”,“8”,“.”,“.”,“.”,“.”,“6”,“.”]
,[“8”,“.”,“.”,“.”,“6”,“.”,“.”,“.”,“3”]
,[“4”,“.”,“.”,“8”,“.”,“3”,“.”,“.”,“1”]
,[“7”,“.”,“.”,“.”,“2”,“.”,“.”,“.”,“6”]
,[“.”,“6”,“.”,“.”,“.”,“.”,“2”,“8”,“.”]
,[“.”,“.”,“.”,“4”,“1”,“9”,“.”,“.”,“5”]
,[“.”,“.”,“.”,“.”,“8”,“.”,“.”,“7”,“9”]]
输出:true

示例 2:

输入:board =
[[“8”,“3”,“.”,“.”,“7”,“.”,“.”,“.”,“.”]
,[“6”,“.”,“.”,“1”,“9”,“5”,“.”,“.”,“.”]
,[“.”,“9”,“8”,“.”,“.”,“.”,“.”,“6”,“.”]
,[“8”,“.”,“.”,“.”,“6”,“.”,“.”,“.”,“3”]
,[“4”,“.”,“.”,“8”,“.”,“3”,“.”,“.”,“1”]
,[“7”,“.”,“.”,“.”,“2”,“.”,“.”,“.”,“6”]
,[“.”,“6”,“.”,“.”,“.”,“.”,“2”,“8”,“.”]
,[“.”,“.”,“.”,“4”,“1”,“9”,“.”,“.”,“5”]
,[“.”,“.”,“.”,“.”,“8”,“.”,“.”,“7”,“9”]]
输出:false
解释:除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。 但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。

【算法思路】

由于board中的整数限定在1到9的范围内,因此可以分别建立哈希表来存储任一个数在相应维度上是否出现过。维度有3个:所在的行所在的列所在的以粗实线分隔的3x3宫

遍历到每个数的时候,例如board[i][j],我们判断其是否满足三个条件:

  • 在第 i 个行中是否出现过;
  • 在第 j 个列中是否出现过;
  • 在第 *j/3 + (i/3)3 个 3x3宫 中是否出现过;

关于第三点从 数组下标3x3宫序号 的变换,为什么是 *j/3 + (i/3)3 呢?

重述一遍问题:给定 i 和 j ,如何判定board[i][j]在第几个3x3宫呢? 显然属于第几个 3x3宫 是由 i 和 j 的组合唯一确定,例如board[2][2]一定是第0个box,board[4][7]一定是第5个box,可以画出来看一下,但是规律在哪里呢?我们可以考虑一种简单的情况: 一个3x9的矩阵,被分成3个 3x3宫,如图:

在这里插入图片描述

显然每个数属于哪个 3x3 宫 就只取决于纵坐标,纵坐标为 0/1/2 的都属于box[0],纵坐标为 3/4/5 的都属于box[1],纵坐标为 6/7/8的都属于box[2]。也就是j/3。

而对于 9x9 的矩阵,我们光根据 j/3 得到 0/1/2 还是不够的,需要加上一个 3 的倍数,例如加0x3,表示本行的box,加1x3,表示在下一行的box,加2x3,表示在下两行的box, 这里的 0/1/2 怎么来的?和 j/3 差不多同理,也就是i/3。

【算法描述】

//只遍历一次
bool isValidSudoku(vector<vector<char>>& board) 
{
    bool row[9][9] = {false};  //哈希表存储每一行的每个数是否出现过,默认初始情况下,每一行每一个数都没有出现过
    bool col[9][9] = {false};  //哈希表存储每一列的每个数是否出现过,默认初始情况下,每一列的每一个数都没有出现过
    bool block[9][9] = {false};  //哈希表存储每一个以粗实线分隔的3x3宫内的每个数是否出现过,默认初始情况下,在每个3x3宫内中每个数都没有出现过
    for(int i = 0; i < 9; i++)
    {
        for(int j = 0; j < 9; j++)
        {
            // 遍历到第i行第j列的那个数,我们要判断这个数在其所在的行有没有出现过
            // 同时判断这个数在其所在的列有没有出现过
            // 同时判断这个数在其所在的3x3宫中有没有出现过
            if(board[i][j] != '.')
            {
                int index = board[i][j] - '1';    //board[i][j]为数字1~9,哈希映射转换为下标0~8
                //如果board[i][j]这个数在行、列、或3x3宫内重复出现,直接返回false
                //注意:j/3+(i/3)*3 是 3x3宫 和 block下标 的对应关系,每个3x3宫对应block的一行
                if(row[i][index] || col[j][index] || block[j/3 + (i/3)*3][index])
                {
                    return false;
                }
                //之前都没出现过,现在出现了,就给它置为1,下次再遇见就能够直接返回false
                row[i][index] = true;    //board每行 映射为 row每行
                col[j][index] = true;    //board每列 映射为 col每行
                block[j/3 + (i/3)*3][index] = true;    //board每3x3宫 映射为 block每行
            }
        }
    }
    return true;
}


//多次遍历——依次判断三个规则(不推荐)
bool isValidSudoku(vector<vector<char>>& board) {
    for(int i = 0; i < 9; i++)
    {
        unordered_set<char> hash1;  //哈希表存储每一行的每个数是否出现过
        unordered_set<char> hash2;  //哈希表存储每一列的每个数是否出现过
        for(int j = 0; j < 9; j++)
        {
            if(board[i][j] != '.')    //检查数字1~9在每一行只能出现一次
            {
                if(hash1.find(board[i][j]) != hash1.end())
                {
                    return false;
                }
                hash1.insert(board[i][j]);
            }
            if(board[j][i] != '.')    //检查数字1~9在每一列只能出现一次
            {
                if(hash2.find(board[j][i]) != hash2.end())
                {
                    return false;
                }
                hash2.insert(board[j][i]);
            }
        }
    }
    for(int k = 0; k < 9; k += 3)  //检查数字1~9在每一个以粗实线分隔的3x3宫内只能出现一次
    {
        unordered_set<char> hash1;
        unordered_set<char> hash2;
        unordered_set<char> hash3;
        for(int i = k; i < k + 3; i++)
        {
            for(int j = 0; j < 3; j++)
            {
                if(board[i][j] != '.')
                {
                    if(hash1.find(board[i][j]) != hash1.end())
                    {
                        return false;
                    }
                    hash1.insert(board[i][j]);
                }
            }
            for(int j = 3; j < 6; j++)
            {
                if(board[i][j] != '.')
                {
                    if(hash2.find(board[i][j]) != hash2.end())
                    {
                        return false;
                    }
                    hash2.insert(board[i][j]);
                }
            }
            for(int j = 6; j < 9; j++)
            {
                if(board[i][j] != '.')
                {
                    if(hash3.find(board[i][j]) != hash3.end())
                    {
                        return false;
                    }
                    hash3.insert(board[i][j]);
                }
            }
        }
    }
    return true;
}

解数独

【题目描述】

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。题目数据 保证 输入数独仅有一个解

【输入输出实例】

示例 1:

上一篇:Git使用指南-基本命令


下一篇:网络层5——IPV6