代码随想录刷题-哈希表总结篇

哈希表

哈希表理论基础

常见的三种哈希结构分为:数组、set、map
在 C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:

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

std::unordered_set 底层实现为哈希表,std::set 和 std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以 key 值是有序的,但 key 不可以修改,改动 key 值会导致整棵树的错乱,所以只能删除和增加。

映射 底层实现 是否有序 数值是否可以重复 能否更改数值 查询效率 增删效率
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)

std::unordered_map 底层实现为哈希表,std::map 和 std::multimap 的底层实现是红黑树。同理,std::map 和 std::multimap 的 key 也是有序的。

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

  • 数组:当需要快速检索和存储固定大小的元素时,应该使用数组
  • set:当需要存储不重复的元素,并且需要快速检索时,应该使用 set
  • map:当需要存储键值对,并且需要快速检索时,应该使用 map

unordered_set 常用操作

  • 插入元素
    • insert(element): 在unordered_set中插入一个元素,如果元素已存在则不进行插入。
  • 删除元素
    • erase(element): 从 unordered_set 中删除一个指定元素,如果元素不存在,则不会删除。
    • clear(): 清空unordered_set内所有元素。
  • 查询元素
    • find(element): 查找特定元素并返回指向其位置的迭代器,未能找到时返回end()迭代器。
    • count(element): 判断某个元素是否存在于 unordered_set 中,如果存在,则返回 1,否则返回 0。
  • 其他操作
    • size():返回 unordered_set 中元素的个数。
    • empty():判断 unordered_set 是否为空集,如果是,则返回 true,否则返回 false。
#include <iostream>
#include <unordered_set>
using namespace std;

int main() {
    
    
    unordered_set<int> uset;

    // 插入元素
    uset.insert(1);
    uset.insert(2);
    uset.insert(3);

    // 删除元素
    uset.erase(2);

    // 查找元素
    auto it = uset.find(3);
    if (it != uset.end()) {
    
    
        cout << "Found " << *it << endl;
    }

    // 获取元素个数
    cout << "Size: " << uset.size() << endl;

    // 判断元素是否存在
    if (uset.count(2)) {
    
    
        cout << "2 exists" << endl;
    } else {
    
    
        cout << "2 does not exist" << endl;
    }

    // 清空元素
    uset.clear();

    // 判断是否为空
    if (uset.empty()) {
    
    
        cout << "Empty set" << endl;
    }

    // 遍历元素
    for (auto x : uset) {
    
    
        cout << x << " ";
    }
    cout << endl;
    return 0;
}

unordered_map 常用操作

  • 插入元素
    • insert(pair<key, value>): 在unordered_map中插入一个键值对,如果键(key)已存在,则更新值(value)。
  • 删除元素
    • erase(key): 从unordered_map中删除指定键(key)的元素,如果键不存在,则不会删除。
    • clear(): 清空unordered_map内所有键值对。
  • 查询元素
    • find(key): 查找特定键并返回指向其位置的迭代器,未能找到时返回end()迭代器。
    • count(key): 判断某个键是否存在于 unordered_map 中,如果存在,则返回 1,否则返回 0。
  • 其他操作
    • size():返回 unordered_map 中键值对的个数。
    • empty():判断 unordered_map 是否为空集,如果是,则返回 true,否则返回 false。
#include <iostream>
#include <unordered_map>   // 引入 unordered_map 头文件
using namespace std;

int main() {
    
    
  
  // 创建一个空的 unordered_map
  unordered_map<int, string> myMap;
  
  // 添加键值对(insert 或 emplace)
  myMap.insert({
    
    1, "apple"});
  myMap.emplace(2, "banana");
  
  // 使用 [] 运算符访问元素,注意:如果元素不存在,会自动创建一个默认值
  cout << myMap[1] << endl;    // 输出:apple
  
  // 查找并访问元素,使用 find 方法获取迭代器进行访问,注意:如果没有找到,返回值为 end()
  auto it = myMap.find(2);
  if (it != myMap.end()) {
    
    
    cout << it->second << endl;   // 输出:banana
  }
  
  // 遍历 unordered_map
  for (auto& [key, value]: myMap) {
    
    
    cout << "key: " << key << ", value: " << value << endl;
  }
  
  // 判断 unordered_map 是否为空
  if (myMap.empty()) {
    
    
    cout << "myMap is empty" << endl;
  } else {
    
    
    cout << "myMap has " << myMap.size() << " elements" << endl;
  }
  
  // 删除元素,使用 erase 方法传入键
  myMap.erase(1); 
  
  return 0;
}

有效的字母异位词

