Skip to content

Lesson 10: 集合专题

  1. Unordered Set 的基本概念
  2. 例题讲解
  3. 举一反三
  4. 课后练习

1. Unordered Set 的基本概念

unordered_set 是 C++ STL 中的一个容器类,用于存储无序的、不重复的元素。unordered_set 不允许包含重复的元素,如果插入相同的元素,只会保留一个。此外,unordered_set 不保证元素的存储顺序。

1.1 unordered_set 的特性:

  1. 元素唯一性:unordered_set 不允许重复元素,每个元素在集合中只能出现一次。
  2. 无序:unordered_set 不保证元素的插入顺序。
  3. 常用方法:
  4. insert(const T& value):向集合中添加元素,如果元素已经存在,则不会插入。
  5. erase(const T& value):从集合中移除指定的元素。
  6. find(const T& value):查找集合中是否包含某个元素,返回一个迭代器,如果找不到则返回 end()。
  7. size():返回集合中元素的数量。
  8. empty():判断集合是否为空。
  9. clear():清空集合中的所有元素。

1.2 unordered_set 的应用场景

unordered_set 基于哈希表实现,提供了插入、删除和查找操作,适用于以下场景:

  • 去重功能:当需要存储不重复的元素时,unordered_set 是理想的选择。例如,用于存储唯一的用户ID、用户名等。
  • 集合操作:unordered_set 支持经典的集合操作,如交集、并集和差集,能够有效处理两个集合间的运算。

1.交集(两个集合共有的元素):

C++
1
2
3
4
5
6
7
8
9
   std::unordered_set<int> set1 = {1, 2, 3};
   std::unordered_set<int> set2 = {2, 3, 4};
   std::unordered_set<int> intersection;
   for (int elem : set1) {
      if (set2.find(elem) != set2.end()) {
          intersection.insert(elem);
      }
   }
   // intersection 现在是 {2, 3}

2.并集(两个集合的所有元素):

C++
1
2
3
std::unordered_set<int> union_set = set1;
union_set.insert(set2.begin(), set2.end());
// union_set 现在是 {1, 2, 3, 4}
3.差集(仅存在于第一个集合的元素):
C++
1
2
3
4
5
6
7
std::unordered_set<int> difference;
for (int elem : set1) {
 if (set2.find(elem) == set2.end()) {
     difference.insert(elem);
 }
}
// difference 现在是 {1}

1.3 完整实例

以下是一个完整的使用 unordered_set 实现集合操作的 C++ 示例:

C++
#include <iostream>
#include <unordered_set>

int main() {
    // 创建两个集合
    std::unordered_set<int> set1 = {1, 2, 3};
    std::unordered_set<int> set2 = {2, 3, 4};

    // 交集
    std::unordered_set<int> intersection;
    for (int elem : set1) {
        if (set2.find(elem) != set2.end()) {
            intersection.insert(elem);
        }
    }
    std::cout << "Intersection (交集): ";
    for (int elem : intersection) std::cout << elem << " ";
    std::cout << std::endl;

    // 并集
    std::unordered_set<int> union_set = set1;
    union_set.insert(set2.begin(), set2.end());
    std::cout << "Union (并集): ";
    for (int elem : union_set) std::cout << elem << " ";
    std::cout << std::endl;

    // 差集
    std::unordered_set<int> difference;
    for (int elem : set1) {
        if (set2.find(elem) == set2.end()) {
            difference.insert(elem);
        }
    }
    std::cout << "Difference (差集): ";
    for (int elem : difference) std::cout << elem << " ";
    std::cout << std::endl;

    return 0;
}

2. 例题讲解

2.1 LC349 两个数组的交集

题目描述

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

解法1:遍历法

思路分析

  1. 遍历:
  2. 使用两个嵌套的循环,遍历 nums2 中的每个元素,并检查它是否在 nums1 中存在。

  3. 去重操作:

  4. 使用 find() 函数来确保结果中没有重复的元素。

代码实现

C++
class Solution {
public:
    std::vector<int> intersection(std::vector<int>& nums1, std::vector<int>& nums2) {
        std::vector<int> result{};
        for(int i = 0; i < nums2.size(); i++) {  // 遍历 nums2 中的每个元素
            for(int j = 0; j < nums1.size(); j++) {  // 在 nums1 中查找该元素
                if(nums1[j] == nums2[i]) {
                    // 确保 result 中没有重复元素
                    if(std::find(result.begin(), result.end(), nums2[i]) == result.end()) {
                        result.push_back(nums2[i]);
                    }
                    break;  // 找到后跳出内层循环
                }
            }
        }
        return result;
    }
};

解法2:遍历 + unordered_set

思路分析

  1. 遍历:
  2. 同样使用两个嵌套的循环,遍历 nums2 中的每个元素,检查是否存在于 nums1 中。

  3. 使用 unordered_set 去重:

  4. 使用 unordered_set 存储结果,避免重复的元素。

  5. 将结果从 unordered_set 转换为 vector:

  6. 最后,将 unordered_set 中的元素存入 vector 并返回。

参考解答

C++
class Solution {
public:
    std::vector<int> intersection(std::vector<int>& nums1, std::vector<int>& nums2) {
        std::unordered_set<int> result{};
        for(int i = 0; i < nums2.size(); i++) {  // 遍历 nums2 中的每个元素
            for(int j = 0; j < nums1.size(); j++) {  // 在 nums1 中查找该元素
                if(nums1[j] == nums2[i]) {
                    result.insert(nums2[i]);  // 使用 unordered_set 自动去重
                    break;  // 找到后跳出内层循环
                }
            }
        }

        // 将 unordered_set 转换为 vector 并返回
        std::vector<int> return_answer{};
        for (auto it = result.begin(); it != result.end(); ++it) {
            return_answer.push_back(*it);
        }

        return return_answer;
    }
};

