LeetCode哈希表篇

159 阅读7分钟

哈希表基础

主要的三个哈希结构

  • 数组:存放的数据较少时可用数组结构

  • set

集合                 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |

| std::set           | 红黑树   |   有序   |       否        |      否      | O(log n) | O(log n)|
| std::multiset      | 红黑树   |   有序   |       是        |      否      | O(logn)  | O(logn) |
| std::unordered_set | 哈希表   |   无序   |       否        |      否      | O(1)     | O(1)    |
  • map
映射                 | 底层实现 | 是否有序  | 数值是否可以重复 | 能否更改数值  | 查询效率  | 增删效率 |

| std::map           |  红黑树  |  key有序  |    key不可重复  |   key不可修改 |  O(logn)  | O(logn)  |
| std::multimap      |  红黑树  |  key有序  |     key可重复   |   key不可修改 |  O(log n) | O(log n) |
| std::unordered_map |  哈希表  |  key无序  |    key不可重复  |   key不可修改 |  O(1)     | O(1)     |

当遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法

经典例题

有效的字母异位词(242)

题目地址

给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。 注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。

示例 1:
输入: s = "anagram", t = "nagaram"
输出: true
示例 2:
输入: s = "rat", t = "car"
输出: false

思路

判断字母是否出现过,用哈希法,英文字母一共就26个,所以可以使用数组来做,要判断次数,将26个英文字母与数组相映射,每个元素对应一个字母,出现一次,相应字母对应的元素位置就加一,判断第二个字符串时,相应字母的元素减一,若数组每个位置的元素都为0,则两个字符串出现的字母次数都相同。 代码

#include <string>

using namespace std;

class Solution {
public:
    bool isAnagram(string s, string t) {
        int record[26] = {0};
        for (int i = 0; i < s.size(); i++)
        {
            record[s[i] - 'a']++;
        }

        for (int j = 0; j < t.size(); j++)
        {
            record[t[j] - 'a']--;
        }

        for (int k = 0; k < 26; k++)
        {
            if (record[k] != 0)
            {
                return false;
            }
        }
        return true;
    }
};

两个数组的交集(349)

题目地址

给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是唯一 的。我们可以不考虑输出结果的顺序 。

示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

思路

这道题看有没有交集,有没有出现过,用哈希法,这道题并没有限制大小,所以不用数组,用set,题目要求元素唯一,所以要去重,并且可以无序,所以使用unordered_set。

代码

#include <vector>
#include <unordered_set>

using namespace std;

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> result_set;
        unordered_set<int> nums_set(nums1.begin(), nums1.end());
        for (int num : nums2)
        {
            // 找到了返回该元素迭代器, 没找到返回end()迭代器
            if (nums_set.find(num) != nums_set.end())
            {
                result_set.insert(num);
            }
        }
        return vector<int> (result_set.begin(), result_set.end());
    }
};

快乐数(202)

题目地址

编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n 是 快乐数 就返回 true ;不是,则返回 false 。

示例 1:
输入:n = 19
输出:true
解释:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1
示例 2:
输入:n = 2
输出:false

思路

如果一个数是快乐数的话,那么sum就不会重复,如果sum重复,就意味着将陷入无限循环,也就意味着该数不是快乐数,所以可以用unordered_set存放sum的结果,若是结果与集合中的重复,那么就返回false。

代码

#include <unordered_set>
using namespace std;

class Solution
{
public:
    bool isHappy(int n)
    {
        unordered_set<int> result;
        while (1)
        {
            int sum = getSum(n);     //计算和
            if (sum == 1)            //sum为1, 是快乐数
            {
                return true;
            }
            if (result.find(sum) != result.end())   //在sum集合中找到了一样的,进入死循环,不是快乐数
            {
                return false;
            }
            else     //将新的sum加入到集合中
            {
                result.insert(sum);
            }

            n = sum;    //更新n
        }
    }

    int getSum(int n)    //计算sum
    {
        int sum = 0;
        while (n)
        {
            sum += (n % 10) * (n % 10);
            n /= 10;
        }
        return sum;
    }
};

两数之和(1)

题目地址

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

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]

思路

要找两数之和,先将遍历的一个数保存下来,然后看当前值能不能找到与遍历过的数相加等于target,但是最后要返回的是下标,所以这里使用键值对来存储元素,题目要求不能重复出现,任意顺序返回,所以可以使用unordered_map来保存元素。

代码

#include <unordered_map>
#include <vector>

using namespace std;

class Solution
{
public:
    vector<int> twoSum(vector<int> &nums, int target)
    {
        unordered_map<int, int> result;
        for (int i = 0; i < nums.size(); i++)
        {
            auto iter = result.find(target - nums[i]);
            if (iter != result.end())
            {
                return {iter->second, i};   //return vector
            }
            else
            {
                result.insert(make_pair(nums[i], i));
            }
        }
        return {};
    }
};

四数相加II(454)

题目地址

给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:

0 <= i, j, k, l < n nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
示例 2:
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1

思路

这道题返回的是满足条件的有多少个数组,所以可以用unordered_map,先遍历nums1和nums2,将两个数组和保存到map中,然后再遍历nums3和nums4,找到能使和map中的数相加等于0的组合。

代码

#include <vector>
#include <unordered_map>

using namespace std;

