1. 问题背景与题目解析
今天我们来拆解LeetCode第487场周赛的第二题——"3828. 删除子数组后的最终元素"。这是一道关于数组操作的题目,考察选手对子数组删除操作的理解以及最终结果的计算能力。题目描述虽然简短,但蕴含着几个需要仔细思考的关键点。
题目大意是:给定一个整数数组,我们可以执行一次操作——删除其中的一个子数组(连续元素序列)。要求返回删除后数组中剩下的第一个和最后一个元素的最小可能差值。换句话说,我们需要找到一种子数组删除方式,使得操作后数组的首尾元素差值最小。
举个例子,假设数组是[1,3,2,4,5],如果我们删除子数组[3,2],剩下的数组就是[1,4,5],首尾元素差值是5-1=4。如果我们删除子数组[2,4],剩下的数组是[1,3,5],差值是5-1=4。如果我们删除整个数组中间部分[3,2,4],剩下[1,5],差值就是4。看起来这个例子的最小差值就是4。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 关键思路分析
2.1 问题转化与简化
这道题的核心在于理解"删除一个子数组"对最终结果的影响。我们需要明确几个关键点:
- 删除的子数组必须是连续的,这是子数组的定义
- 删除操作只执行一次
- 我们需要关注的是删除后数组的第一个和最后一个元素的差值
首先,我们可以考虑极端情况:
- 不删除任何元素(即删除空子数组):此时数组完整保留,结果就是原数组首尾差值
- 删除整个数组(除了第一个或最后一个元素):此时数组只剩下一个元素,差值为0
- 删除数组中间的一部分:需要计算各种可能的删除方式
2.2 可能的情况分类
经过分析,我们可以将可能的删除情况分为三类:
- 删除数组的前缀:即从开头删除若干连续元素
- 删除数组的后缀:即从末尾删除若干连续元素
- 删除数组的中间部分:即保留开头和结尾的一部分
对于每种情况,我们需要计算删除后数组的首尾差值,然后取所有可能情况中的最小值。
3. 算法设计与实现
3.1 暴力解法思路
最直观的方法是尝试所有可能的子数组删除方式,然后计算每种方式后的首尾差值,最后取最小值。这种方法的时间复杂度是O(n²),因为对于长度为n的数组,有O(n²)种可能的子数组。
具体步骤:
- 遍历所有可能的子数组起始和结束位置
- 对于每个子数组,构造删除后的新数组
- 计算新数组的首尾差值
- 记录所有差值中的最小值
这种方法在小规模数据上可行,但对于大规模数据(比如n=10^5)就会超时。
3.2 优化思路
我们需要寻找更高效的算法。观察题目要求,我们实际上只需要关注删除操作对数组首尾元素的影响。具体来说:
- 如果我们不删除任何元素,差值就是nums[0] - nums[-1]的绝对值
- 如果我们删除前缀,即保留从i开始的子数组,那么首尾差就是nums[i] - nums[-1]的绝对值
- 如果我们删除后缀,即保留从开始到j的子数组,那么首尾差就是nums[0] - nums[j]的绝对值
- 如果我们删除中间部分,即保留开头一段和结尾一段,那么首尾差就是nums[0] - nums[k]的绝对值,其中k是后半部分的起始位置
因此,我们实际上只需要考虑以下几种情况的最小值:
- 原始首尾差
- 首元素与数组末尾某个元素的最小差
- 尾元素与数组开头某个元素的最小差
- 首元素与尾元素的最小差(当删除整个中间部分时)
3.3 最优算法实现
基于上述分析,我们可以得出O(n)时间复杂度的算法:
- 计算原始首尾差original_diff = abs(nums[0] - nums[-1])
- 查找nums[0]与nums中最后一个元素之后所有元素的最小差prefix_diff
- 查找nums[-1]与nums中第一个元素之前所有元素的最小差suffix_diff
- 考虑删除整个中间部分的情况,即只保留首尾元素,diff = 0
- 取上述所有情况中的最小值
具体实现时,我们可以:
- 从数组开头向后遍历,计算nums[i]与nums[0]的差,记录最小值
- 从数组末尾向前遍历,计算nums[i]与nums[-1]的差,记录最小值
- 比较这些最小值与0,取其中的最小者
4. 代码实现与解释
4.1 Python实现
python复制def minimumDifference(nums):
n = len(nums)
if n == 1:
return 0
# 情况1:不删除任何元素
original_diff = abs(nums[0] - nums[-1])
# 情况2:删除某个前缀,保留从i到末尾
min_prefix_diff = float('inf')
for i in range(n):
diff = abs(nums[i] - nums[-1])
if diff < min_prefix_diff:
min_prefix_diff = diff
# 情况3:删除某个后缀,保留从开始到j
min_suffix_diff = float('inf')
for j in range(n):
diff = abs(nums[0] - nums[j])
if diff < min_suffix_diff:
min_suffix_diff = diff
# 情况4:删除中间部分,只保留首尾
only_ends_diff = 0
# 取所有情况的最小值
return min(original_diff, min_prefix_diff, min_suffix_diff, only_ends_diff)
4.2 代码优化
上述代码可以进行一些优化,减少不必要的计算:
python复制def minimumDifference(nums):
n = len(nums)
if n == 1:
return 0
min_prefix = min(abs(nums[i] - nums[-1]) for i in range(n))
min_suffix = min(abs(nums[0] - nums[j]) for j in range(n))
return min(abs(nums[0] - nums[-1]), min_prefix, min_suffix, 0)
4.3 复杂度分析
- 时间复杂度:O(n),我们只需要线性扫描数组两次(计算min_prefix和min_suffix)
- 空间复杂度:O(1),只使用了常数个额外变量
5. 边界情况与测试用例
5.1 常见边界情况
- 单元素数组:应该返回0
- 两元素数组:最小差值就是两元素的差,或者删除其中一个元素后差值为0
- 所有元素相同:无论怎么删除,差值都是0
- 升序或降序数组:需要考虑删除前缀或后缀的影响
5.2 测试用例示例
python复制# 测试用例1:常规情况
assert minimumDifference([1,3,2,4,5]) == 0
# 测试用例2:单元素数组
assert minimumDifference([5]) == 0
# 测试用例3:两元素数组
assert minimumDifference([2,5]) == 0
# 测试用例4:所有元素相同
assert minimumDifference([3,3,3,3]) == 0
# 测试用例5:升序数组
assert minimumDifference([1,2,3,4,5]) == 0
# 测试用例6:降序数组
assert minimumDifference([5,4,3,2,1]) == 0
6. 解题心得与技巧
在解决这类数组操作题目时,有几点经验值得分享:
- 先考虑极端情况:比如不删除、全删除、删除单边等情况,这往往能帮助发现规律
- 可视化操作:在纸上画出数组和不同的删除方式,有助于理解问题本质
- 寻找问题转化:这道题的关键是将"删除子数组"转化为对首尾元素的影响
- 边界条件检查:特别是数组长度为1或2的情况,往往有特殊处理
- 从暴力法入手:即使知道暴力法不够高效,先实现暴力解法可以帮助验证思路的正确性
对于竞赛题目,时间有限的情况下,建议:
- 先用小例子手动模拟,理解题意
- 思考是否有O(n)或O(nlogn)的解法,因为O(n²)通常无法通过大规模测试
- 先写出主要逻辑,再处理边界情况
- 提交前用几个简单测试用例验证
这道题的一个关键insight是:最小差值要么是0(当可以只保留首或尾元素时),要么是原始首尾差,或者是首元素与某个尾元素的差/尾元素与某个首元素的差。通过这种分析,我们避免了考虑所有可能的子数组删除情况,大大降低了问题复杂度。