本节对应代码随想录中:代码随想录,讲解视频:学透哈希表,数组使用有技巧!Leetcode:242.有效的字母异位词_哔哩哔哩_bilibili

习题

题目链接:242. 有效的字母异位词 - 力扣(LeetCode)

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

示例 1:
输入: s = “anagram”, t = “nagaram”
输出: true

示例 2:
输入: s = “rat”, t = “car”
输出: false

进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?

排序

先对两个字符串排序,然后就可以直接比较两个字符串是否相等

class Solution {
    
    
public:
    bool isAnagram(string s, string t) {
    
    
        if (s.length() != t.length()) {
    
    
            return false;
        }
        sort(s.begin(), s.end());
        sort(t.begin(), t.end());
        return s == t;
    }
};
  • 时间复杂度:O(nlogn)。其中 n 为字符串的长度。因为使用了排序函数,排序的时间复杂度为 O(nlogn)。字符串比较的时间复杂度为 O(n),因此总时间复杂度为 O(nlogn + n) = O(nlogn)。
  • 空间复杂度:O(log n)。其中 n 为字符串的长度。排序需要使用 O(logn) 的栈空间,因此空间复杂度为 O(logn)。

我的解法

既然 s 和 t 都仅包含小写字母,那就可以分别定义两个数组,用来存对应字符的出现次数,如果两个数组对应位置的数值一样那就是 true。同时如果两个字符串长度不同,可以直接返回 false

class Solution {
    
    
   public:
    bool isAnagram(string s, string t) {
    
    
        // 两个字符若长度不等直接返回false
        int size = s.length();
        if (size != t.length()) {
    
    
            return false;
        }
        int num_s[26] = {
    
    0};
        int num_t[26] = {
    
    0};
        // 遍历字符串,两个计数的数组计数
        for (int i = 0; i < size; i++) {
    
    
            num_s[int(s[i]) - 97] += 1;
            num_t[int(t[i]) - 97] += 1;
        }
        // 一旦某一个字符的次数不一样直接返回false
        for (int i = 0; i < 26; i++) {
    
    
            if (num_s[i] != num_t[i]) {
    
    
                return false;
            }
        }
        return true;
    }
};
  • 时间复杂度:O(n)。其中 n 为字符串的长度。遍历字符串需要 O(n) 的时间,计数数组的遍历需要 O(1) 的时间,因此总时间复杂度为 O(n)。
  • 空间复杂度:O(1)。由于只使用了长度为 26 的计数数组,空间复杂度是常数级别的。

可以优化的点

  • int(s[i]) - 97 可以替换为 s[i] - 'a',这样无需记住字符 a 的 ASCII

哈希表

其实我的解法也属于哈希表,不过其实不需要两个数组来计数,只需要一个数组统计其中一个字符串,然后遍历另一个字符串的时候对应位置的数值减1,如果对应位置数值<0,说明 t 的这个位置的元素比 s 的多,则返回 false

例如,s 为 aab,t 为 aaa。遍历到 t 的第二个 a 的时候,table[0]已经等于0了,当遍历到 t 的第三个 a 的时候,table[0]=-1<0,说明 t 中的 a 比 s 中的 a 至少多一个(后面可能还会有更多的 a,但没必要再去遍历了)。

代码随想录中是先遍历 t,对应位置减1,再判断数组是否全为0,但下面这种解法只需使用两个 for 循环,比代码随想录上的3个 for 循环更优

class Solution {
    
    
public:
    bool isAnagram(string s, string t) {
    
    
        if (s.length() != t.length()) {
    
    
            return false;
        }
        int table[26] = {
    
    0};
        // 数组计数
         for (int i = 0; i < s.length(); i++) {
    
    
            table[s[i] - 'a']++;
        }
        for (int i = 0; i < t.length(); i++) {
    
    
            // 遍历字符串t对应位置减1
            table[t[i] - 'a']--;
            // 如果<0说明这个位置t的比s的多,即多减了一次
            if (table[t[i] - 'a'] < 0) {
    
    
                return false;
            }
        }
        return true;
    }
};
  • 时间复杂度:O(n)。其中n为字符串的长度,需要遍历两个字符串,以及进行常数次的数组操作。
  • 空间复杂度:O(1)。因为使用了一个大小为26的数组来存储每个字母出现的次数,空间大小与输入的字符串长度无关。

进阶解法

如果输入字符串包含 unicode 字符,那么可能的字符就不再是26个,没法使用大小为26的数组进行计数。此时就需要使用 unordered_map 了。

