当前位置: 首页 > news >正文

剑指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],输出任意一个重复数字,如 23

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,表示数组中元素的类型(可以是 intdouble 等)
  • 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++标准库的容器,属于无序关联容器,其核心特性如下:

  1. 存储唯一元素
    • 容器中不会存在重复元素(每个元素都是唯一的),插入已存在的元素时会失败(返回的迭代器指向已有的元素,且不会改变容器)。
  2. 无序性
    • 元素的存储顺序与插入顺序无关,也不按任何规则排序(内部通过哈希表实现,元素分散存储在桶中)。
    • 无法通过下标访问元素,只能通过迭代器遍历(遍历顺序不确定)。
  3. 基于哈希表实现
    • 内部使用哈希表(散列表)存储元素,通过元素的哈希值快速定位其存储位置。
    • 插入、删除、查找操作的平均时间复杂度为 O (1)(理想情况下,无哈希冲突时),最坏情况为 O (n)(哈希冲突严重时)。
  4. 元素不可修改
    • 存储的元素是常量(const),不能直接修改容器中的元素值(若需修改,需先删除旧元素,再插入新元素),因为修改会改变哈希值,破坏存储结构。
  5. 支持高效查找
    • 核心优势是快速查找,通过 find() 方法可在平均 O (1) 时间内判断元素是否存在,这也是它适合用于 “去重”“判重” 场景的原因(如前文中检测重复数字)。
  6. 模板参数
    • 需指定元素类型(如 unordered_set<int>unordered_set<string>),默认使用 std::hash<T> 计算哈希值,也可自定义哈希函数和比较函数。

简言之,unordered_set 的核心特性是无序、唯一、哈希存储、高效查找,适合需要快速判断元素是否存在且不关心顺序的场景。

4.2 unordered_set判断元素是否存在?

  1. seen.find(attr[i])
    • 作用:在哈希集合 seen 中查找值为 attr[i] 的元素。
    • 返回值:
      • 若找到该元素,返回指向该元素的迭代器(类似指针,指向元素位置)。
      • 若未找到,返回一个特殊迭代器 seen.end()(表示集合的 “尾部”,即不存在该元素)。
  2. seen.end()
    • 这是 unordered_set 的一个成员函数,返回指向集合最后一个元素之后位置的迭代器(不指向任何实际元素),用于标记 “查找失败” 或 “遍历结束”。
  3. 整个表达式的含义
    • seen.find(attr[i]) != seen.end() 表示:
      “在集合中找到了 attr[i] 这个元素”(因为查找返回的迭代器不是 end(),说明指向了有效元素)。
    • 反之,若 == seen.end(),则表示 “未找到该元素”。
http://www.dtcms.com/a/327652.html

相关文章:

  • RabbitMQ-知识技能图谱(总结篇)
  • 【时时三省】(C语言基础)建立动态链表
  • LeetCode189~191、198~214题解
  • 探秘酵母单杂交技术:解锁基因调控的密码
  • WEB虚拟主机3种部署方式全解析
  • 【Java Web 快速入门】九、事务管理
  • 【数据分享】2018-2024年中国10米分辨率春小麦和冬小麦分布栅格数据
  • Unity:GUI笔记(一)——文本、按钮、多选框和单选框、输入框和拖动条、图片绘制和框绘制
  • vue3大事件
  • 4.运算符
  • TF-IDF——红楼梦案例
  • IIS Express中可以同时加载并使用.net4.0和.NET 2.0的 DLL
  • Linux服务:Apache 虚拟主机配置指南:多站点部署三种方式详解
  • 【DL】浅层神经网络
  • 一键设置 NTP 时区的脚本(亲测,适用于部署 K8S 的前置环境)
  • 测试环境下因网络环境变化导致集群无法正常使用解决办法
  • Java调用Vue前端页面生成PDF文件
  • 【K8s】K8s控制器——Deamonset、Statefulset、Job与CronJob
  • 基于 Easy Rules 的电商订单智能决策系统:构建可扩展的业务规则引擎实践
  • 使用ceph-deploy安装和配置RADOS Gateway (RGW)并使用S3访问集群
  • SQL Server 常用运维命令整理
  • 武汉火影数字|VR红色文化馆打造 沉浸式体验红色文化
  • GeoScene 空间大数据产品使用入门(5)在线建模
  • SpringBoot集成MyBatis的SQL拦截器
  • Unity-VR插件AutoHand
  • Python 爬虫获取淘宝商品信息、价格及主图的实战指南
  • iOS 签名证书实践日记,我的一次从申请到上架的亲历
  • Spring框架如何解决循环依赖
  • C++11语法(2)
  • Flink Redis维表:Broadcast Join与Lookup Join对比及SQL示例