十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LeetCode 908 最小差值 I:从极值调整到数学公式的算法思维

LeetCode 908 最小差值 I:从极值调整到数学公式的算法思维 如果你在刷 LeetCode 时看到“最小差值 I”这种题目第一反应是不是觉得这题太简单甚至有点“送分”的感觉先别急着划走。这道力扣第 908 题表面上是考察数组和简单数学运算但它真正考验的是你在面对一个看似明确的算法问题时能否快速抓住问题的数学本质并写出既简洁又高效的代码。很多人在面试或周赛中恰恰是在这种“简单题”上栽了跟头要么想复杂了导致超时要么忽略了边界条件导致结果错误。本文将带你深入拆解“最小差值 I”这道题。我们不止步于给出一个能通过的答案而是要彻底弄明白为什么这道题被归类为“简单”它的核心考点究竟是什么如何从最朴素的思路出发一步步推导出那个最优的、时间复杂度为 O(n) 的解法更重要的是我们将通过 Python 代码展示如何将数学洞察力转化为优雅的编程实践并探讨这类“极值调整”问题背后的通用解题框架。读完本文你将获得对 LeetCode 908 题“最小差值 I”的完整、透彻的理解。一个从问题分析到代码实现的清晰思维路径。掌握利用 Python 内置函数max,min高效解决极值问题的技巧。了解如何正确处理算法题中的边界条件与数学推理。获得可复用的解题模板应对同类型“调整范围求极值”的题目。1. 问题重述与核心洞察首先我们准确理解题目LeetCode 908. 最小差值 I。题目描述 给你一个整数数组nums和一个整数k。你可以对数组中的每个元素进行一次操作将该元素加上-k到k之间的任意一个整数包含-k和k。你的目标是通过这样的操作使得数组中的最大值和最小值之间的差值即score尽可能小。你需要返回这个最小的可能差值。简单翻译 我们有一个原始数组。对于数组里的每一个数我们都可以给它加上一个在[-k, k]范围内的任意整数每个数加的值可以不同。加完之后会得到一个新数组。这个新数组的最大值减去最小值就是我们要计算的“分数”。我们的任务是通过精心选择每个数所加的值让这个“分数”尽可能的小。最后返回这个最小的分数。关键约束1 nums.length 10^40 nums[i] 10^40 k 10^4核心洞察 这道题被标记为“简单”其关键就在于它有一个非常强的数学性质。我们不需要模拟给每个数加什么值的过程。思考一下我们的操作可以抬高一个数加正数也可以压低一个数加负数。目标是让最终的最大值尽可能小同时让最终的最小值尽可能大这样它们的差值才会小。对于原始数组的最大值max_val我们最好的策略是把它压低最多能压低k所以它最低能变成max_val - k。对于原始数组的最小值min_val我们最好的策略是把它抬高最多能抬高k所以它最高能变成min_val k。如果压低后的最大值(max_val - k)仍然大于抬高后的最小值(min_val k)那么无论我们怎么调整中间的其他数字最终数组的差值至少是(max_val - k) - (min_val k)。如果压低后的最大值已经小于等于抬高后的最小值这意味着我们可以通过调整让所有数字最终落在同一个值附近从而使差值变为0。因此问题的答案就简化为一个公式max(0, (max_val - min_val) - 2 * k)这个公式就是本题的全部精髓。它直接将一个看似需要遍历和决策的问题转化为了一个基于数组极值的常数时间计算。理解了这个推导过程代码写起来就非常简单了。2. 从暴力思路到最优解法的思维推导很多同学在刚开始解题时可能会陷入一些思维误区。让我们走一遍完整的思考过程这比直接背答案更有价值。2.1 误区一模拟所有可能的加法组合这是最直观但最不可行的想法。数组长度最多 10^4每个数有(2k1)种加法选择。组合数是指数级的完全不可能计算。2.2 误区二排序后贪心调整有人可能会想先排序然后尽量让两头的数字向中间靠拢。这个想法方向是对的但依然复杂。因为排序需要 O(n log n) 的时间而本题存在 O(n) 的解法。更重要的是我们不需要知道中间数字的具体情况。2.3 正确的思维路径聚焦于极值让我们一步步推理定义目标设最终数组的最大值为final_max最小值为final_min。目标是最小化final_max - final_min。建立联系final_max是由原始数组中的某个数nums[i]加上某个值x_i-k x_i k得到的。显然final_max max(nums) k因为最大值最多被抬高 k。同理final_min min(nums) - k因为最小值最多被压低 k。寻找下界我们无法控制是哪个数变成了最终的最大值或最小值。但我们可以确定一个绝对下界final_max - final_min (max(nums) - k) - (min(nums) k) max(nums) - min(nums) - 2k。这是因为最终的最大值至少是max(nums)-k最坏情况我们把最大值压到最低最终的最小值至多是min(nums)k最坏情况我们把最小值抬到最高。这两个“最坏情况”值之间的差就是差值可能的最小值。处理非负性上面的差值可能是个负数比如当k很大时max(nums)-min(nums)-2k 0。这意味着我们甚至可以把最大值压得比最小值还低从而让所有数相等差值为 0。所以最终答案应该是max(0, max(nums) - min(nums) - 2k)。这个推导过程清晰地告诉我们数组中除了最大值和最小值其他所有数字的操作都不会影响最终差值的最小可能下界。它们的存在只是为了填充最大值和最小值之间的“空隙”但无法突破由最大值和最小值设定的边界。3. 环境准备与 Python 基础在编写代码前确保你有一个可以运行 Python 的环境。本题对环境要求极低。Python 版本Python 3.6 及以上均可。本题解使用 Python 3.8 语法但兼容所有主流版本。开发工具任何文本编辑器如 VSCode, PyCharm, Sublime Text或直接在 LeetCode 网页编辑器、Jupyter Notebook 中编写均可。所需知识基本的 Python 列表操作。内置函数max()和min()的使用。基本的数学运算。如果你还没有安装 Python可以参考以下简要步骤以 Windows 为例访问 Python 官网下载安装包。运行安装程序务必勾选 “Add Python to PATH”。安装完成后打开命令提示符CMD或 PowerShell输入python --version确认安装成功。4. 代码实现与逐行解析理解了核心公式代码实现就变得异常简单。我们将提供两种风格的代码一种是直接在 LeetCode 答题模板中编写的函数另一种是包含完整测试用例的本地脚本。4.1 解法一直接应用公式推荐这是最简洁、最高效的解法。class Solution: def smallestRangeI(self, nums: List[int], k: int) - int: 计算在给定操作下数组最大值与最小值的最小可能差值。 参数: nums (List[int]): 输入的整数数组。 k (int): 允许对每个元素加上的整数的范围限制-k 到 k。 返回: int: 可能的最小差值。 # 步骤1: 找到原始数组的最大值和最小值 max_val max(nums) min_val min(nums) # 步骤2: 应用核心公式 # 理论最小差值 (最大值 - 最小值) - 2 * k # 但如果这个值小于0说明我们可以让所有数相等差值为0 potential_diff max_val - min_val - 2 * k # 步骤3: 返回差值确保非负 return max(0, potential_diff)代码解析max(nums)和min(nums)Python 内置函数分别以 O(n) 的时间复杂度遍历数组一次找到最大值和最小值。注意虽然这里调用了两个函数但 Python 3.11 在某些优化下可以更高效总体复杂度仍是 O(n)。potential_diff max_val - min_val - 2 * k这行代码计算了理论上的最小可能差值。如果k足够大这个值可能是负数。return max(0, potential_diff)使用max函数确保返回值不为负数。如果potential_diff为负说明我们可以使最终数组的所有元素相等此时最小差值为 0。复杂度分析时间复杂度O(n)其中 n 是数组nums的长度。我们只遍历了数组两次max和min各一次且是线性关系。空间复杂度O(1)。只使用了常数级别的额外空间几个变量。4.2 解法二一步到位更简洁的写法如果你追求极致的代码简洁可以写成一行class Solution: def smallestRangeI(self, nums: List[int], k: int) - int: return max(0, max(nums) - min(nums) - 2 * k)这种写法在功能上和解法一完全等价直接体现了问题的数学本质。在面试或竞赛中写出这样的代码并能清晰解释其原理会非常加分。4.3 本地测试完整脚本为了更好的理解和调试我们可以在本地编写一个完整的测试脚本#!/usr/bin/env python3 # -*- coding: utf-8 -*- LeetCode 908. 最小差值 I 本地测试脚本 def smallestRangeI(nums, k): 功能同上述Solution类中的方法。 return max(0, max(nums) - min(nums) - 2 * k) def test(): 测试用例组 test_cases [ # (输入数组, k, 期望输出) ([1], 0, 0), # 单元素数组差值必为0 ([0, 10], 2, 6), # 原始差10k2理论最小差10-46 ([1, 3, 6], 3, 0), # 原始差5k32*k6 5可调为0 ([2, 7, 2], 1, 3), # 原始差5k1理论最小差5-23 ([3, 1, 10], 4, 0), # 原始差9k42*k8 9但9-810等等算一下max10, min1, diff9, 9-2*41返回max(0,1)1。我期望写错了应该是1。我们修正一下。 ] # 修正第三个测试用例的期望值 test_cases[2] ([1, 3, 6], 3, 0) # 1,3,6 - max6, min1, diff5, 5-6-1 - max(0,-1)0正确。 # 修正第五个测试用例的期望值 test_cases[4] ([3, 1, 10], 4, 1) # max10, min1, diff9, 9-2*41 - max(0,1)1。 print(开始测试 LeetCode 908. 最小差值 I) print( * 50) all_passed True for i, (nums, k, expected) in enumerate(test_cases): result smallestRangeI(nums, k) if result expected: print(f测试用例 {i1} 通过: nums{nums}, k{k}, 结果{result} (期望{expected})) else: print(f测试用例 {i1} 失败: nums{nums}, k{k}, 结果{result} (期望{expected})) all_passed False print( * 50) if all_passed: print(所有测试用例通过) else: print(部分测试用例失败请检查代码。) return all_passed if __name__ __main__: test()运行这个脚本你会看到以下输出开始测试 LeetCode 908. 最小差值 I 测试用例 1 通过: nums[1], k0, 结果0 (期望0) 测试用例 2 通过: nums[0, 10], k2, 结果6 (期望6) 测试用例 3 通过: nums[1, 3, 6], k3, 结果0 (期望0) 测试用例 4 通过: nums[2, 7, 2], k1, 结果3 (期望3) 测试用例 5 通过: nums[3, 1, 10], k4, 结果1 (期望1) 所有测试用例通过5. 算法正确性证明与边界条件分析一个健壮的算法必须经得起各种边界情况的考验。我们来系统性地分析一下。5.1 算法正确性证明我们的算法核心是max(0, max(nums) - min(nums) - 2 * k)。证明可达性我们总可以这样操作让原最大值max_val减去k让原最小值min_val加上k。对于其他任意元素x我们总可以找到一个在[-k, k]范围内的数delta使得x delta落在区间[min_val k, max_val - k]内因为x本身就在[min_val, max_val]内。因此最终所有数都可以落在这个区间其宽度为(max_val - k) - (min_val k) max_val - min_val - 2k。如果这个值非负它就是可达的最小差值。最优性假设存在一种操作方式使得最终差值d max(0, max_val - min_val - 2k)。如果max_val - min_val - 2k 0那么max(0, ...) 0。d 0是不可能的因为差值是非负的。所以d不可能更小。如果max_val - min_val - 2k 0那么max(0, ...) max_val - min_val - 2k。假设d更小。那么最终数组的最大值final_max必须更小或者最小值final_min必须更大或者两者兼有。但是final_max max_val - k最大值最多被压低 kfinal_min min_val k最小值最多被抬高 k。所以final_max - final_min (max_val - k) - (min_val k) max_val - min_val - 2k。这与d更小的假设矛盾。 因此我们的算法给出的值就是全局最优解。5.2 边界条件与特殊输入边界情况输入示例算法处理结果说明数组长度为1nums[5], k任意值max(0, 5-5-2*k) max(0, -2k)0单个元素最大值等于最小值无论怎么操作差值恒为0。k0nums[1,5,9], k0max(0, 9-1-0) 88不允许任何调整差值就是原数组极差。k非常大nums[1,100], k100max(0, 100-1-200) max(0, -101)0调整范围足够大可以将最大值压到1以下最小值抬到100以上使所有数相等。极差刚好等于2knums[0,10], k5max(0, 10-0-10) 00临界情况刚好可以将最大值压到5最小值抬到5使差值变为0。元素值全相同nums[7,7,7], k3max(0, 7-7-6) 00原始差值已为0操作后差值仍为0。k为0且数组有负值nums[-5, -1, 0], k0max(0, 0-(-5)-0)55算法不关心数值正负只关心极差。max和min函数能正确处理负数。从表格可以看出我们的算法简洁地处理了所有边界情况这得益于max(0, ...)这个操作它自动将负值截断为0符合“差值非负”的物理意义。6. 常见错误与思维陷阱即使理解了算法在实现时也可能遇到一些陷阱。下面列举几个常见的错误6.1 错误一误解操作对象错误理解认为只能对所有元素加上同一个值。导致错误可能会尝试寻找一个最优的偏移量x使得(max(nums)x) - (min(nums)x)最小这显然是原极差与k无关。正确理解每个元素可以独立地加上不同的值范围都在[-k, k]。这是降低整体差值的关键。6.2 错误二忽略差值的非负性错误代码def smallestRangeI_wrong(nums, k): return max(nums) - min(nums) - 2 * k # 当k很大时可能返回负数导致错误当k很大使得计算结果为负数时函数返回负值。而数组最大值和最小值的差值不可能为负。修正必须用max(0, ...)包裹。6.3 错误三过度复杂化——使用排序错误代码def smallestRangeI_complex(nums, k): nums.sort() return max(0, nums[-1] - nums[0] - 2 * k)问题虽然结果正确但排序的时间复杂度是 O(n log n)而我们的最优解法是 O(n)。在数据量达到上限10^4时排序比直接求极值慢一个数量级。这在算法竞赛或面试中是不被接受的。6.4 错误四错误计算调整量错误理解认为最大值只能减k最小值只能加k所以差值减少2k。这是正确的。但有人会错误地认为差值减少k。核心最大值和最小值相向而行各自移动k所以它们之间的距离缩短了2k。7. 性能优化与最佳实践尽管本题的 O(n) 解法已经是最优但我们仍可以讨论一些编码和思维上的最佳实践。7.1 利用一次遍历求极值在标准的解题环境中分别调用max()和min()是清晰且高效的做法。但如果你想知道如何手动实现或者在某些限制环境下可以一次遍历同时找到最大值和最小值def smallestRangeI_one_pass(nums, k): if not nums: # 虽然题目保证长度1但这是好习惯 return 0 min_val max_val nums[0] for num in nums[1:]: if num min_val: min_val num elif num max_val: # 使用elif因为一个数不可能同时小于min且大于max max_val num return max(0, max_val - min_val - 2 * k)这种方法将两次遍历合并为一次常数因子更优但代码稍复杂。对于本题两种方式都是 O(n)直接使用内置函数更 Pythonic。7.2 处理超大数组的思考题目约束n 10^4O(n) 算法完全足够。但如果n非常大例如 10^7且数组以数据流形式给出无法全部存入内存那么一次遍历求极值的算法就是必须的。这时我们的算法依然有效只需要在读取流的过程中不断更新max_val和min_val即可。7.3 Pythonic 写法在 Python 中最优雅的写法就是一行流def smallestRangeI(nums, k): return max(0, max(nums) - min(nums) - 2 * k)清晰、简洁、高效充分体现了 Python 的哲学。8. 同类题型扩展与解题模板“最小差值 I”属于“极值调整”或“范围压缩”类问题。掌握其核心思想后可以解决一系列变种问题。8.1 问题特征给你一个数组或一组数字。允许你对每个元素进行有限范围内的调整加/减一个值或进行某种变换。目标是让整个集合的某个指标如极差、方差、和特定目标的距离等最小化或最大化。8.2 解题模板确定目标明确要优化的是什么本题是最终最大值 - 最终最小值。分析操作的影响边界对于要最小化的指标思考如何让“大的变小小的变大”。对于要最大化的指标思考如何让“大的更大小的更小”。确定每个元素经过操作后其值可能处于的范围。聚焦关键元素通常决定整体指标的是边界元素如最大值、最小值。分析这些边界元素在操作下的最有利变化。推导数学公式根据步骤3推导出指标的理论最优值。这个值往往可以直接由原始数据的极值和操作范围计算得出。处理边界与有效性检查理论最优值是否可达例如差值不能为负。如果不可达则可达的最优值是什么8.3 举一反三LeetCode 910. 最小差值 II这是一道与本题相关的“中等”难度题目可以作为很好的练习。题目描述给你一个整数数组nums和一个整数k。你可以对每个元素进行以下两种操作之一加上k或减去k。目标是使数组的最大值和最小值之间的差值最小化。区别本题908题是每个数可以加[-k, k]的任意值。而 910 题是每个数必须同时加k或减k即所有数加的数绝对值相同只是符号可正可负。思路提示910题不能再简单地对最大值减k、最小值加k。因为操作是“全体加k或减k”一个数加了k就不能再减k。这需要更复杂的分析通常需要先对数组排序然后寻找一个分割点分割点前的数都加k分割点后的数都减k再计算这种策略下的最小极差。这涉及到枚举分割点并利用有序性快速计算极值。通过对比这两道题你可以深刻理解“操作自由度”对问题难度和解法的影响。9. 总结与进阶学习建议LeetCode 908 “最小差值 I”是一道经典的、用于区分候选人是否真正理解问题本质的题目。它教会我们在面对算法问题时不要急于编码而应先进行深入的数学分析和逻辑推理。很多时候一个看似需要复杂模拟的问题其背后隐藏着一个简洁的数学公式。本文核心要点回顾问题本质通过独立调整每个元素在[-k, k]范围内最小化数组的极差。关键洞察最优策略是尽力压低原始最大值、抬高原始最小值。其他中间元素总能被调整到介于这两者之间的位置。最终公式最小差值 max(0, max(nums) - min(nums) - 2 * k)。代码实现Python 的一行解max(0, max(nums)-min(nums)-2*k)既简洁又高效。边界处理max(0, ...)自动处理了差值为负的情况使其归零。给你的建议刷题时多思考遇到简单题不要满足于通过。多问自己“为什么可以这样解”“有没有更本质的理解”。这道题就是培养这种思维的绝佳材料。掌握 Python 内置函数max(),min(),sum()等函数在解决数组相关问题时非常高效要熟悉它们。尝试变种问题主动去寻找和练习类似题目如上面提到的 LeetCode 910或者 LeetCode 462最小操作次数使数组元素相等 II它们都涉及对数组元素进行统一或非统一调整以达到某种目标。重视数学推导很多算法问题尤其是标签为“数学”或“贪心”的题目其最优解往往来自于对问题约束的数学转化。培养自己的数学建模能力。希望这篇详细的解析能帮助你不仅解决这一道题更能掌握一类题的思考方法。建议你将本文的推导过程和代码示例收藏在遇到类似“范围调整求极值”的问题时可以快速回顾核心思路。
返回列表