思路和上面的哈希表的差不多,先遍历 s,将其每个元素放进哈希表中。再遍历字符串 t,如果某个元素的次数<0说明 t 的这个位置的元素比 s 的多,则返回 false。只不过哈希表解法计数是根据字符-a 找索引,而这个是根据 key 直接找索引

class Solution {
    
    
   public:
    bool isAnagram(string s, string t) {
    
    
        if (s.size() != t.size()) {
    
      // 先判断两个字符串长度是否相同
            return false;
        }
        unordered_map<char, int> hash;  // 定义哈希表

        for (int i = 0; i < s.size(); i++) {
    
      // 遍历一个字符串
            hash[s[i]]++;  // 把s的每个字符放进哈希表中
        }                  
        for (int i = 0; i < t.size(); i++) {
    
    
            hash[t[i]]--;  // 在hash表中抵消该字符次数
            if (hash[t[i]] < 0) {
    
    
                return false;
            }
        }
        return true;  // 若出现次数都一致,则返回true
    }
};
  • 时间复杂度:O(n)。其中n为字符串的长度,需要遍历两个字符串,并对哈希表进行常数次操作。
  • 空间复杂度:O(n)。因为哈希表中最多存储 n 个字符,所以空间复杂度与输入的字符串长度有关。

两个数组的交集

本节对应代码随想录中:代码随想录,讲解视频:学透哈希表,set使用有技巧!Leetcode:349. 两个数组的交集_哔哩哔哩_bilibili

习题

题目链接:349. 两个数组的交集 - 力扣(LeetCode)

给定两个数组 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] 也是可通过的

我的解法

这题我是用的上题进阶解法用到的 unordered_map,只不过由于取交集,如果有重复的只输出一次即可。所以我在存的时候让 nums1对应的次数都为1,遍历 nums2时,如果次数为1则 push_back 并且次数+1,这样就不会重复输出。

class Solution {
    
    
   public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
    
    
        unordered_map<int, int> hash;
        vector<int> res;
        // 不同的数只放一次
        for (int i = 0; i < nums1.size(); i++) {
    
    
            hash[nums1[i]] = 1;
        }
        for (int i = 0; i < nums2.size(); i++) {
    
    
            // 次数为1说明重叠,加1后后面就不会push_back同样的元素
            if (hash[nums2[i]] == 1) {
    
    
                res.push_back(nums2[i]);
                hash[nums2[i]]++;
            }
        }
        return res;
    }
};
  • 时间复杂度:O(m+n)。其中m为nums1的长度,n为nums2的长度。需要遍历两个数组来建立哈希表和查找重叠元素,时间复杂度为O(m+n)。
  • 空间复杂度:O(min(m,n))。哈希表中最多存储 min(m,n)个不同的元素,因此空间复杂度为 O(min(m,n))。

set 解法

这道题由于去重,所以其实用 set 更合适。

代码随想录中用了两个 set,一个遍历 nums1,另一个存放结果(这样结果就会去重)。但其实只用一个 set 即可,用一个 set 遍历 nums1后,再去遍历 nums2时,判断元素是否出现,若出现则 push_back 并且 erase 即删除这个元素。这样例如示例1中后面即使有重复的2,但我们在遍历第一个2进行 push_back 后就将 set 中的2删除了,遇到第二个2的时候其实就是找不到的状态了,所以就不会重复输出。

class Solution {
    
    
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
    
    
        unordered_set<int> nums_set(nums1.begin(), nums1.end());
        vector<int> res;
        for (int num : nums2) {
    
    
            // 如果nums_set包含num,则num是交集之一,加入结果并从集合中删除num
            if (nums_set.erase(num)) {
    
    
                res.push_back(num);
            }
        }
        return res;
    }
};
  • 时间复杂度:O(m+n)。其中m和n分别为nums1和nums2的长度。因为需要遍历nums1和nums2各一次,而unordered_set的查找和删除操作的时间复杂度都是常数级别的,所以总时间复杂度为O(m+n)。
  • 空间复杂度:O(min(m,n))。其中m和n分别为nums1和nums2的长度。因为需要使用unordered_set存储nums1中的元素,所以空间复杂度取决于nums1和nums2中较小的那个。

快乐数

本节对应代码随想录中:代码随想录,讲解视频:暂无

习题

题目链接:202. 快乐数 - 力扣(LeetCode)

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

「快乐数」 定义为:

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

示例 1:
输入:n = 19 输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1

set 解法

刚开始看到题目后,一直纠结于题目中说的可能无限循环。拆分数字并判断和是否为1并不困难,但如果程序可能陷入无限循环,那一直找不到结果就会超时,但是和一直不为1,又无法确定后面的和是否会为1。

没有思路的时候可以输出下每个数的和,看看整个过程,代码如下

