文章背景图

LeetCode热题100(简单/中等)—— 普通数组

2026-02-03
0
-
- 分钟

1 最大子数组和

难度:中等

问题描述

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

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

 

示例 1:

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

示例 2:

输入:nums = [1]
输出:1

示例 3:

输入:nums = [5,4,-1,7,8]
输出:23

 

提示:

  • 1 <= nums.length <= 105

  • -104 <= nums[i] <= 104

 

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

思路 - 初见

  • 同样采用前缀和的思路,map中存储和的值和当前坐标,最后用值最大的前缀和减去值最小的前缀和

  • 哈希map存在重复和无法存放问题,采用数组存储,只需存储和,数组下标与题目数组保持一致

  • 存在单数组无法解决问题,单数组单独处理

代码 - 初见

class Solution {
    public int maxSubArray(int[] nums) {
        // 创建数组存储前缀和
        int[] prefixSums = new int[nums.length];
        
        int currentSum = 0;
        // 1. 遍历计算前缀和并存入数组
        for (int i = 0; i < nums.length; i++) {
            currentSum += nums[i];
            prefixSums[i] = currentSum;
        }
        
        // 2. 初始化为整型的最大值和最小值 (按照要求修改)
        int maxPrefix = Integer.MIN_VALUE;
        int minPrefix = Integer.MAX_VALUE;
        
        // 3. 遍历前缀和数组找出最大值和最小值
        for (int sum : prefixSums) {
            if (sum > maxPrefix) {
                maxPrefix = sum;
            }
            if (sum < minPrefix) {
                minPrefix = sum;
            }
        }
        
        // 4. 返回最大前缀和减去最小前缀和
        return maxPrefix - minPrefix;
    }
}

问题和改进

🚩 核心逻辑漏洞

你的代码有一个致命的问题:顺序性丢失

其中必须满足 j < i(减去的那个前缀和,必须出现在当前前缀和的前面)。

你的逻辑是“全局最大前缀 - 全局最小前缀”,这会导致以下两个错误:

  1. “时光倒流”错误:如果全局最大值出现在全局最小值之前,你的算法会用前面的值减去后面的值,这在数学上是不合法的(相当于子数组的结束位置在开始位置之前)。

    • 反例[1, -5]

    • 前缀和:[1, -4]

    • 你的逻辑:Max(1) - Min(-4) = 5。但显然最大子数组和是 1。

  2. 忽略了初始位置:如果不减去任何东西(从头开始的子数组),相当于减去 0。你的代码如果 minPrefix 算出来不是 0,就会出错。

✅ 改进思路:一边遍历,一边减去“当前遇到的最小前缀”

我们需要在遍历的过程中,动态维护一个 “目前为止遇到的最小前缀和”

对于每一个当前的 currentPrefix,我们计算: 潜在最大和 = currentPrefix - minPrefixSoFar

这样能保证我们减去的永远是出现在当前位置之前的最小值。

代码 - 改进

class Solution {
    public int maxSubArray(int[] nums) {
        // 记录当前的前缀和
        int currentPrefix = 0;
        
        // 关键点1:minPrefix 初始化为 0
        // 代表“什么都不减”,即子数组从下标 0 开始的情况
        int minPrefix = 0;
        
        // 结果初始化为极小值,防止由负数组成的数组导致结果错误
        int maxResult = Integer.MIN_VALUE;

        for (int x : nums) {
            currentPrefix += x;

            // 1. 核心逻辑:尝试用当前前缀和,减去之前出现过的最小前缀和
            // 这保证了计算的一定是合法的子数组
            maxResult = Math.max(maxResult, currentPrefix - minPrefix);

            // 2. 只有在计算完结果后,才更新最小前缀和
            // 这样保证了 minPrefix 永远在 currentPrefix 之前(或者就是初始的0)
            minPrefix = Math.min(minPrefix, currentPrefix);
        }

        return maxResult;
    }
}

发现

这其中是不是还有一个细节,就是只有最小前缀和小于0时才会记录,大于0时不需要减