class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map<int, int> umap;
        for (int a : nums1)
        {
            for (int b : nums2)
            {
                umap[a + b]++;
            }
        }
        int count = 0;
        for (int c : nums3)
        {
            for (int d : nums4)
            {
                if (umap.find((0 - c - d)) != umap.end())
                {
                    count += umap[0 - c - d];
                }
            }
        }
        return count;
    }
};

赎金信(383)

题目地址

给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以,返回 true ;否则返回 false 。

magazine 中的每个字符只能在 ransomNote 中使用一次。

示例 1:
输入:ransomNote = "a", magazine = "b"
输出:false
示例 2:
输入:ransomNote = "aa", magazine = "ab"
输出:false
示例 3:
输入:ransomNote = "aa", magazine = "aab"
输出:true

都是小写字母

思路

这道题是看字符串A能不能由字符串B构成,小写字母一共就26位,所以可以用数组节省空间,如果字符串A比字符串B还要长那么就直接return false,创建一个数组record用来保存字母的出现次数,先记录B的每个字符的出现次数,然后再减掉A每个字符出现的次数,若A所对应的字符小于0,则说明A不能由B组成。 代码

#include <iostream>
#include <string>

using namespace std;
  
class Solution
{
public:
    bool canConstruct(string ransomNote, string magazine)
    {
        int record[26] = {0};
        if (ransomNote.size() > magazine.size())   //A比B长,不可能
        {
            return false;
        }

        for (int i = 0; i < magazine.size(); i++)    //记录B出现次数
        {
            record[magazine[i] - 'a']++;
        }

        for (int j = 0; j < ransomNote.size(); j++)
        {
            record[ransomNote[j] - 'a']--;

            if (record[ransomNote[j] - 'a'] < 0)    //A的某个字符出现次数比B的多
            {
                return false;
            }
        }
        
        return true;
    }
};

三数之和(15)

题目地址

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

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

示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1][-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0

思路

题目要求不能包含重复的三元组,所以得要有去重的操作,这道题目是在同一个数组nums中找到三个数使得三数之和为0,所以可以设置三个指针分别代表三个数a,b,c在数组上滑动。首先将数组排序,将a从头开始遍历,b为a+1,c在最后位置上,若nums[a]+nums[b]+nums[c]>0,则说明要变小,所以c向做移动,若<0,则说明要变大,b向右移动,直到b和c相遇。

代码

#include <vector>
#include <algorithm>

using namespace std;

class Solution
{
public:
    vector<vector<int>> threeSum(vector<int> &nums)
    {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());    //排序

        for (int a = 0; a < nums.size(); a++)
        {
            if (nums[a] > 0)   //若第一个元素就比0大那么怎么也不可能三数之和为0
            {
                return result;
            }

            //对a进行去重,若判断a和a+1,则相当于去除掉了三元组内重复的情况 EX[0, 0, 0]
            if (a > 0 && nums[a] == nums[a - 1])
            {
                continue;
            }

            int b = a + 1;
            int c = nums.size() - 1;

            while (b < c)
            {
                if (nums[a] + nums[b] + nums[c] > 0)
                {
                    c--;
                }
                else if (nums[a] + nums[b] + nums[c] < 0)
                {
                    b++;
                }
                else
                {
                    result.push_back(vector<int> {nums[a], nums[b], nums[c]});
                    while (b < c && nums[b] == nums[b + 1])    //b去重
                    {
                        b++;
                    }
                    while (b < c && nums[c] == nums[c - 1])    //c去重
                    {
                        c--;9
                    } 

                    b++;
                    c--;
                }
            }
        }
        return result;
    }
};

四数之和(18)

题目地址

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • a、b、c 和 d 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target
  • 你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

思路

这道题和之前的三数之和还不太一样,之前的是要找到一个三元组使得和为0,这道题是找到四元组和为target,其为不确定的值,所以这里不能直接判断当数组的第一个数大于target就直接pass,逻辑可以改为:num[i] > target && (target > 0 || nums[i] > 0)。在用双指针遍历数组时,先要用两层for循环确定 nums[k] + nums[i] 的值,循环内有双指针left和right,找到nums[k] + nums[i] + nums[left] + nums[right] == target。时间复杂度O(n3)

代码

#include <vector>
#include <algorithm>

using namespace std;

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());       //排序
        for (int a = 0; a < nums.size(); a++)     //遍历第一个数
        {
            if (nums[a] > target && nums[a] >= 0)
            {
                break;
            }
            if (a > 0 && nums[a] == nums[a - 1])   //对a去重
            {
                continue;
            }

            for (int b = a + 1; b < nums.size(); b++)   //遍历第二个数
            {
                if (nums[a] + nums[b] > target && nums[a] + nums[b] >= 0)
                {
                    break;
                }
                if (b > a + 1 && nums[b] == nums[b - 1])  //对b去重
                {
                    continue;
                }

                //定义双指针
                int left = b + 1;
                int right = nums.size() - 1;

                while (left < right)
                {
                    if ((long)nums[a] + nums[b] + nums[left] + nums[right] > target)
                    {
                        right--;
                    }
                    else if ((long)nums[a] + nums[b] + nums[left] + nums[right] < target)
                    {
                        left++;
                    }
                    else
                    {
                        result.push_back(vector<int>{nums[a], nums[b], nums[left], nums[right]});
                        while(right > left && nums[left] == nums[left + 1])      //对c去重
                        {
                            left++;
                        }
                        while (right > left && nums[right] == nums[right - 1])      //对d去重
                        {
                            right--;
                        }

                        left++;
                        right--;
                    }

                }
            }
        }
        return result;
    }
};