1. 问题背景与核心思路
这道题目来自力扣的贪心算法分类,编号1005。题目要求我们对一个整数数组进行最多K次取反操作(每次可以选择任意元素取反),最终使得数组的和最大化。这看似简单的操作背后,其实考察了对贪心算法本质的理解和灵活应用。
贪心算法的核心在于"局部最优导致全局最优"。在这个问题中,我们需要找到一系列局部最优的取反决策,使得最终数组和达到最大。关键在于理解什么时候取反、对哪个元素取反才是最优选择。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 算法设计与实现步骤
2.1 排序预处理
首先对数组进行排序是解决这个问题的关键第一步。排序后我们可以更方便地找到绝对值最小的元素,这是贪心策略的基础:
java复制Arrays.sort(nums);
排序的时间复杂度是O(nlogn),这是整个算法的主要时间消耗点。为什么要排序?因为我们需要优先处理绝对值较大的负数,这样取反后能带来最大的收益。
2.2 贪心取反策略
具体实现时,我们采用以下策略:
- 从左到右遍历数组(此时数组已排序)
- 遇到负数就取反,同时K减1
- 当K还有剩余时,反复对当前最小元素取反
java复制for (int i = 0; i < nums.length && K > 0; i++) {
if (nums[i] < 0) {
nums[i] = -nums[i];
K--;
}
}
2.3 处理剩余的K次操作
如果遍历完所有负数后K仍然大于0,我们需要处理剩余的操作次数。此时所有数都是非负数,我们应该反复对最小的数进行取反:
java复制if (K % 2 == 1) {
Arrays.sort(nums);
nums[0] = -nums[0];
}
这里有个优化点:我们只需要知道K的奇偶性。因为对一个数取反两次等于没操作,所以当K为偶数时可以忽略,奇数时只需要取反一次。
3. 完整Java实现
下面是完整的Java实现代码,包含了上述所有步骤:
java复制import java.util.Arrays;
class Solution {
public int largestSumAfterKNegations(int[] nums, int K) {
// 第一次排序:将数组按升序排列
Arrays.sort(nums);
// 第一次遍历:优先取反负数
for (int i = 0; i < nums.length && K > 0; i++) {
if (nums[i] < 0) {
nums[i] = -nums[i];
K--;
}
}
// 如果还有剩余K,且K为奇数,取反最小元素
if (K > 0 && K % 2 == 1) {
Arrays.sort(nums);
nums[0] = -nums[0];
}
// 计算最终和
int sum = 0;
for (int num : nums) {
sum += num;
}
return sum;
}
}
4. 复杂度分析与优化
4.1 时间复杂度
- 排序操作:O(nlogn)
- 线性扫描:O(n)
- 可能的第二次排序:O(nlogn)
最坏情况下时间复杂度为O(nlogn),主要由排序操作决定。
4.2 空间复杂度
我们只使用了常数级别的额外空间,因此空间复杂度是O(1)。
4.3 优化思路
可以优化掉第二次排序,只需要在第一次遍历时记录最小元素:
java复制int minPos = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] < nums[minPos]) {
minPos = i;
}
}
// 然后直接对nums[minPos]进行操作
这样可以将时间复杂度稳定在O(nlogn),避免最坏情况下的两次排序。
5. 边界条件与测试用例
5.1 常见边界情况
- 全为正数的数组
- 全为负数的数组
- K大于数组长度的情况
- K为0的情况
- 数组包含0的情况
5.2 测试用例示例
java复制// 测试用例1:常规情况
int[] nums1 = {4,2,3};
int K1 = 1;
// 预期输出:5 (取反2得到[4,-2,3],和为5)
// 测试用例2:全负数
int[] nums2 = {-2,-3,-1};
int K2 = 4;
// 预期输出:4 (先取反所有负数得到[2,3,1],剩余1次操作取反1)
// 测试用例3:包含0
int[] nums3 = {0,1,-2};
int K3 = 2;
// 预期输出:3 (先取反-2得到[0,1,2],剩余1次操作对0取反不影响)
6. 贪心算法的正确性证明
为什么这种贪心策略能得到最优解?我们可以从以下几个方面证明:
- 对于负数,取反它们总是能增加总和,所以应该优先取反绝对值大的负数(这样增加的总和最多)
- 当只剩下正数时,反复取反最小的数可以使得总和的减少最小化
- 如果数组中有0,任何剩余的操作次数都可以用在0上而不影响总和
这种策略确保了每次操作都带来最大的收益或最小的损失,因此最终结果是最优的。
7. 实际应用场景
这类问题在实际中有多种应用场景:
- 资源分配问题:如何在有限次数的调整下最大化收益
- 投资组合优化:在有限次交易中调整资产配置
- 信号处理:对信号进行有限次的反相操作以达到最佳效果
理解这类贪心算法问题有助于培养解决实际优化问题的思维能力。
8. 常见错误与调试技巧
8.1 常见错误
- 忘记处理K为奇数的情况
- 在第二次排序时没有考虑数组可能已经改变
- 没有正确处理全为正数且K为奇数的情况
8.2 调试技巧
- 打印每次操作后的数组状态
- 特别关注K的奇偶性变化
- 对于边界情况单独测试
例如可以添加调试输出:
java复制System.out.println("After initial sort: " + Arrays.toString(nums));
System.out.println("After negation pass: " + Arrays.toString(nums));
System.out.println("Final array: " + Arrays.toString(nums));
9. 算法变种与扩展
这个问题可以有多种变体:
- 限制每次只能取反相邻元素
- 每次取反有成本,在总成本限制下最大化数组和
- 多维数组的取反问题
理解基础问题的解法后,可以尝试解决这些更复杂的变种。
10. 性能对比与语言特性
在Java中实现这个算法时,有几个性能考虑点:
Arrays.sort()对于原始类型使用快速排序,对于对象使用归并排序- 对于小数组(长度<47),Java会使用插入排序
- 在热点代码中,JIT编译器会优化循环操作
如果处理特别大的数组,可能需要考虑更高效的排序算法或并行处理。但在力扣的题目约束下,当前的实现已经足够高效。
