news 2026/9/10 6:38:45

力扣3371:移项变形+哈希表,O(n)找出最大离群值

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
力扣3371:移项变形+哈希表,O(n)找出最大离群值

第一次在周赛题单里看到 3371 这道题时,我第一反应是“这题估计又是模拟题,把数组里的元素试一遍,看哪个是离群值”。真动手写才发现,如果照着题面去模拟,逻辑很容易绕进死胡同。反而是先把题目翻译成一个等式,再用“枚举一个变量、用哈希表维护另一个变量的候选集合”这个思路来解,代码又短又不容易错。

这篇文章就以力扣 3371 为例子,把“移项变形”和“枚举右维护左”这两个技巧掰开揉碎讲清楚。内容不光是给答案,更会把题目背后的思维过程、边界条件、以及我在实际提交中踩过的坑都过一遍。适合正在刷力扣、准备面试、或者想提升“把文字题变成数学公式再变成代码”能力的朋友。

1. 从题目设定到数学关系:先把“离群值”翻译成等式

1.1 题干到底在讲什么

力扣 3371 的题面看起来有点绕:数组里有 n 个元素,其中恰好 n-2 个是“特殊数字”(special numbers)。剩下两个元素里,一个是“和元素”(值等于所有特殊数字的总和),另一个才是我们要找的“离群值”(outlier)。

这里很容易产生一个误区:以为要先找出哪两个是“剩下的元素”,再判断哪个是和元素、哪个是离群值。实际上完全不需要这样。

我举个具体例子,假设数组是[2, 2, 1]

  • 特殊数字只有 1 个,值是2
  • 和元素的值就是所有特殊数字的和,也就是2
  • 离群值是1

再比如[3, 3, 6, 4]

  • 特殊数字是3, 3, 4,和为10
  • 和元素是10?但数组里没有10,所以这个例子不成立。

从这个角度看,题目的核心并不是“找到哪两个元素比较特殊”,而是“判断某个元素是否可能作为离群值,同时让其余元素满足‘和元素 = 特殊数字之和’这一条件”。

1.2 移项变形:把一段绕口的话变成一行公式

如果把条件写成数学式,事情会清晰得多。

不妨设:

  • total是整个数组所有元素的和;
  • x是离群值;
  • S是所有特殊数字的总和;
  • 和元素的值就是S

根据题面,数组总和total由三部分组成:

  1. 所有特殊数字的加和,也就是S
  2. 和元素本身的值,也是S
  3. 离群值x

所以有:

total = S + S + x

稍微整理一下,把x挪到等式左边:

total - x = 2 * S

这就是整道题最关键的一步,也是标题里“移项变形”指的东西。

原来那个“一堆特殊数字加起来要等于某个特殊元素”的表述,经过移项之后变成了一个非常干净的判断条件:如果某个元素x是离群值,那么数组中必须存在另一个元素S,使得2 * S == total - x

这一步变形的意义在于:我们不再需要关心“哪些数字是特殊数字”,也不需要去枚举各种子集。只要检查total - x能不能被 2 整除、以及对应的(total - x) / 2是否存在于数组里就够了。

1.3 为什么暴力模拟是坏的直觉

说实话,我第一次看到这题时,脑子里冒出的暴力解法是这样的:

  • 枚举每一个元素,假设它是离群值;
  • 把它从数组里删掉,剩下的元素两两组合,判断“某个元素能作为和元素,其余元素之和等于它”。

这个做法有几个问题:

第一,它需要双重枚举,复杂度轻松变成O(n^2),在n = 10^5的约束下直接超时。

第二,也是最关键的,“删掉离群值后再去找和元素”这种思路,会在逻辑上把“和元素”和“特殊数字”对立起来,导致忘记一个事实:特殊数字的总和可能非常大,甚至可能等于和元素本身的值,而和元素只是数组中的一个普通元素,它跟离群值一样,只是一个位置。

也就是说,暴力模拟的问题在于它把条件当成过程来理解,而不是当成等式来理解。一旦写成total - x = 2 * S这个等式,所有原本含糊的地方都消失了。

这里我建议刷题的朋友养成一个习惯:拿到题先在草稿纸上用字母把条件写下来,看看能不能通过移项、代入、变形变成更简单的形式,再动手设计算法。很多所谓“中等题”,其实考的就是这个“翻译”能力。

2. “枚举右维护左”的思维框架:为什么这题 O(n) 就能解

2.1 先把这个框架讲明白

“枚举右维护左”听起来像某种高级双指针技巧,实际上它的核心思想非常朴素:

当问题需要在一对元素之间建立某种关系时,不要用双重循环去配对,而是固定其中一个元素(枚举“右”),把另一个元素的可能信息提前维护好(维护“左”),从而把配对过程从 O(n^2) 降到 O(n)。