int n = 12;
for (int i = 0; i < 20; i++) {
    
    
    int sum = 0;
    while (n != 0) {
    
    
        sum += (n % 10) * (n % 10);
        n /= 10;
    }
    cout << sum << "#";
    n = sum;
}

每次会输出数字的和,共输出20次

当 n=12时,会输 5#25#29#85#89#145#42#20#4#16#37#58#89#145#42#20#4#16#37#58#,注意看数字出现了循环。#89#145#42#20#4#16#37#58# 从89开始就有了这一段循环。既然出现循环,说明当计算的和为89的时候,程序计算一段时间和后又会回到89。也就是说,如果一个数字出现了第二次,那么在这个数前面一定存在一段循环,那这个数字 n 一定不是快乐数,因此就可以用 unordered_set 来判断元素是否重复出现,若重复出现则可以返回 false,终止这个无限循环。

class Solution {
    
    
   public:
    bool isHappy(int n) {
    
    
        unordered_set<int> mySet;
        while (1) {
    
    
            int sum = 0;
            // 计算每个位置上的数字的平方和
            while (n != 0) {
    
    
                sum += (n % 10) * (n % 10);
                n /= 10;
            }
            // 和为1则说明是快乐数
            if (sum == 1) {
    
    
                return true;
            }
            // 如果set中存在sum,说明一定会陷入循环中,即不可能是快乐数
            if (mySet.count(sum)) {
    
    
                return false;
            }
            mySet.insert(sum);
            n = sum;    // 将sum赋值给n,继续计算数字n每个位置上的数字的平方和
        }
    }
};
  • 时间复杂度:O(logn)。查找给定数字的下一个值的成本为 O(logn)
  • 空间复杂度:O(k)。其中 k 为循环的次数,每次循环都会存入 unordered_set 中。

两数之和

本节对应代码随想录中:代码随想录,讲解视频:梦开始的地方,Leetcode:1.两数之和,学透哈希表,map使用有技巧!_哔哩哔哩_bilibili

习题

题目链接:1. 两数之和 - 力扣(LeetCode)

给定一个整数数组 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]

暴力解法

作为 LeetCode 的第一题,题意很明确,就是数组中找和为 target 的两个数,返回他们的下标,并且一定能找到这俩数,并且答案唯一(顺序可以不一样)。

最直接的就是两个 for 循环遍历一个数的时候看看剩余的数里面有没有满足和为 target 的

class Solution {
    
    
   public:
    vector<int> twoSum(vector<int>& nums, int target) {
    
    
        int size = nums.size();
        vector<int> res;
        for (int i = 0; i < size; i++) {
    
    
            for (int j = i + 1; j < size; j++) {
    
    
                if (nums[i] + nums[j] == target) {
    
    
                    res.push_back(i);
                    res.push_back(j);
                    return res;
                }
            }
        }
        return res;
    }
};
  • 时间复杂度:O(n^2)。有两个循环,每个循环运行 n 次,所以总的运行次数为 n * n = n^2
  • 空间复杂度:O(n)。创建了长度为 n 的向量 res 来存储结果

可以优化的点

  • 其实不用新定义个 res,在返回的时候直接返回 return {i,j} 即可,这样空间复杂度就优化成 O(1)

哈希表

上面的暴力解法,就是先遍历一个数,然后再查找剩余的数中有没有 target-nums[i]。那就简化成了在数组中查找一个元素是否存在的问题,所以就可以使用哈希表,因为哈希表查找元素的时间复杂度为 O(1)。

而哈希表有3种:数组、set 和 map,只有 set 和 map 是有 O(1)的 find 函数的。但是 set 只存储 key,而题目要求返回的是下标,set 无法直接得到原来的数的下标。

STL 中有个 distance 函数可以计算两个元素之间的距离,计算找到的元素和 set.begin()之间的距离就可以获得下标吗?
答:这种方法无法获得原有的下标,使用 unordered_set 插入元素时不会保留原始顺序,而是根据哈希表中元素的内部顺序随机排序。也就是说,即使向 set 中添加与 vector 中相同的元素,它们在 set 中的顺序也可能与在 v1中的顺序不同。例如示例中的{2,7,11,15}在 unordered_set 中就会变成{11,7,15,2}的顺序

map 中只有 unordered_map 的查询效率为 O(1),那么 key 和 value 存什么呢?map 查找的时候查的是 key,而我们要找的是 target-nums[i]即查的是元素本身。那么 key 存的就是数组中的元素,而 value 存的就是对应的下标

我的写法如下

