方向一:豆包MarsCode AI 刷题-找单独的数

74 阅读8分钟

寻找独特数字卡片

问题描述

在一个班级中,每位同学都拿到了一张卡片,上面有一个整数。有趣的是,除了一个数字之外,所有的数字都恰好出现了两次。现在需要你帮助班长小C快速找到那个拿了独特数字卡片的同学手上的数字是什么。 要求:

  1. 设计一个算法,使其时间复杂度为 O(n),其中 n 是班级的人数。
  2. 尽量减少额外空间的使用,以体现你的算法优化能力。

测试样例

样例1:

输入:cards = [1, 1, 2, 2, 3, 3, 4, 5, 5]
输出:4
解释:拿到数字 4 的同学是唯一一个没有配对的。 样例2: 输入:cards = [0, 1, 0, 1, 2]
输出:2
解释:数字 2 只出现一次,是独特的卡片。 样例3: 输入:cards = [7, 3, 3, 7, 10]
输出:10
解释:10 是班级中唯一一个不重复的数字卡片。

约束条件

1.1 ≤ cards.length ≤ 1001 2.0 ≤ cards[i] ≤ 1000 3.班级人数为奇数 4.除了一个数字卡片只出现一次外,其余每个数字卡片都恰好出现两次


解题思路

需要在一个整数数组中找到唯一一个没有重复的数字。数组中的其他数字都恰好出现两次,只有这个数字出现一次。 由于题目要求时间复杂度为 O(n),并且尽量减少额外空间的使用,我们可以考虑使用位运算来解决这个问题。

异或运算的性质

  1. 自反性:任何数与自己异或结果为 0: a⊕a=0
  2. 单位元性:任何数与 0 异或,结果为它自己: a⊕0=a
  3. 交换律:异或操作满足交换律,两个数的异或顺序可以交换: a⊕b=b⊕a
  4. 结合律:异或操作满足结合律,多个数的异或可以任意组合: a⊕(b⊕c)=(a⊕b)⊕c
  5. 对称性:异或操作是对称的,即: a⊕b=b⊕a

给定一个数组,其中有一个数字只出现一次,其他数字都出现两次。利用异或运算的特性,可以在 O(n) 时间复杂度内找出这个唯一的数字。原因是所有成对出现的数字在异或运算中会抵消掉,剩下的就是唯一出现的那个数字。

分析问题:首先需要容器vector来存储每位同学的卡片,在函数solution()中,用基于范围for()循环遍历容器,通过异或运算,只会返回单独的一个数字;根据题目,动态数组类型为int,可在函数中传入参数,vector < int > cards;用result作为返回值。

#include <iostream>
#include <vector>
using namespace std;
int solution(vector<int> cards) {
    int result = 0;
    for (int num : cards) 
    {
        result=result^num;
    }
    return result;
}
int main() {
    cout << solution({1, 1, 2, 2, 3, 3, 4, 5, 5}) << endl;
    cout << solution({0, 1, 0, 1, 2} ) << endl;
    cout << solution({7, 3, 3, 7, 10})  << endl;
    return 0;
}

基于此问题:掌握的知识点

为什么用vector容器,而不用其他?

在解决“找单独数”的问题时,我们可以利用 异或(XOR)操作 的特性来高效地找到唯一的数字。对于容器的选择,使用 vector 容器而不是 listmap,主要是出于以下几个原因:

1. 访问效率
  • vectorvector 是一个动态数组,支持 常数时间访问,即可以通过索引直接访问任何位置的元素 (O(1) 时间复杂度)。遍历 vector 中的每个元素时,可以高效地进行顺序访问。
  • listlist 是一个双向链表,每次访问一个元素都需要 O(n)  时间,因为你需要从头开始遍历链表直到目标元素。因此,list 在这种需要按顺序遍历所有元素的情况中,效率较低。
  • mapmap 是基于红黑树实现的(或者其他平衡二叉树结构),每次插入、查找、删除操作的时间复杂度是 O(log n) ,相比 vector 的 O(1)  访问效率,map 更慢。同时,map 会消耗额外的空间来存储键值对的结构,这对于仅仅需要进行简单异或运算的任务来说,是不必要的
2. 内存消耗
  • vector 是一个连续内存块,它使用的内存结构紧凑,空间复杂度为 O(n) ,其中 n 是元素个数。
  • list 和 map 都会有更大的内存开销。list 需要为每个节点存储前向和后向指针,而 map 不仅需要存储键和值的组合,还要存储树的结构和指针,通常会比 vector 占用更多的内存。
3. 算法要求

