剑指offer第2版——面试题3:数组中重复的数字
文章目录
- 一、题目
- 1.1 数组中重复的数字
- 1.2 考点
- 二、答案
- 2.1 思路
- 2.2 我的答案
- 三、扩展题目
- 3.1 哈希表 / 集合辅助法(空间换时间)
- 3.2 二分查找法(利用数字范围特性,时间换空间)
- 3.3 总结
- 四、知识扩展
- 4.1 unordered_set是C++标准库的容器,属于无序关联容器,其核心特性如下:
- 4.2 unordered_set判断元素是否存在?
一、题目
1.1 数组中重复的数字
在一个长度为 n
的数组里,所有数字都在 0~n-1
的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例:
输入数组 [2, 3, 1, 0, 2, 5, 3]
,输出任意一个重复数字,如 2
或 3
。
1.2 考点
- 对一维数组的理解及编程能力:一维数组在内存中占用连续空间,可基于下标快速定位对应元素。解题时需灵活运用数组特性,通过下标访问与操作元素来寻找重复数字。
- 算法的时间与空间复杂度分析:需掌握不同解法的时间和空间成本,并能够优化。像先排序再找重复值,时间复杂度为 O (n log n),空间复杂度为 O (1);利用哈希表记录访问情况,时间复杂度达 O (n),但空间复杂度为 O (n);而利用数组数字范围在 0 至 n-1 这一条件,用原地置换法能实现时间复杂度 O (n) 与空间复杂度 O (1),是本题的较优解法。
- 分析问题与挖掘题目隐含条件的能力:关键在于留意到数字范围 “0 至 n-1” 和数组长度 n 的对应关系,进而设计出原地置换的高效解法,这体现了挖掘出题条件背后价值以构思算法的能力。
二、答案
2.1 思路
①把数组排序一下!
②比较前头两个数字即可!
2.2 我的答案
#include <iostream>
#include <algorithm>
using namespace std;template <typename T,size_t N>//模板参数用于接收N的大小
void printArray(T (&attr)[N]) // 接收大小为 N 的 int 数组的引用
{sort(begin(attr), end(attr));T temp = attr[0];for (int i = 0; i < N; ++i){if (attr[i] == attr[i + 1]){cout << attr[i] << endl;}}
}int main() {int a[7] = { 2,3,1,0,2,5,3 };printArray(a);return 0;
}
模板定义:
template <typename T, size_t N>
template <>
:声明这是一个模板函数typename T
:定义第一个模板参数T
,表示数组中元素的类型(可以是int
、double
等)size_t N
:定义第二个模板参数N
,表示数组的大小(编译时确定的常量)
函数参数:
void printArray(T (&attr)[N])
T (&attr)[N]:接收一个数组的引用,其中:
T
是数组元素的类型(由模板参数指定)N
是数组的大小(由模板参数指定)- 使用引用
&
是为了精确传递数组类型,避免数组被隐式转换为指针(从而能获取到数组的实际大小N
)
为啥不能是T& attr[N]
?
T& attr[N]
声明的是 “一个包含 N 个 T
类型引用的数组”,但 C++ 明确禁止 “引用数组”:
- 引用本质上是变量的别名,不是对象,而数组是 “对象的集合”,因此无法创建 “引用的数组”。
- 编译器会直接报错(如
error: declaration of 'attr' as array of references
)。
与原代码意图的区别
原代码 T (&attr)[N]
是 “数组的引用”(引用一个大小为 N 的 T
类型数组),目的是:
- 精确传递数组类型,保留数组的大小信息(
N
可通过模板推导)。 - 避免数组被隐式转换为指针(如果写成
T attr[]
,函数内部会退化为指针,无法获取N
)。
三、扩展题目
如何不改变数组的情况下?达到目的?
3.1 哈希表 / 集合辅助法(空间换时间)
-
原理:遍历数组时,用哈希表(或集合)记录已出现的数字,每次判断当前数字是否已在集合中,若存在则直接返回(重复数字)。
-
特点:
- 时间复杂度 O (n)(一次遍历),空间复杂度 O (n)(最多存储 n-1 个不重复数字)。
- 不修改原数组,实现简单,适合数字范围无限制的场景。
#include <iostream> #include <algorithm> #include <unordered_set> // 哈希集合头文件 using namespace std;template <typename T, size_t N> void printArray(T(&attr)[N]) {unordered_set<T> seen; // 哈希集合存储已出现的元素for (size_t i = 0; i < N; ++i) {// 若当前元素已在集合中,说明找到重复数字if (seen.find(attr[i]) != seen.end()) {cout << "找到重复数字:" << attr[i] << endl;return; // 找到一个即返回}// 否则将元素加入集合seen.insert(attr[i]);}// 题目保证有重复,实际可省略此句cout << "无重复数字" << endl; }int main() {int a[7] = { 2,3,1,0,2,5,3 };printArray(a); // 输出:找到重复数字:2return 0; }
3.2 二分查找法(利用数字范围特性,时间换空间)
-
原理:基于题目中 “数字范围为 0~n-1” 的特性,通过二分法统计每个区间内数字的出现次数,找到存在重复的区间,逐步缩小范围。
-
例如:数组长度为 7,数字范围 0~6。先二分取中间值 3,统计 0~3 范围内的数字出现次数,若次数 >4(3-0+1),则重复数字在 0~3 中;否则在 4~6 中,以此类推。
-
特点:
-
时间复杂度 O (n log n)(每次二分需遍历数组统计次数,共 log n 轮),空间复杂度 O (1)。
-
不修改原数组,无需额外空间,但仅适用于 “数字范围与数组长度匹配” 的场景。
#include <iostream> #include <algorithm> using namespace std;// 统计数组中在[left, right]范围内的数字个数 template <typename T, size_t N> int countInRange(T(&attr)[N], int left, int right) {int count = 0;for (size_t i = 0; i < N; ++i) {if (attr[i] >= left && attr[i] <= right) {count++;}}return count; }// 二分查找法找重复数字 template <typename T, size_t N> void printArray(T(&attr)[N]) {int left = 0;int right = N - 1; // 数字范围是0~n-1while (left < right) {int mid = left + (right - left) / 2; // 避免溢出int count = countInRange(attr, left, mid);// 若[left, mid]范围内的数字个数超过区间长度,说明重复数字在此区间if (count > mid - left + 1) {right = mid;} else {// 否则在[mid+1, right]区间left = mid + 1;}}// 循环结束时left == right,即为重复数字cout << "找到重复数字:" << left << endl; }int main() {int a[7] = { 2,3,1,0,2,5,3 };printArray(a); // 输出:找到重复数字:2(或3,取决于二分过程)return 0; }
-
3.3 总结
- 哈希表法更通用,实现简单,适合大多数场景;
- 二分查找法空间更优,但依赖题目特定条件(数字范围 0~n-1),是对 “利用隐含条件” 考点的延伸。
两种方法均满足 “不改变原数组” 的要求,核心是在时间与空间复杂度之间做权衡。
四、知识扩展
4.1 unordered_set是C++标准库的容器,属于无序关联容器,其核心特性如下:
- 存储唯一元素
- 容器中不会存在重复元素(每个元素都是唯一的),插入已存在的元素时会失败(返回的迭代器指向已有的元素,且不会改变容器)。
- 无序性
- 元素的存储顺序与插入顺序无关,也不按任何规则排序(内部通过哈希表实现,元素分散存储在桶中)。
- 无法通过下标访问元素,只能通过迭代器遍历(遍历顺序不确定)。
- 基于哈希表实现
- 内部使用哈希表(散列表)存储元素,通过元素的哈希值快速定位其存储位置。
- 插入、删除、查找操作的平均时间复杂度为 O (1)(理想情况下,无哈希冲突时),最坏情况为 O (n)(哈希冲突严重时)。
- 元素不可修改
- 存储的元素是常量(
const
),不能直接修改容器中的元素值(若需修改,需先删除旧元素,再插入新元素),因为修改会改变哈希值,破坏存储结构。
- 存储的元素是常量(
- 支持高效查找
- 核心优势是快速查找,通过
find()
方法可在平均 O (1) 时间内判断元素是否存在,这也是它适合用于 “去重”“判重” 场景的原因(如前文中检测重复数字)。
- 核心优势是快速查找,通过
- 模板参数
- 需指定元素类型(如
unordered_set<int>
、unordered_set<string>
),默认使用std::hash<T>
计算哈希值,也可自定义哈希函数和比较函数。
- 需指定元素类型(如
简言之,unordered_set
的核心特性是无序、唯一、哈希存储、高效查找,适合需要快速判断元素是否存在且不关心顺序的场景。
4.2 unordered_set判断元素是否存在?
seen.find(attr[i])
- 作用:在哈希集合
seen
中查找值为attr[i]
的元素。 - 返回值:
- 若找到该元素,返回指向该元素的迭代器(类似指针,指向元素位置)。
- 若未找到,返回一个特殊迭代器
seen.end()
(表示集合的 “尾部”,即不存在该元素)。
- 作用:在哈希集合
seen.end()
- 这是
unordered_set
的一个成员函数,返回指向集合最后一个元素之后位置的迭代器(不指向任何实际元素),用于标记 “查找失败” 或 “遍历结束”。
- 这是
- 整个表达式的含义
seen.find(attr[i]) != seen.end()
表示:
“在集合中找到了attr[i]
这个元素”(因为查找返回的迭代器不是end()
,说明指向了有效元素)。- 反之,若
== seen.end()
,则表示 “未找到该元素”。