一分钟解决 | 高频面试算法题——最大子数组之和

一、题目描述------最大子数组之和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组是数组中的一个连续部分。

示例 1:

ini 复制代码
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:

ini 复制代码
输入: nums = [1]
输出: 1

示例 3:

ini 复制代码
输入: nums = [5,4,-1,7,8]
输出: 23

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104

进阶: 如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。

二、题解

js 复制代码
/**
 * @param {number[]} nums
 * @return {number}
 */
var maxSubArray = function(nums) {
  // 初始化最大和和当前和,都设置为数组的第一个元素
  let maxSoFar = nums[0];
  let currentMax = nums[0];

  // 遍历数组,从第二个元素开始
  for (let i = 1; i < nums.length; i++) {
    // 关键步骤:决定是延续之前的子数组,还是从当前元素开始一个新的子数组
    // 如果之前的子数组(currentMax)加上当前元素比当前元素本身还小,
    // 说明之前的子数组是负收益,不如直接从当前元素开始。
    currentMax = Math.max(nums[i], currentMax + nums[i]);

    // 更新全局最大和,取当前最大和与之前最大和的较大值
    maxSoFar = Math.max(maxSoFar, currentMax);
  }

  // 返回找到的最大和
  return maxSoFar;
};

核心思想

Kadane 算法的核心思想是使用动态规划,以线性时间复杂度(O(n))解决最大子数组和问题。它通过迭代数组,在每一步维护两个变量:

  1. currentMax: 表示以当前元素结尾的连续子数组的最大和。
  2. maxSoFar: 表示到目前为止找到的最大子数组和(全局最大和)。

算法的关键在于每一步如何更新 currentMax。我们要做出一个决定:是以当前元素 nums[i] 重新开始一个新的子数组,还是将当前元素添加到之前的子数组中。 这个决定基于哪个选择能带来更大的和。

详细解析

  1. 初始化:

    • maxSoFar = nums[0]: 初始时,假设最大的子数组就是第一个元素本身。
    • currentMax = nums[0]: 初始时,也假设以第一个元素结尾的子数组的最大和就是它本身。
  2. 循环遍历:

    • 从数组的第二个元素开始迭代 (i = 1)

    • currentMax = Math.max(nums[i], currentMax + nums[i]); (关键步骤) :

      • nums[i]: 如果 nums[i]currentMax + nums[i] 大,这意味着以 nums[i] 重新开始一个新的子数组更好。之前的子数组的和是负数(或者 0),拖累了结果,所以放弃。
      • currentMax + nums[i]: 如果 currentMax + nums[i]nums[i] 大,这意味着将 nums[i] 添加到之前的子数组中是更有利的。之前的子数组至少带来了一些正向的贡献。
      • Math.max(): 选取两者之间的较大值,更新 currentMax,使其始终表示 以当前元素结尾的最大的子数组和
    • maxSoFar = Math.max(maxSoFar, currentMax); :

      • 更新全局最大和 maxSoFar。在每一步中,检查当前的 currentMax 是否大于之前找到的 maxSoFar。如果大于,则更新 maxSoFar
  3. 返回:

    • 循环结束后,maxSoFar 存储的就是整个数组中最大子数组的和。

注意事项

  1. 空数组或 null 数组: 尽管代码没有显式地检查 nums 是否为 null 或空数组,但如果真是这种情况,代码会抛出错误(访问 nums[0] 时)。 在实际应用中,建议首先添加一个检查:

    ini 复制代码
      
    if (!nums || nums.length === 0) {
        return 0; // 或者抛出错误
    }
  2. 数组全为负数: 算法可以正确处理数组中的所有元素都是负数的情况。 在这种情况下,maxSoFar 将会是数组中最大的负数(绝对值最小的负数)。 这是因为初始化时 maxSoFar = nums[0]

  3. 理解 currentMax 的含义至关重要: 不要将 currentMax 视为到目前为止的最大和。 它是 以当前元素结尾的 最大子数组和。 这是算法能够以线性时间复杂度工作的关键。

  4. 空间复杂度: Kadane 算法具有 O(1) 的空间复杂度,因为它只需要几个常数级的变量(maxSoFar, currentMax, i)。

  5. 适用性: Kadane 算法专门用于一维数组的最大子数组和问题。 如果需要解决二维数组或更复杂情景下的最大子数组问题,需要使用其他算法。