题目要求我们 遍历一次数组 对所有元素进行异或操作。由于 vector 支持随机访问,它非常适合这种顺序遍历的场景。实际上,我们只需要一次遍历即可解决问题,因此没有必要引入复杂的容器(如 listmap)。而且,由于异或运算的 交换律结合律,遍历顺序不影响最终结果,vector 的顺序访问已经足够满足要求。

4. 不需要额外存储频次信息

题目中只有一个元素是单独出现的,其他元素都成对出现。因此,不需要像在一些其他问题中那样存储每个元素的频次信息,这样的操作通常需要 哈希表(如 map计数数组 等容器。相反,使用 vector 直接对所有数字进行异或 就能够得到正确答案,不需要额外的空间来存储频次,因此 map 的使用是完全不必要的。

为什么不用 list 或 map
  • list:对比 vectorlist 在按顺序访问时的性能较差,因为每次访问都需要从头节点或尾节点开始遍历,效率较低。
  • map:为了保存每个元素及其出现的次数,使用 map 会显著增加时间和空间复杂度。在本题中,只需要通过异或运算求解,因此 map 的使用不仅复杂,而且低效。
总结:
  • vector 在此类问题中非常合适,因为它具有高效的顺序访问性能,并且不需要额外存储元素频次,符合题目要求的高效解决方案。
  • list 和 map 由于额外的内存和操作开销,不适合用于这种问题的解决。
为什么用范围for(),而不用其他迭代器?
1. 使用传统的 for 循环(基于索引)

你可以使用传统的 for 循环,通过索引来访问容器中的元素:

cpp
for (int i = 0; i < cards.size(); ++i) {
    int num = cards[i];
    // 使用 num
}

这种写法通过索引 i 来访问 cards 中的元素,cards[i] 给出容器的第 i 个元素。

2. 使用迭代器(iterator

C++ STL 容器(如 std::vector)提供了迭代器来遍历容器。你可以使用 begin()end() 方法来获取容器的迭代器,然后通过迭代器来访问每个元素:

cpp
for (std::vector<int>::iterator it = cards.begin(); it != cards.end(); ++it) {
    int num = *it;  // 解引用迭代器获取元素
    // 使用 num
}

在这个写法中,it 是一个迭代器,通过 *it 来获取当前迭代器所指向的元素。

3. 使用 auto 结合迭代器(简化版本)

C++11 引入了 auto 关键字,可以自动推导变量的类型。结合迭代器和 auto,可以使代码更加简洁:

cpp
for (auto it = cards.begin(); it != cards.end(); ++it) {
    int num = *it;
    // 使用 num
}

这里,auto 会自动推导 it 的类型为 std::vector<int>::iterator,从而避免手动指定迭代器类型。

4. 使用 std::for_each 和 Lambda 表达式

std::for_each 是 C++ 标准库提供的一个算法,用于遍历容器并对每个元素执行某些操作。你可以与 Lambda 表达式结合使用:

cpp
#include <algorithm>  // 引入 std::for_each
#include <functional> // 引入 std::function

std::for_each(cards.begin(), cards.end(), [](int num) {
    // 使用 num
});

在这个写法中,std::for_each 会遍历 cards 容器中的每个元素,并对每个元素执行 Lambda 表达式中的操作。Lambda 表达式中的 num 参数会依次获得容器中的每个元素。

5. 使用 C++11 范围基 for 循环的引用(如果需要修改元素)

如果你需要在遍历过程中修改 cards 中的元素,可以通过引用来遍历容器:

cpp
for (int& num : cards) {
    num *= 2;  // 假设我们将每个数字都乘以 2
}

这里,int& num 使用引用类型,这意味着我们在循环中修改的是 cards 中的实际元素,而不是它们的副本。

6. 使用 while 循环和迭代器

你也可以使用 while 循环结合迭代器来遍历容器:

cpp
auto it = cards.begin();
while (it != cards.end()) {
    int num = *it;
    ++it;
    // 使用 num
}

这种方式通过手动控制迭代器来遍历容器,适用于需要在循环内部做一些额外的逻辑控制的场景。

总结

除了 for (int num : cards) 这种范围基 for 循环,C++ 提供了多种方法来遍历容器。每种方法有不同的优点:

  • 传统 for 循环:适用于你需要索引或者需要访问元素的下标时。
  • 迭代器:适用于需要精确控制遍历过程,或者在容器类型不确定时(如 std::liststd::set 等)。
  • std::for_each 和 Lambda:适用于函数式编程风格,代码简洁,且能与 STL 算法结合使用。
  • 引用遍历:适用于需要修改元素时。
  • while 循环和迭代器:适用于手动控制循环的场景。