一个最经典的例子是“两数之和”:给定数组和一个目标值target,找两个数使它们的和等于target。暴力做法是枚举ij,双重循环;优化做法是遍历一遍数组,把已经见过的数字放进哈希表,对于当前数字nums[i],只需要查target - nums[i]在不在哈希表里即可。这个过程就是典型的“枚举右,维护左”——遍历到i时,i左侧所有元素的信息都被维护好了。

回到 3371 这道题,虽然它并不是严格意义上的“左右位置关系”,但思维模型是一样的:枚举一个变量作为“基准”,用辅助数据结构维护另一个变量的可选择性。

2.2 如何套用到本题

根据前面的数学推导,我们要做的事情是:

  1. 枚举每个元素x,把它当作可能的离群值;
  2. 计算target = total - x
  3. 如果target是偶数,令y = target / 2,判断y是否是数组中一个不同于x的元素;
  4. 如果成立,x就是一个合法的离群值,记录最大值。

在这个模型里:

  • 被枚举的x就是“右”;
  • 我们需要判断的候选y的集合,就是“左”;
  • 维护这个集合的数据结构,是哈希表(记录每个元素出现的频率)。

也就是说,“枚举右维护左”在这里并不需要指针,它的本质是用空间换时间,把“在剩余数组中查找某个值是否存在”的操作变成O(1)查询。

2.3 复杂度为什么是 O(n)

整个算法只需要遍历数组一次,构建频率表是O(n),枚举离群值也是O(n),每次枚举里的查表和算术运算都是O(1)。总时间复杂度O(n),空间复杂度O(n)

这里还需要注意一个细节:数组中元素可能很大,也可能有负数。题目给的数值范围,极端情况下total - x可能超出int的表示范围,所以建议用long long来存储总和以及计算过程中的中间值。我在 C++ 里就习惯性地把total定义成long long,省得边界数据一上来就溢出。

3. 完整实现与逐行拆解:一份不会写错的参考代码

3.1 C++ 实现

下面是我实际提交通过的版本,关键逻辑都写了注释:

class Solution { public: int getLargestOutlier(vector<int>& nums) { unordered_map<int, int> freq; long long total = 0; // 第一遍扫描:统计频率,计算总和 for (int v : nums) { freq[v]++; total += v; } int ans = -1; // 如果不存在合法离群值,返回 -1 // 枚举每个元素,假设它是离群值 for (int x : nums) { long long remain = total - x; // remain 必须是偶数,否则 2 * S = remain 无整数解 if (remain % 2 != 0) continue; int y = remain / 2; // 可能的和元素值 // y 必须出现在数组里 if (freq.find(y) == freq.end()) continue; // 如果 y 和 x 数值相同,必须保证数组里至少有两个这样的值 // 因为和元素与离群值在位置上必须是两个不同的元素 if (y == x && freq[y] < 2) continue; ans = max(ans, x); } return ans; } };

最容易被忽略的是最后那个if (y == x && freq[y] < 2)。如果没有这个判断,像[1, 1, 1]这样的用例就会出问题。

逐个推一下[1, 1, 1]

  • total = 3
  • 枚举第一个1作为离群值,remain = 2y = 1
  • y在频率表里,且频率为 3,但当前枚举的x = 1
  • 频率至少为 2,说明数组中确实存在另一个1可以作为和元素,因此离群值1合法。

再看[1, 2, 3]

  • total = 6
  • 枚举x = 1remain = 5,奇数,跳过;
  • 枚举x = 2remain = 4y = 2,但y == xfreq[2] == 1,不合法;
  • 枚举x = 3remain = 3,奇数,跳过;
  • 返回-1,符合预期。

3.2 Python 实现

Python 版本逻辑一模一样,用Counter会非常简洁:

from collections import Counter class Solution: def getLargestOutlier(self, nums: List[int]) -> int: freq = Counter(nums) total = sum(nums) ans = -1 for x in nums: remain = total - x if remain % 2 != 0: continue y = remain // 2 if y not in freq: continue if y == x and freq[y] < 2: continue ans = max(ans, x) return ans

两个版本的核心都是:枚举离群值 → 计算目标和元素值 → 用频率表判断合法性。代码量很少,但每一步都有明确的数学依据,不容易写错。

3.3 为什么用频率表而不是 set 或排序

你可能会有疑问:判断“某个值是否存在”,用set不就行了吗?为什么非要频率表?

因为存在“离群值和和元素值相同”的情况。如果用set[1, 2, 1]这种用例中,枚举x = 1时,y = 1set里存在,我们无法判断这个1是不是就是当前枚举的这个位置。万一数组只有一个1,它既当离群值又当和元素,就乱套了。

频率表记录的是“值出现了几次”,所以能精确回答:除了当前正在枚举的这个元素,数组中是否还有另一个相同的值可以作为和元素。这是set做不到的。

那排序能不能做?可以,排序之后配合某种查找方式也能判断,但没必要。排序本身是O(n log n),而哈希表方案是严格的O(n)。在面试或者周赛这种场景下,哈希表方案更直接、更好解释。

我把三种方案的对比整理了一下:

方案时间复杂度空间复杂度能否处理 y == x 的情况评价
双重循环暴力O(n^2)O(1)容易处理数据量一大就超时
排序 + 二分查找O(n log n)O(1)需要额外处理频率可行,但没必要
哈希表计数O(n)O(n)频率判断即可最优,推荐

4. 真正容易翻车的边界:频率、奇偶性、负值

这道题提交时真正会卡住人的,不是主逻辑,而是几个边界细节。我在测试和实际提交过程中,至少踩过三个坑,下面一个一个说。

4.1 离群值与和元素值相同的情况

先看一个最简单的用例:[1, 1, 2]