class Solution {
    
    
   public:
    vector<int> twoSum(vector<int>& nums, int target) {
    
    
        // 创建一个无序哈希表map,key为整数型,value为整数型
        unordered_map<int, int> map;
        // 遍历nums中的元素,将元素值作为键,元素下标作为值插入到map中
        for (int i = 0; i < nums.size(); i++) {
    
    
            map.insert({
    
    nums[i], i});
        }
        for (int i = 0; i < nums.size(); i++) {
    
    
            int tmp = target - nums[i];
            // 判断map中是否含有该差值,如果存在且满足条件则返回下标值
            if (map.find(tmp) != map.end()) {
    
    
                if (i != map[tmp]) {
    
    
                    return {
    
    i, map[tmp]};
                }
            }
        }
        return {
    
    };
    }
};
  • 时间复杂度:O(n)。其中,第一个 for 循环遍历整个 nums 数组,需要 n 次操作;第二个 for 循环也遍历整个 nums 数组,需要 n 次操作,哈希表map插入n个元素,需要n次操作。因此总的时间复杂度为O(n+n+n)=O(n)
  • 空间复杂度:O(n)。创建了一个哈希表,存储了n个元素,因此空间复杂度为O(n)

而看过题解后,上面的写法可以有优化的地方。我是先将所有元素插入到 map 中,而实际上没有必要先插入所有元素。只需要当没有 target - nums[i]的时候再将当前 nums[i]插入到 map 中,这样也避免了和自己匹配即2*nums[i]=target。如2,7,11,15,target=9,当 i=0时,虽然没有找到 map 中含有7。但是将2插入 map 后,当 i=1即去找 map 中是否含有2的时候就可以找到了。

class Solution {
    
    
   public:
    vector<int> twoSum(vector<int>& nums, int target) {
    
    
        unordered_map<int, int> map;
        for (int i = 0; i < nums.size(); ++i) {
    
    
            auto it = map.find(target - nums[i]);
            if (it != map.end()) {
    
    
                // 如果找到了,返回这两个元素的下标
                return {
    
    it->second, i};
            }
            // 否则将当前的数插入哈希表
            map[nums[i]] = i;
        }
        return {
    
    };
    }
};
  • 时间复杂度:O(n)。其中n是nums数组中元素的个数,因为只需要遍历一次nums数组就能找到符合条件的目标值两个元素
  • 空间复杂度:O(n)。在最坏情况下,所有的元素都需要插入哈希表中,所以哈希表的空间占用和nums数组大小相同

四数相加II

本节对应代码随想录中:代码随想录,讲解视频:学透哈希表,map使用有技巧!LeetCode:454.四数相加II_哔哩哔哩_bilibili

习题

题目链接:454. 四数相加 II - 力扣(LeetCode)

给你四个整数数组 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

暴力解法

暴力解法会超时,这里只作为参考

题目意思简单来说就是四个数组分别找出一个数,让它们相加为0。最直接的就是四个 for 循环遍历每个数组,只要和为0,那就加1。但是四重 for 循环的时间复杂度是 O(n^4),肯定会超时。

那我们可不可以把四重 for 循环降到三重 for 循环?

我们这章学的是哈希表,而哈希表查找指定元素是否出现过的时间复杂度为 O(1)。第四层 for 循环实际上就是查找有没有0-nums1-nums2-nums3的数存在,存在的话,它的次数是多少。那我们就可以使用一个 unordered_map 来存放一个数组如 nums4,key 存放 nums4的元素,value 存放其出现次数。这样就可以把时间复杂度降到 O(n^3),不过即使优化到三重 for 循环但还是会超时

注意不能用 set,set 只能存放 key,即不能统计出现次数。虽然用 multiset 然后再用 count 函数计数也能统计次数,但是 multiset 的查找时间复杂度为 O(logn)。

class Solution {
    
    
   public:
    int fourSumCount(vector<int>& nums1,vector<int>& nums2,vector<int>& nums3,vector<int>& nums4) {
    
    
        int size = nums1.size();
        int res = 0;
        unordered_map<int, int> hash_map;
        // key存放nums4的元素,value存放出现的次数
        for (int i = 0; i < size; i++) {
    
    
            hash_map[nums4[i]]++;
        }
        for (int i = 0; i < size; i++) {
    
    
            for (int j = 0; j < size; j++) {
    
    
                for (int m = 0; m < size; m++) {
    
    
                    // 如果能在hash_map中找到-(nums1+nums2+nums3),则将res+出现的次数
                    auto it = hash_map.find(0 - nums1[i] - nums2[j] - nums3[m]);
                    if (it != hash_map.end()) {
    
    
                        res += it->second;
                    }
                }
            }
        }
        return res;
    }
};
  • 时间复杂度:O(n^3)。其中 n 为 nums1, nums2, nums3, nums4 数组的长度,因为有三个嵌套的 for 循环
  • 空间复杂度:O(n)。因为使用了一个哈希表来存储 nums4 的元素及其出现次数,而哈希表的大小最多为 n