我们在寻找“被减数”时,只要它是正的,它就是累赘(会把结果变小)。只有当之前的累积和变成了负数(拖后腿了),我们才需要把它“切掉”(减去它)

🚀 进阶:这就是 Kadane 算法

基于你的这个思路,代码可以写得更简单。我们不再显式维护 minPrefix,而是直接维护一个 “当前的贡献值”

逻辑如下:

  1. 如果之前的累加和(sum)是正的,那它对我有增益,我就留着它,继续往上加。

  2. 如果之前的累加和是负的(或者 0),它只会拖累我(让我变小),我就直接丢弃它,从当前数字重新开始计算。

这被称为 Kadane算法,是这道题的终极解法。

class Solution {
    public int maxSubArray(int[] nums) {
        int maxAns = nums[0];
        int pre = 0; // 代表“之前的累加和”

        for (int x : nums) {
            // 你的细节在这里体现:
            // 如果 pre > 0,说明前面的积累是正资产,加上它 (pre + x)
            // 如果 pre <= 0,说明前面的积累是负债,扔掉它,直接从 x 开始 (x)
            pre = Math.max(pre + x, x);
            
            // 记录过程中的最大值
            maxAns = Math.max(maxAns, pre);
        }
        return maxAns;
    }
}

2 合并区间

难度:中等

题目描述

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

 

示例 1:

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例 2:

输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

示例 3:

输入:intervals = [[4,7],[1,4]]
输出:[[1,7]]
解释:区间 [1,4] 和 [4,7] 可被视为重叠区间。

 

提示:

  • 1 <= intervals.length <= 104

  • intervals[i].length == 2

  • 0 <= starti <= endi <= 104

思路 - 初见

  • 合并规则:区间[a,b],[m,n]中m<=b,合并后的终点取两者最大值,即 Math.max(current.end, next.end);

  • 先将数组根据第一个数字排序,遍历,查看下一个数组的第一个数字是否满足拼接条件,要么拼接并记录,要么从下一个开始拼

  • 结果记录采用Array转int[][]

class Solution {
    public int[][] merge(int[][] intervals) {
        // 1. 边界检查
        if (intervals.length == 0) {
            return new int[0][0];
        }

        // 2. 排序:根据每个区间的第一个数字(start)进行升序排序
        Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));

        // 使用 ArrayList 存储结果,方便动态添加
        List<int[]> result = new ArrayList<>();

        // 3. 初始化:取出第一个区间作为当前正在比较的区间 [a, b]
        int[] current = intervals[0];

        for (int i = 1; i < intervals.length; i++) {
            // 下一个区间 [m, n]
            int[] next = intervals[i];

            // 4. 核心判断:如果 next 的起点 m <= current 的终点 b
            if (next[0] <= current[1]) {
                // 发生重叠,进行合并
                // 新的终点是两者终点的最大值:max(b, n)
                current[1] = Math.max(current[1], next[1]);
            } else {
                // 没有重叠,说明 current 已经断开了
                // 1. 记录当前合并好的 interval
                result.add(current);
                // 2. 将 next 更新为新的 current,继续往后找
                current = next;
            }
        }

        // 5. 别忘了把最后一个 pending 的 interval 加入结果
        result.add(current);

        // 6. 结果记录采用 List 转 int[][]
        return result.toArray(new int[result.size()][]);
    }
}

问题和改进

  1. 逻辑简化:消除对 current 变量的单独维护,也不需要循环结束后再补加最后一次,防止漏掉。

代码 - 改进

class Solution {
    public int[][] merge(int[][] intervals) {
        if (intervals.length == 0) return new int[0][0];

        // 1. 排序
        Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));

        List<int[]> result = new ArrayList<>();
        
        // 2. 遍历
        for (int[] interval : intervals) {
            // 如果结果集为空,或者当前区间的 start > 结果集最后一个的 end
            // 说明没有重叠,直接加入
            if (result.isEmpty() || interval[0] > result.get(result.size() - 1)[1]) {
                result.add(interval);
            } else {
                // 有重叠,直接更新结果集最后一个区间的 end
                // 此时不需要 add,因为已经在 list 里了
                int[] lastInterval = result.get(result.size() - 1);
                lastInterval[1] = Math.max(lastInterval[1], interval[1]);
            }
        }

        return result.toArray(new int[result.size()][]);
    }
}

