普通数组模块 — LeetCode 热门 100 题精讲(Java 向)

从最大子数组和到缺失的第一个正数,贪心、排序、原地翻转、前后缀积、原地哈希 — 五种数组经典技巧一次掌握

普通数组问题概述

数组是算法题中最基础也最灵活的数据结构。所谓"普通数组",指不涉及树、图等复杂结构,纯粹在数组上进行操作,其解题技巧往往依赖于观察规律巧妙的空间复用

LeetCode 热门 100 题中,有五道经典题覆盖了数组操作的五个不同维度:

  • 53. 最大子数组和 — 贪心 / DP 的简洁应用(Kadane 算法)。
  • 56. 合并区间 — 排序后线性扫描,典型区间合并模板。
  • 189. 轮转数组 — 原地翻转技巧,空间 O(1) 的反转三趟。
  • 238. 除自身以外数组的乘积 — 前后缀积,避免除法。
  • 41. 缺失的第一个正数 — 原地哈希,利用数组下标作桶。

它们各自代表了数组问题中一类常见的优化思路。下面逐一拆解。


1. 最大子数组和(LeetCode 53)

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

暴力法:枚举所有子数组,计算和,O(n²)。

优化思路(Kadane 算法): 遍历数组,维护两个变量:currentSum 表示以当前元素结尾的子数组最大和,maxSum 记录全局最大和。递推公式为:

currentSum = max(nums[i], currentSum + nums[i])
maxSum = max(maxSum, currentSum)

即要么从当前元素重新开始,要么将当前元素追加到之前的子数组上。这本质是贪心或一维动态规划。

public int maxSubArray(int[] nums) {
    int currentSum = nums[0];
    int maxSum = nums[0];
    for (int i = 1; i < nums.length; i++) {
        currentSum = Math.max(nums[i], currentSum + nums[i]);
        maxSum = Math.max(maxSum, currentSum);
    }
    return maxSum;
}

复杂度:时间 O(n),空间 O(1)。

关键点:当 currentSum 为负数时,舍弃之前的部分,从当前元素重新开始会更优。


2. 合并区间(LeetCode 56)

题目:给出一个区间的集合,合并所有重叠的区间。

优化思路(排序 + 线性扫描): 先将区间按左端点升序排序。然后遍历区间,若当前区间的左端点 <= 上一个合并区间右端点,则合并(更新右端点为较大值);否则,将上一个合并区间加入结果,开始新合并区间。

public int[][] merge(int[][] intervals) {
    if (intervals.length <= 1) return intervals;
    Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
    List<int[]> merged = new ArrayList<>();
    int[] current = intervals[0];
    for (int i = 1; i < intervals.length; i++) {
        if (intervals[i][0] <= current[1]) {
            current[1] = Math.max(current[1], intervals[i][1]);
        } else {
            merged.add(current);
            current = intervals[i];
        }
    }
    merged.add(current);
    return merged.toArray(new int[merged.size()][]);
}

复杂度:排序 O(n log n),扫描 O(n),总时间 O(n log n),空间 O(log n) 用于排序。

关键点:排序后只需要一次遍历;合并时右端点取两者最大值。


3. 轮转数组(LeetCode 189)

题目:将数组中的元素向右轮转 k 个位置。要求空间 O(1) 原地操作。

优化思路(三趟翻转)

  • 先对整个数组翻转。
  • 再对前 k 个元素翻转(k = k % n)。
  • 最后对剩余元素翻转。
public void rotate(int[] nums, int k) {
    int n = nums.length;
    k %= n;
    reverse(nums, 0, n - 1);
    reverse(nums, 0, k - 1);
    reverse(nums, k, n - 1);
}
private void reverse(int[] nums, int left, int right) {
    while (left < right) {
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
        left++;
        right--;
    }
}

复杂度:时间 O(n),空间 O(1)。

关键点k %= n 处理 k 大于数组长度的情况;翻转顺序不能颠倒。


4. 除自身以外数组的乘积(LeetCode 238)

题目:返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。要求不用除法,O(n) 时间。

优化思路(前后缀乘积): 先用 answer 数组存储左侧乘积,然后从右往左乘以右侧乘积,滚动更新右侧乘积变量。

public int[] productExceptSelf(int[] nums) {
    int n = nums.length;
    int[] ans = new int[n];
    ans[0] = 1;
    for (int i = 1; i < n; i++) {
        ans[i] = ans[i-1] * nums[i-1];
    }
    int rightProduct = 1;
    for (int i = n - 1; i >= 0; i--) {
        ans[i] = ans[i] * rightProduct;
        rightProduct *= nums[i];
    }
    return ans;
}

复杂度:时间 O(n),空间 O(1)(除返回数组外)。


5. 缺失的第一个正数(LeetCode 41)

题目:找出未排序整数数组中没有出现的最小正整数。要求 O(n) 时间,O(1) 空间。

优化思路(原地哈希): 将数组视为哈希表,下标 i 对应数值 i+1。遍历数组,将每个数放到它应该在的位置(即 nums[i] 应该在 nums[nums[i]-1] 处),交换直到当前位置的值是正确值或越界。然后再遍历一次,第一个 nums[i] != i+1 的位置就是缺失的最小正数。

public int firstMissingPositive(int[] nums) {
    int n = nums.length;
    for (int i = 0; i < n; i++) {
        while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
            int temp = nums[nums[i] - 1];
            nums[nums[i] - 1] = nums[i];
            nums[i] = temp;
        }
    }
    for (int i = 0; i < n; i++) {
        if (nums[i] != i + 1) {
            return i + 1;
        }
    }
    return n + 1;
}

复杂度:时间 O(n)(每个元素最多交换两次),空间 O(1)。


五题对比与启发

题目 核心技巧 时间复杂度 空间复杂度 关键特征
最大子数组和 Kadane 贪心/DP O(n) O(1) 递推当前最优,舍弃负数前缀
合并区间 排序 + 线性扫描 O(n log n) O(log n) 按左端点排序,合并重叠
轮转数组 三次翻转 O(n) O(1) 利用翻转交换前后块
除自身以外乘积 前后缀积 O(n) O(1) 两次遍历,右侧乘积滚动
缺失第一个正数 原地哈希 O(n) O(1) 利用下标做桶,交换归位

总结心法

  1. 如果涉及子数组最值 → 考虑 Kadane 或 DP。
  2. 如果涉及区间合并 → 排序后线性扫描。
  3. 如果涉及循环移位 → 考虑翻转或取模映射。
  4. 如果涉及所有元素乘积(不含自身) → 用前后缀积空间优化。
  5. 如果涉及最小缺失正数 → 用原地哈希,通过交换把数放回"正确位置"。


🤖