哈希表

既然上面的三重 for 循环会超时,那解法一定是小于三重 for 循环的。比如,可不可以只使用两重 for 循环即 O(n^2)

上面的解法中,我们是用 map 存放一个数组的元素,然后遍历剩下的三个数组。那如果我们先遍历两个数组,让 map 存放的是它们的和,然后再遍历另外两个数组。就可以查找 map 中是否含有-(nums3+nums4),有的话加上它的次数即可。这样我们就可以继续优化到 O(n^2)。

同理,如果是六个数组,那就可以存放三个数组的和,再三重 for 循环,即存放一半数组的和

class Solution {
    
    
   public:
   int fourSumCount(vector<int>& nums1,vector<int>& nums2,vector<int>& nums3,vector<int>& nums4) {
    
    
        int size = nums1.size();
        int res = 0;
        unordered_map<int, int> hash_map;
        // key存放nums1+nums2的和,value存放和出现的次数
        for (int i = 0; i < size; i++) {
    
    
            for (int j = 0; j < size; j++) {
    
    
                hash_map[nums1[i] + nums2[j]]++;
            }
        }
        for (int i = 0; i < size; i++) {
    
    
            for (int j = 0; j < size; j++) {
    
    
                // 如果能在hash_map中找到-(nums3+nums4),则将res+出现的次数
                auto it = hash_map.find(0 - nums3[i] - nums4[j]);
                if (it != hash_map.end()) {
    
    
                    res += it->second;
                }
            }
        }
        return res;
    }
};
  • 时间复杂度:O(n^2)。n 为 nums1、nums2、nums3、nums4 数组的长度,因为使用了两层 for 循环遍历四个数组,其中每一次操作都花费 O(1) 的时间。并且还有一次对哈希表的查找操作,查找的时间复杂度为 O(1),所以总时间复杂度为 O(n^2)
  • 空间复杂度:O(n^2)。n 为 nums1、nums2、nums3、nums4 数组的长度,我们使用一个哈希表记录 nums1 和 nums2 中所有元素的两两之和,最多有 n^2 种不同的可能性,所以空间复杂度为 O(n^2)

赎金信

本节对应代码随想录中:代码随想录,讲解视频:暂无

习题

题目链接:383. 赎金信 - 力扣(LeetCode)

给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。如果可以,返回 true ;否则返回 false 。

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

示例 1:
输入:ransomNote = “aa”, magazine = “ab”
输出:false

示例 2:
输入:ransomNote = “aa”, magazine = “aab”
输出:true

提示:

  • 1 <= ransomNote.length, magazine.length <= 105
  • ransomNote 和 magazine 由小写英文字母组成

哈希表

这道简单题和之前的有效的字母异位词那题差不多,是写的最快的一道题了。

判断 ransomNote 能不能由 magazine 构成,只需要先遍历下 magazine 将每个字符出现的次数存起来,然后再遍历 ransomNote,遇到每个字符将其计数-1,如果计数小于0说明 ransomNote 中的这个字符比 magazine 的至少多一个,即不能由 magazine 构成。而如果遍历完 ransomNote 也没能找到计数小于0的则说明可以由 magazine 构成。

查找元素那肯定是用哈希表,解题的关键是选择数组、set、map 中的哪一个更优?首先,set 只能存放 key,而我们还要计数,那肯定是不合适的。而题目中说了 ransomNote 和 magazine 由小写英文字母组成,这就说明最多有26个字符,那么用一个26的数组就更合适。当然,也可以用 map,但是 map 要维护哈希表比数组费时,所以数组更优,后面我会给出两种解法的代码和比较。

首先是数组的解法

class Solution {
    
    
   public:
    bool canConstruct(string ransomNote, string magazine) {
    
    
        int nums[26] = {
    
    0};
        // 先遍历magazine计算每个字符的出现次数
        for (int i = 0; i < magazine.length(); i++) {
    
    
            nums[magazine[i] - 'a']++;  // 根据-'a'计算相对位置
        }
        // 再遍历ransomNote
        for (int i = 0; i < ransomNote.length(); i++) {
    
    
            // 先将次数-1,这样如果<0(0-1<0)说明比magazine至少多一个
            nums[ransomNote[i] - 'a']--;
            if (nums[ransomNote[i] - 'a'] < 0) {
    
    
                return false;
            }
        }
        return true;
    }
};
  • 时间复杂度:O(n)。其中 N 是 magazine 的长度,因为遍历 magazine 可以花费 O(N) 的时间,并且对于每个字符,可以在常数时间内更新 nums 数组。然后我们再遍历 ransomNote,有 N 个字符,对于每个字符,我们需要常数时间检查 nums 中是否存在当前字符的频率,因此总时间复杂度为O(N)
  • 空间复杂度:O(1)。因为 nums 数组的大小始终都是常数级别的 (26)