  • total = 4
  • 枚举x = 1remain = 3,奇数,跳过;
  • 枚举x = 1(第二个),同样跳过;
  • 枚举x = 2remain = 2y = 1
  • y != x,频率表里1出现两次,合法;
  • 答案是2

这个用例没什么问题。真正容易出错的是[1, 1, 1]这种所有元素都相同的情况。如果不加y == x && freq[y] < 2这个条件,第一次枚举x = 1时就会把1当成合法离群值,这本身没问题,但如果数组是[1, 2, 2]这样,只枚举到x = 2,而数组里只有两个2,其中一个被当成离群值后,另一个确实可以作为和元素,这是合法的。

关键在于:我们要找的“和元素”和“离群值”在位置上必须是两个不同的元素,值相同没关系,但位置不能重合。频率表能直接体现这一点。

我建议在写代码时,把这段判断单独拎出来写成注释,因为它是整个算法里最容易在复查时被误删的逻辑。

4.2total - x为奇数时的处理

题目里的元素都是整数,所以2 * S必然是偶数。也就是说,如果total - x是奇数,那么x不可能成为合法离群值,直接跳过即可。

这个判断放在循环体的最前面,能省掉后面一半的计算量,也让逻辑更清晰。

举个例子,数组[1, 2, 3]total = 6

  • 枚举x = 1remain = 5,奇数,跳过;
  • 枚举x = 3remain = 3,奇数,跳过。

如果没有这个判断,remain / 2在 C++ 里做整数除法会得到错误结果,比如5 / 2 = 2,而实际上2 * 2 = 4 != 5,就会误判。Python 的/会得到浮点数,又可能引入精度问题,所以这个奇偶判断不是可选项,而是必选项。

4.3 负数元素带来的干扰

数组允许负数,比如[-2, -2, 2]。这时候total = -2

让我推一遍:

  • 枚举x = -2remain = 0y = 00不在数组里,跳过;
  • 枚举x = -2(第二个),同样跳过;
  • 枚举x = 2remain = -4y = -2,频率表里有,且y != x,合法;
  • 答案是2

能够正常工作。但有两点要提醒初学者:

第一,remainy都可能是负数,所以freq.find(y)查的是负数值的频率,这个没问题,因为哈希表支持任意整数值作为键。

第二,初始化ans = -1可能会导致一个语义上的误会:如果合法离群值恰好也是-1,返回值依然是-1,和“不存在”混淆了。不过按照题意,元素范围里完全可能出现-1,这时候更稳妥的做法是把ans初始化为INT_MIN,然后用一个bool变量记录是否找到了合法值。

我实际写的时候更习惯这样:

int ans = INT_MIN; bool found = false; for (int x : nums) { // ... 判断逻辑 if (合法) { ans = max(ans, x); found = true; } } return found ? ans : -1;

这样就不会被“离群值恰好是 -1”这种巧合干扰。虽然力扣这题的官方用例里似乎没有针对这个点的极端测试,但面试时写成这样更严谨。

4.4 极端小规模用例验证

n = 3时,整个数组形如[S, S, x],因为此时只有一个特殊数字,它的总和S等于它自身,和元素的值也必须是S

来几个用例:

  • [2, 2, 1]total = 5,枚举x = 1remain = 4y = 2,合法,答案1
  • [0, 0, 0]total = 0,枚举x = 0remain = 0y = 0,频率是 3,y == x但频率大于等于 2,合法,答案0
  • [1, 2, 4]total = 7,枚举x = 1remain = 6y = 3,不存在;枚举x = 2remain = 5,奇数;枚举x = 4remain = 3,奇数;返回-1

这两个用例能过,说明逻辑基本闭环。

5. 从这题延伸出去:同类变形与刷题/面试的发挥点

5.1 如果题目换个问法,代码怎么改

这题常见的变体有几种:

  • 找最小离群值:把ans = max(ans, x)改成ans = min(ans, x),同时注意初始化值为INT_MAX
  • 统计合法离群值的个数:把ans改成计数器,每次合法就cnt++
  • 要求离群值必须大于等于某个阈值:在更新答案前再加一个判断。

这些改动都不影响核心算法,因为枚举和判断的逻辑完全不变。这也说明,理解“等式”比背代码重要得多。

5.2 这类“等式 + 枚举一个变量 + 查另一个变量”的问题模型

3371 不是唯一用这种模型解决的题。我随便就能举出几个:

  • 两数之和:枚举nums[i],查target - nums[i]是否在哈希表里;
  • 连续子数组和为 k:枚举右端点,维护前缀和的哈希表,查prefix_sum - k出现过几次;
  • 分割等和子集:先求总和的一半作为目标值,再转化为 0/1 背包或子集和问题。

它们的共同点是:用一个等式把一个“配对问题”变成“存在性查询问题”。而“枚举右维护左”这个框架,就是这套思维的概括。

顺带说一句,很多看起来需要“贪心”、“二分”、“排序”的题目,其实底层也可以用这个模型去推导。比如某些贪心题,先排序之后枚举一个端点、用某种数据结构维护另一侧的最优值,本质上也是“枚举右维护左”。学会这套框架之后,后面遇到新题会少吃很多苦头。

5.3 面试时如何一步步引导出这个解法

如果面试官抛出这道题,我会建议按这样的顺序来沟通:

  1. 先复述题目,确认“特殊数字”“和元素”“离群值”这三个概念;
  2. 在纸上写出total = 2*S + x这个等式,并向面试官解释“移项变形”的思路;
  3. 提出最直观的暴力解法:枚举离群值,再枚举和元素,O(n^2)
  4. 指出可以用频率表把查找优化到O(1)
  5. 重点讨论y == x时的频率判断,以及total - x为奇数时的跳过逻辑。

整个过程不需要背代码,只需要把等式讲清楚,代码自然就出来了。

我个人在实际刷题中的体会是:这道题最大的价值不是考你会不会哈希表,而是考你有没有“先把题目翻译成公式”的意识。很多人卡住,不是因为不会哈希表,而是因为从头到尾都在用自然语言理解条件,没有迈出“移项变形”这一步。最后再分享一个小技巧:如果你在草稿纸上把total = 2*S + x写出来,然后再去枚举,整个思路会顺很多。反过来,如果你先写了代码再想为什么,大概率会在边界条件上反复试探。先公式,后代码,能省一半调试时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 6:38:36

MATLAB电-气耦合系统CVaR-DRO备用优化建模

简介&#xff1a;本资源是一套面向能源系统优化研究者与电力/气网联合调度方向研究生的MATLAB仿真代码&#xff0c;聚焦电-气综合能源系统在不确定性下的能量与备用联合调度问题。代码完整复现SCI期刊《Energy and Reserve Dispatch with Distributionally Robust Joint Chance…

作者头像 李华
网站建设 2026/9/10 6:37:49

从UIView到ViewGroup:iOS转Android的心智模型重装指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 6:35:28

数学可视化工具选型指南:按场景挑工具,一张表看完

数学可视化工具选型指南&#xff1a;按场景挑工具&#xff0c;一张表看完 【免费下载链接】awesome-math A curated list of awesome mathematics resources 项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-math 公式和符号堆在一起时&#xff0c;抽象概念很…

作者头像 李华
网站建设 2026/9/10 6:34:25

Hot100数组题全攻略:双指针、前缀和与哈希表套路详解

数组算是我在力扣Hot 100这个题库里认真啃下来的第一个专题。刚开始真没当回事&#xff0c;觉得数组不就是for循环加下标访问&#xff0c;能难到哪里去&#xff1f;直到有一次面试&#xff0c;被一道“和为K的子数组”问得当场卡壳&#xff0c;我才意识到数组题型远没有想象中简…

作者头像 李华
网站建设 2026/9/10 6:34:02

购物商城APP源码解读:从Android Studio导入到答辩演示全流程

简介&#xff1a;面向毕业设计和大作业场景的Android购物商城APP完整源码&#xff0c;基于Android Studio开发&#xff0c;覆盖注册登录、修改密码、重置密码&#xff08;邮箱验证&#xff09;、商品详情加载、购物车、个人信息修改等功能模块&#xff0c;适合正在深入学习Andr…

作者头像 李华
网站建设 2026/9/10 6:33:20

CANN/GE性能剖析特性介绍

GE Profiling 特性介绍 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、Ten…

作者头像 李华