2.2 LC645 错误的集合

问题描述

集合 s 包含从 1 到 n 的整数。不幸的是,因为数据错误,导致集合里面某一个数字复制了成了集合里面的另外一个数字的值,导致集合丢失了一个数字,并且有一个数字重复 。

给定一个数组 nums 代表了集合 S 发生错误后的结果。

请你找出重复出现的整数,再找到丢失的整数,将它们以数组的形式返回。

解题思路

  1. 使用 unordered_set 存储出现的数字:
  2. 遍历 nums,将数字添加到 unordered_set 中。由于 unordered_set 不允许重复元素,我们可以在插入时检查是否有重复的数字。

  3. 计算丢失的数字:

  4. 通过数学公式计算从 1 到 n 的总和,并减去 unordered_set 中的元素和,即可得出丢失的数字。

  5. 返回结果:

  6. 将找到的重复数字和丢失的数字以数组形式返回。

参考解答

C++
class Solution {
public:
    vector<int> findErrorNums(vector<int>& nums) {
        unordered_set<int> arrange{};
        int repeat;

        // 找到重复的数字
        for(int c : nums) {
            bool can_insert = arrange.insert(c).second;
            if (!can_insert) {
                repeat = c;
            } 
        }

        // 计算 1 到 n 的总和
        int expected_sum = (1 + nums.size()) * nums.size() / 2;

        // 计算实际集合中的数字和
        int sum = 0;
        for(int c : arrange) {
            sum += c;
        }

        // 计算丢失的数字
        int difference = expected_sum - sum;

        // 返回结果
        vector<int> output{repeat, difference};
        return output;
    }
};

3. 举一反三

3.1 LC575 分糖果

问题描述

Alice 有 n 枚糖,其中第 i 枚糖的类型为 candyType[i]。Alice 注意到她的体重正在增长,所以前去拜访了一位医生。

医生建议 Alice 要少摄入糖分,只吃掉她所有糖的 n / 2 即可(n 是一个偶数)。Alice 非常喜欢这些糖,她想要在遵循医生建议的情况下,尽可能吃到最多不同种类的糖。

给你一个长度为 n 的整数数组 candyType。

返回: Alice 在仅吃掉 n / 2 枚糖的情况下,可以吃到糖的最多种类数。

解题思路

  1. 统计糖果的种类:
  2. 使用 unordered_set 来存储糖果的不同种类。unordered_set 自动去除重复元素,从而可以记录不同类型的糖果。

  3. 计算可吃的糖果数量:

  4. 由于 Alice 只能吃掉 n / 2 枚糖果,计算糖果的不同种类数量与 n / 2 的最小值,即 Alice 最多能吃到的种类数。

  5. 返回结果:

  6. 使用 min 函数返回 unordered_set 中糖果种类的数量与 Alice 能吃掉糖果数量(n / 2)的较小值。

参考解答

C++
#include <vector>
#include <unordered_set>
#include <algorithm>
using namespace std;

class Solution {
public:
    int distributeCandies(vector<int>& candyType) {
        // 使用 unordered_set 来存储糖果的种类
        unordered_set<int> types{};

        // 将所有糖果类型插入 unordered_set,自动去重
        for (int c : candyType) {
            types.insert(c);
        }

        // 计算 Alice 可以吃的最多种类的糖果
        return min(types.size(), candyType.size() / 2);
    }
};

解释

  1. 存储糖果种类:通过遍历 candyType 数组,将所有糖果类型存储在 unordered_set 中,自动去除重复的糖果种类。

  2. 计算能吃的糖果种类:

  3. types.size() 表示糖果的不同种类数。
  4. candyType.size() / 2 表示 Alice 按照医生建议可以吃的糖果总数。

  5. 取最小值:最终返回 min(types.size(), candyType.size() / 2),表示 Alice 能够吃到的最多不同种类的糖果。

3.2 LC771 宝石与石头

问题描述

给你一个字符串 jewels 代表石头中宝石的类型,另有一个字符串 stones 代表你拥有的石头。

stones 中每个字符代表了一种你拥有的石头的类型,你想知道你拥有的石头中有多少是宝石。

字母区分大小写,因此 "a" 和 "A" 是不同类型的石头。

解题思路

  1. 遍历宝石类型:
  2. 不使用额外的容器,直接遍历字符串 stones 中的每个字符,并在 jewels 字符串中查找是否存在该字符。

  3. 统计宝石数量:

  4. 如果在 jewels 中找到对应的字符,计数器 count 增加。

参考解答

C++
#include <string>
using namespace std;

class Solution {
public:
    int numJewelsInStones(string jewels, string stones) {
        int count = 0;

        // 遍历 stones 中的每个字符,检查是否在 jewels 中
        for (char i : stones) {
            if (jewels.find(i) != string::npos) {  // 如果在 jewels 中找到对应的字符
                count += 1;
            }
        }

        return count;  // 返回宝石的数量
    }
};

4. 课后练习

题目编号 题目名称 简介
LC 187 重复的 DNA 序列 找到所有出现次数超过一次的 DNA 序列。
LC 961 N 次重复元素在 2N 数组中 在长度为 2N 的数组中找到出现 N 次的元素。
LC 2441 存在其负数的最大正整数 找到一个正整数,其负数也在数组中存在。
LC 2357 通过减去相同数使数组为零 通过减去相同的数使数组中的所有元素变为零的最小操作次数。
LC 217 存在重复元素 给你一个整数数组nums,如果任一值在数组中出现至少两次,返回true,否则false。