优化:可以在最前面加一个判断,如果 ransomNote 比 magazine 更长,那一定无法由 magazine 构成,返回 false

map 的解法如下

class Solution {
    
    
   public:
    bool canConstruct(string ransomNote, string magazine) {
    
    
        unordered_map<char, int> hash_map;
        // 先遍历magazine计算每个字符的出现次数
        for (int i = 0; i < magazine.length(); i++) {
    
    
            hash_map[magazine[i]]++;    // 直接将相应元素做为key
        }
        // 再遍历ransomNote
        for (int i = 0; i < ransomNote.length(); i++) {
    
    
            // 先将次数-1,这样如果<0(0-1<0)说明比magazine至少多一个
            hash_map[ransomNote[i]]--;
            if (hash_map[ransomNote[i]] < 0) {
    
    
                return false;
            }      
        }
        return true;
    }
};
  • 时间复杂度:O(n)。其中n是magazine的长度加上ransomNote的长度,因为代码中包含两个for循环,每个循环执行次数不超过输入字符串的长度
  • 空间复杂度:O(m)。其中 m 是 magazine 中不同字符的个数。在最坏的情况下,哈希表将包含 magazine 中的全部字符

可以看到两个解法其实就是存的 key 不太一样,数组用相对位置当 key,而 map 直接用对应元素当 key。下面是 LeetCode 两种解法的提交记录,从图中也可以看出数组的解法更优

三数之和

本节对应代码随想录中:代码随想录,讲解视频:梦破碎的地方!| LeetCode:15.三数之和_哔哩哔哩_bilibili

习题

题目链接:15. 三数之和 - 力扣(LeetCode)

给你一个整数数组 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] 。
注意,输出的顺序和三元组的顺序并不重要。

双指针

这道题目难点主要在"不可重复",这样的话如果直接使用哈希表就会出现重复,但去重的操作又很麻烦。实际上使用双指针更合适。我在写的时候也想到了先排序,再对第一个数去重,但一直没想明白怎么对第二个和第三个数去重。实际上就因为这章题目是哈希表,所以一直想着用哈希表来解决。

在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]

在这里插入图片描述

为什么先进行排序?如动画上的,排序后,这样数组就是有序的。比如对 a 去重,当 i 为第二个-1时,由于和前面的-1相同,所以就可以直接跳过即 nums[i] == nums[i - 1]。注意这里不能用 i 和 i+1进行比较,因为三元组内部是可以重复的,如-1 -1 2。

对于 i 指针,是遍历三元组的第一个数,用上面的方法可以对 a 去重,那剩下的两个数该怎么选择呢?

如果 nums[i] + nums[left] + nums[right] > 0 就说明此时三数之和大了,因为数组是排序后了,所以 right 下标就应该向左移动,这样才能让三数之和小一些。如果 nums[i] + nums[left] + nums[right] < 0 说明此时三数之和小了,left 就向右移动,才能让三数之和大一些,直到 left 与 right 相遇为止。当 left=right 时,第二个数和第三个数为同一个数,不符合题意,因此边界条件是 right > left

对于 b 和 c 去重,当找到一个合适的3元组后,如果 nums[right] == nums[right - 1] 时就让 right 一直–,因为数组是有序的,知道找到一个不在重复的数为止即可完成去重操作。同理,left 一直++

