回到课程

最大子数组

重要程度: 2

输入是以数字组成的数组,例如 arr = [1, -2, 3, 4, -9, 6].

任务是:找出所有项的和最大的 arr 数组的连续子数组。

写出函数 getMaxSubSum(arr),用其找出并返回最大和。

例如:

getMaxSubSum([-1, 2, 3, -9]) == 5(高亮项的加和)
getMaxSubSum([2, -1, 2, 3, -9]) == 6
getMaxSubSum([-1, 2, 3, -9, 11]) == 11
getMaxSubSum([-2, -1, 1, 2]) == 3
getMaxSubSum([100, -9, 2, -3, 5]) == 100
getMaxSubSum([1, 2, 3]) == 6(所有项的和)

如果所有项都是负数,那就一个项也不取(子数组是空的),所以返回的是 0:

getMaxSubSum([-1, -2, -3]) = 0

请尝试想出一个快速的解决方案:复杂度可以是 O(n2),有能力达到 O(n) 则更好。

打开带有测试的沙箱。

慢的解决方案

我们可以计算所有可能的子集的和。

最简单的方法就是获取每个元素然后计算从它开始所有子数组的和。

[-1, 2, 3, -9, 11] 为例:

// 从 -1 开始:
-1
-1 + 2
-1 + 2 + 3
-1 + 2 + 3 + (-9)
-1 + 2 + 3 + (-9) + 11

// 从 2 开始:
2
2 + 3
2 + 3 + (-9)
2 + 3 + (-9) + 11

// 从 3 开始:
3
3 + (-9)
3 + (-9) + 11

// 从 -9 开始:
-9
-9 + 11

// 从 11 开始:
11

这样写出来的代码实际上是一个嵌套循环:外部循环遍历数组所有元素,内部循环计算从当前元素开始的所有子数组各自的和。

function getMaxSubSum(arr) {
  let maxSum = 0; // 如果没有取到任何元素,就返回 0

  for (let i = 0; i < arr.length; i++) {
    let sumFixedStart = 0;
    for (let j = i; j < arr.length; j++) {
      sumFixedStart += arr[j];
      maxSum = Math.max(maxSum, sumFixedStart);
    }
  }

  return maxSum;
}

alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5
alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11
alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3
alert( getMaxSubSum([1, 2, 3]) ); // 6
alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100

该方案的时间复杂度是 O(n2)。也就是说,如果我们把数组大小增加 2 倍,那么算法的运行时间将会延长4倍。

对于大型数组(1000,10000 或者更多项)这种算法会导致严重的时间消耗。

快的解决方案

让我们遍历数组,将当前局部元素的和保存在变量 s 中。如果 s 在某一点变成负数了,就重新分配 s=0。所有 s 中的最大值就是答案。

如果文字描述不太好理解,就直接看下面的代码吧,真的很短:

function getMaxSubSum(arr) {
  let maxSum = 0;
  let partialSum = 0;

  for (let item of arr) { // arr 中的每个 item
    partialSum += item; // 将其加到 partialSum
    maxSum = Math.max(maxSum, partialSum); // 记住最大值
    if (partialSum < 0) partialSum = 0; // 如果是负数就置为 0
  }

  return maxSum;
}

alert( getMaxSubSum([-1, 2, 3, -9]) ); // 5
alert( getMaxSubSum([-1, 2, 3, -9, 11]) ); // 11
alert( getMaxSubSum([-2, -1, 1, 2]) ); // 3
alert( getMaxSubSum([100, -9, 2, -3, 5]) ); // 100
alert( getMaxSubSum([1, 2, 3]) ); // 6
alert( getMaxSubSum([-1, -2, -3]) ); // 0

该算法只需要遍历 1 轮数组,所以时间复杂度是 O(n)。

你也可以在这获取更多该算法的细节信息:最大子数组问题。如果还是不明白,那就调试上面的例子,观察它是怎样工作的,说得再多也没有自己去调试好使。

function getMaxSubSum(arr) {
  let maxSum = 0;
  let partialSum = 0;

  for (let item of arr) {
    partialSum += item;
    maxSum = Math.max(maxSum, partialSum);
    if (partialSum < 0) partialSum = 0;
  }
  return maxSum;
}

使用沙箱的测试功能打开解决方案。