3 轮转数组

难度:中等

题目描述

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

 

示例 1:

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]

示例 2:

输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释: 
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]

 

提示:

  • 1 <= nums.length <= 105

  • -231 <= nums[i] <= 231 - 1

  • 0 <= k <= 105

思路 - 初见

  • 这个我见过

  • 定义reverse方法,先反转length - k个,再反转后k个数字,最后整个数组反转

代码 - 初见

class Solution {
    /**
     * 轮转数组
     * 核心思路:三次翻转法
     * 1. 数组长度为 n,移动 k 位。
     * 2. 注意:题目要求是“向右”轮转,意味着最后 k 个元素会跑到最前面。
     * 3. 策略:
     * - 先反转前半部分 (0 到 n-k-1)
     * - 再反转后半部分 (n-k 到 n-1)
     * - 最后反转整个数组,即可恢复顺序并完成位置交换
     */
    public void rotate(int[] nums, int k) {
        int n = nums.length;
        
        // 1. 预处理 k
        // 如果 k 大于数组长度,实际上是绕圈圈,取模得到有效移动步数
        k %= n; 
        
        // 如果 k 为 0,无需移动,直接返回
        if (k == 0) return;

        // 2. 三步反转逻辑 (基于你的原始思路)
        
        // 第一步:反转前半部分 [1, 2, 3, 4] -> [4, 3, 2, 1]
        // 范围:0 到 n - k - 1
        reverse(nums, 0, n - k - 1);
        
        // 第二步:反转后半部分 [5, 6, 7] -> [7, 6, 5]
        // 范围:n - k 到 n - 1
        reverse(nums, n - k, n - 1);
        
        // 当前数组状态:[4, 3, 2, 1, 7, 6, 5]
        
        // 第三步:反转整个数组,恢复内部顺序
        // [4, 3, 2, 1, 7, 6, 5] -> [5, 6, 7, 1, 2, 3, 4]
        reverse(nums, 0, n - 1);
    }

    /**
     * 双指针反转数组指定区间
     */
    private void reverse(int[] nums, int start, int end) {
        while (start < end) {
            int temp = nums[start];
            nums[start] = nums[end];
            nums[end] = temp;
            start++;
            end--;
        }
    }
}

2 除了自身以外数组的乘积

难度:中等

题目描述

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除了 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在  32 位 整数范围内。

请 不要使用除法,且在 O(n) 时间复杂度内完成此题。

 

示例 1:

输入: nums = [1,2,3,4]
输出: [24,12,8,6]

示例 2:

输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]

 

提示:

  • 2 <= nums.length <= 105

  • -30 <= nums[i] <= 30

  • 输入 保证 数组 answer[i] 在  32 位 整数范围内

 

进阶:你可以在 O(1) 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组 不被视为 额外空间。)

思路 - 初见

  • 存储每一个元素的前缀乘积和后缀乘积,最后让它俩相乘。

代码 - 初见

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] answer = new int[n];

        // 1. 先计算“左侧所有元素的乘积” (前缀乘积)
        // answer[i] 表示 i 左边所有元素的乘积
        answer[0] = 1; // 第0个元素左边没有数字,视为1
        for (int i = 1; i < n; i++) {
            answer[i] = nums[i - 1] * answer[i - 1];
        }

        // 2. 再计算“右侧所有元素的乘积” (后缀乘积)
        // 我们不需要开辟新数组,直接用一个变量 R 来记录右边的乘积
        int R = 1; // 刚开始右边没有数字,视为1
        for (int i = n - 1; i >= 0; i--) {
            // 对于索引 i:
            // 左边的乘积已经在 answer[i] 里了
            // 右边的乘积在变量 R 里
            // 直接相乘更新 answer[i]
            answer[i] = answer[i] * R;
            
            // 更新 R,把当前 nums[i] 乘进去,供下一个位置 (i-1) 使用
            R *= nums[i];
        }

        return answer;
    }
}

评论交流

文章目录