class Solution {
    
    
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
    
    
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        // 找出a + b + c = 0
        // a = nums[i], b = nums[left], c = nums[right]
        for (int i = 0; i < nums.size(); i++) {
    
    
            // 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
            if (nums[i] > 0) {
    
    
                return result;
            }
            // 错误去重a方法,将会漏掉-1,-1,2 这种情况
            /*
            if (nums[i] == nums[i + 1]) {
                continue;
            }
            */
            // 正确去重a方法
            if (i > 0 && nums[i] == nums[i - 1]) {
    
    
                continue;
            }
            int left = i + 1;
            int right = nums.size() - 1;
            while (right > left) {
    
    
                // 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
                /*
                while (right > left && nums[right] == nums[right - 1]) right--;
                while (right > left && nums[left] == nums[left + 1]) left++;
                */
                if (nums[i] + nums[left] + nums[right] > 0) right--;
                else if (nums[i] + nums[left] + nums[right] < 0) left++;
                else {
    
    
                    result.push_back(vector<int>{
    
    nums[i], nums[left], nums[right]});
                    // 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
                    while (right > left && nums[right] == nums[right - 1]) right--;
                    while (right > left && nums[left] == nums[left + 1]) left++;

                    // 找到答案时,双指针同时收缩
                    right--;
                    left++;
                }
            }

        }
        return result;
    }
};
  • 时间复杂度:O(n^2)。其中 n 为输入数组 nums 的大小。主要是在外层 for 循环和内部 while 循环中。由于 for 循环遍历每一个元素,而内部 while 循环是双指针移动,因此两个循环嵌套,时间复杂度为 O(n^2)
  • 空间复杂度:O(n)。主要是在存储结果的 vector<vector> result 中,该二维向量的大小最大为 C(n,3),即 n 个元素中取出三个的组合数。因此空间复杂度为 O(n)

四数之和

本节对应代码随想录中:代码随想录,讲解视频:难在去重和剪枝!| LeetCode:18. 四数之和_哔哩哔哩_bilibili

习题

题目链接:18. 四数之和 - 力扣(LeetCode)

给你一个由 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]]

双指针

这题的解法和上面的三数之和类似。在三数之和中,我们使用一个 i 指针遍历第一个元素,然后使用 left 和 right 指针寻找合适的第二个和第三个元素。在这题中,还是用 left 和 right 指针去查找合适的最后两个元素,不过要多加一层 for 循环去遍历第二个元素

对于第一个元素去重,我们的判断条件是 i>0&&nums[i]==nums[i-1],而对于第二个元素去重,还是 nums[j]==nums[j-1] 只不过这里是 j>i+1 不能写成 j>0,比如0 0 0 0,target =0 ,i=0,j=1时 nums[j]==nums[j-1] 这其实就是用 nums[j]和前面的 nums[i]进行了对比。当 j=i+1 的时候是可能满足四元组的条件的,因此,判断条件是 j>i+1&&nums[j]==nums[j-1],其实就是要大于它循环的初始值

另外,还需要注意的是,四个数直接相加,在 LeetCode 给的测试用例中会有整数溢出的情况,因此要转换成 long。可以写四条赋值语句将四个数都转为 long,但更好的写法是在相加的时候将其中一个数转为 long,其余的 int 类型的元素也会被自动升级为 long 类型进行计算,从而避免了整数溢出的问题

一样的道理,五数之和、六数之和等等都采用这种解法

class Solution {
    
    
   public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
    
    
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        // 找出a + b + c + d = target
        // a = nums[i], b = nums[j],b = nums[left], c = nums[right]
        for (int i = 0; i < nums.size(); i++) {
    
    
            // 对a去重
            if (i > 0 && nums[i] == nums[i - 1]) {
    
    
                continue;
            }
            for (int j = i + 1; j < nums.size(); j++) {
    
    
                // 对b去重
                if (j > i + 1 && nums[j] == nums[j - 1]) {
    
    
                    continue;
                }
                int left = j + 1;
                int right = nums.size() - 1;
                while (right > left) {
    
    
                    // nums[i] + nums[j] + nums[left] + nums[right] > target 会溢出
                    if ((long)nums[i] + nums[j] + nums[left] + nums[right] > target) {
    
    
                        right--;
                        // nums[i] + nums[j] + nums[left] + nums[right] < target会溢出
                    } else if ((long)nums[i] + nums[j] + nums[left] + nums[right] < target) {
    
    
                        left++;
                    } else {
    
    
                        result.push_back(
                            {
    
    nums[i], nums[j], nums[left], nums[right]});
                        // 去重逻辑应该放在找到一个四元组之后,对b和c去重
                        while (right > left && nums[right] == nums[right - 1])
                            right--;
                        while (right > left && nums[left] == nums[left + 1])
                            left++;

                        // 找到答案时,双指针同时收缩
                        right--;
                        left++;
                    }
                }
            }
        }
        return result;
    }
};
  • 时间复杂度:O( n 3 n^3 n3)。外层有两个 for 循环,时间复杂度为 O( n 2 n^2 n2),内部用了 while 循环,时间复杂度为 O(n),最终总的时间复杂度为 O( n 2 ∗ n n^2 * n n2n) = O( n 3 n^3 n3)。
  • 空间复杂度:O(n)。只开辟了一个 vector<\vector> result 存储答案,因此空间复杂度为 O(n)

猜你喜欢

转载自blog.csdn.net/zss192/article/details/129804411