实例与展示

HTML动态可交互题解(直接运行即可)

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>最大子数组和可视化</title>
    <style>
        body {
            font-family: 'Microsoft YaHei', sans-serif;
            margin: 20px;
        }
        .container {
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        .array-container {
            display: flex;
            margin-bottom: 20px;
        }
        .array-element {
            width: 60px;
            height: 60px;
            border: 1px solid #333;
            display: flex;
            justify-content: center;
            align-items: center;
            margin-right: 5px;
            font-weight: bold;
            position: relative;
        }
        .current {
            background-color: #ffeb3b;
        }
        .max-so-far {
            background-color: #4caf50;
            color: white;
        }
        .controls {
            margin: 20px 0;
        }
        button {
            padding: 8px 16px;
            margin: 0 5px;
            cursor: pointer;
        }
        .explanation {
            margin-top: 20px;
            padding: 15px;
            border: 1px solid #ddd;
            background-color: #f9f9f9;
            max-width: 600px;
        }
        .variables {
            display: flex;
            justify-content: center;
            margin-bottom: 20px;
        }
        .variable {
            margin: 0 15px;
            padding: 10px;
            border: 1px solid #333;
            min-width: 100px;
            text-align: center;
        }
        .variable-name {
            font-weight: bold;
            margin-bottom: 5px;
        }
        .variable-value {
            font-size: 18px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>最大子数组和可视化</h1>
        
        <div class="variables">
            <div class="variable">
                <div class="variable-name">当前最大值</div>
                <div class="variable-value" id="currentMaxVal">0</div>
            </div>
            <div class="variable">
                <div class="variable-name">全局最大值</div>
                <div class="variable-value" id="maxSoFarVal">0</div>
            </div>
        </div>
        
        <div class="array-container" id="arrayContainer"></div>
        
        <div class="controls">
            <button id="prevBtn">上一步</button>
            <button id="nextBtn">下一步</button>
            <button id="resetBtn">重置</button>
            <button id="autoBtn">自动运行</button>
        </div>
        
        <div class="explanation" id="explanation">
            <p><strong>初始化:</strong> 将当前最大值和全局最大值都设为第一个元素 (nums[0] = -2)</p>
        </div>
    </div>

    <script>
        // 示例数组 (可以修改)
        const nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4];
        let currentStep = 0;
        let autoInterval = null;
        
        // 初始化数组可视化
        function initArray() {
            const container = document.getElementById('arrayContainer');
            container.innerHTML = '';
            
            nums.forEach((num, index) => {
                const element = document.createElement('div');
                element.className = 'array-element';
                element.textContent = num;
                element.id = `element-${index}`;
                container.appendChild(element);
            });
            
            updateVisualization();
        }
        
        // 根据当前步骤更新可视化
        function updateVisualization() {
            // 重置所有高亮
            document.querySelectorAll('.array-element').forEach(el => {
                el.classList.remove('current', 'max-so-far');
            });
            
            // 更新变量显示
            document.getElementById('currentMaxVal').textContent = getCurrentMax();
            document.getElementById('maxSoFarVal').textContent = getMaxSoFar();
            
            if (currentStep === 0) {
                // 初始状态
                document.getElementById('element-0').classList.add('current', 'max-so-far');
                document.getElementById('explanation').innerHTML = 
                    '<p><strong>初始化:</strong> 将当前最大值和全局最大值都设为第一个元素 (nums[0] = ' + nums[0] + ')</p>';
            } else if (currentStep <= nums.length) {
                const i = currentStep - 1;
                const element = document.getElementById(`element-${i}`);
                element.classList.add('current');
                
                const prevCurrentMax = i > 0 ? getCurrentMaxForStep(i-1) : nums[0];
                const newCurrentMax = Math.max(nums[i], prevCurrentMax + nums[i]);
                const newMaxSoFar = Math.max(getMaxSoFarForStep(i-1), newCurrentMax);
                
                document.getElementById('explanation').innerHTML = `
                    <p><strong>第 ${currentStep} 步:</strong> 处理 nums[${i}] = ${nums[i]}</p>
                    <p>当前最大值 = max(nums[${i}], 当前最大值 + nums[${i}]) = max(${nums[i]}, ${prevCurrentMax} + ${nums[i]}) = ${newCurrentMax}</p>
                    <p>全局最大值 = max(全局最大值, 当前最大值) = max(${getMaxSoFarForStep(i-1)}, ${newCurrentMax}) = ${newMaxSoFar}</p>
                `;
                
                if (newCurrentMax === nums[i]) {
                    document.getElementById('explanation').innerHTML += 
                        '<p>从这个元素开始新的子数组,因为它比继续之前的子数组更好</p>';
                }
                
                // 高亮当前最大子数组
                let start = i;
                let sum = 0;
                while (start >= 0) {
                    sum += nums[start];
                    if (sum === newCurrentMax) {
                        for (let j = start; j <= i; j++) {
                            document.getElementById(`element-${j}`).classList.add('max-so-far');
                        }
                        break;
                    }
                    start--;
                }
            } else {
                document.getElementById('explanation').innerHTML = 
                    '<p><strong>完成:</strong> 最大子数组和是 ' + getMaxSoFar() + '</p>';
            }
        }
        
        // 辅助函数获取特定步骤的值
        function getCurrentMaxForStep(step) {
            if (step < 0) return nums[0];
            let currentMax = nums[0];
            let maxSoFar = nums[0];
            
            for (let i = 1; i <= step; i++) {
                currentMax = Math.max(nums[i], currentMax + nums[i]);
                maxSoFar = Math.max(maxSoFar, currentMax);
            }
            
            return currentMax;
        }
        
        function getMaxSoFarForStep(step) {
            if (step < 0) return nums[0];
            let currentMax = nums[0];
            let maxSoFar = nums[0];
            
            for (let i = 1; i <= step; i++) {
                currentMax = Math.max(nums[i], currentMax + nums[i]);
                maxSoFar = Math.max(maxSoFar, currentMax);
            }
            
            return maxSoFar;
        }
        
        function getCurrentMax() {
            return getCurrentMaxForStep(currentStep - 1);
        }
        
        function getMaxSoFar() {
            return getMaxSoFarForStep(currentStep - 1);
        }
        
        // 事件监听
        document.getElementById('nextBtn').addEventListener('click', () => {
            if (currentStep < nums.length + 1) {
                currentStep++;
                updateVisualization();
            }
        });
        
        document.getElementById('prevBtn').addEventListener('click', () => {
            if (currentStep > 0) {
                currentStep--;
                updateVisualization();
            }
        });
        
        document.getElementById('resetBtn').addEventListener('click', () => {
            currentStep = 0;
            if (autoInterval) {
                clearInterval(autoInterval);
                autoInterval = null;
            }
            updateVisualization();
        });
        
        document.getElementById('autoBtn').addEventListener('click', () => {
            if (autoInterval) {
                clearInterval(autoInterval);
                autoInterval = null;
                return;
            }
            
            autoInterval = setInterval(() => {
                if (currentStep >= nums.length + 1) {
                    clearInterval(autoInterval);
                    autoInterval = null;
                    return;
                }
                currentStep++;
                updateVisualization();
            }, 1500);
        });
        
        // 初始化
        initArray();
    </script>
</body>
</html>

三、结语

再见!

相关推荐
_一条咸鱼_28 分钟前
Python 数据类型之可变与不可变类型详解(十)
人工智能·python·面试
_一条咸鱼_28 分钟前
Python 入门之基本运算符(六)
python·深度学习·面试
_一条咸鱼_28 分钟前
Python 语法入门之基本数据类型(四)
人工智能·深度学习·面试
_一条咸鱼_29 分钟前
Python 用户交互与格式化输出(五)
人工智能·深度学习·面试
_一条咸鱼_30 分钟前
Python 流程控制之 for 循环(九)
人工智能·python·面试
_一条咸鱼_33 分钟前
Python 语法入门之流程控制 if 判断(七)
人工智能·python·面试
Senar34 分钟前
如何判断浏览器是否开启硬件加速
前端·javascript·数据可视化
_一条咸鱼_34 分钟前
Python 流程控制之 while 循环(八)
人工智能·python·面试
_一条咸鱼_36 分钟前
Python 垃圾回收机制 GC 深度解析(三)
人工智能·深度学习·面试
_一条咸鱼_37 分钟前
Android Picasso 监听器模块深度剖析(八)
android·面试·android jetpack