langShiftlangShift

普通数组(题单顺序)

热题 100 - 普通数组分组(按题单顺序):53 / 56 / 189 / 238 / 41

本页包含题单「普通数组」分组的全部题目,顺序与 list.json 保持一致。


53. 最大子数组和

力扣原题:53. 最大子数组和
Medium
数组
分治
动态规划

给你一个整数数组 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) 的解法,尝试使用更为精妙的 分治法 求解。

最优解(Kadane:要么接着累加,要么从当前重开)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)
  • 问题本质:在连续子数组里挑一段,让它的和最大。
  • 朴素想法:枚举所有区间 ((l,r)) 求和取最大,(O(n^2))(或更慢)。
  • 关键推理:只看“以 i 结尾”的最优解
    cur 表示“以当前位置 i 结尾的最大子数组和”。当我们处理 nums[i]=x 时,只有两种选择:
    • 接着前面那段cur + x
    • 从当前重新开一段x 所以 cur = max(x, cur + x)
  • 为什么这样就够了?:因为任何最优子数组一定有一个“结尾位置”,我们在遍历时把每个结尾的最优都算出来,并用 best 取最大即可。
  • 直觉解释:如果你手里的 cur 已经是负数,那么再加上 x 只会拖后腿,还不如从 x 重开。
  • 复杂度:时间 (O(n)),空间 (O(1))(只用常数变量)。
类似题目(一维 DP:以 i 结尾的最优)
let cur = init
let best = init
for (const x of arr) {
cur = transition(cur, x) // 常见形式:max(x, cur + x)
best = Math.max(best, cur)
}

56. 合并区间

以数组 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
最优解(排序 + 一次扫描合并)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)
  • 先想清楚为什么要排序:如果区间乱序,你很难判断“当前区间该和谁合并”。把区间按起点排序后,区间会按时间线从左到右出现。
  • 排序后的关键性质:当你处理到某个区间 [l,r],它的起点 l 一定不小于前面任何区间的起点。
  • 用一个“当前正在合并的区间”滚动维护
    • 当前合并段是 [curL, curR]
    • 来了新区间 [l,r]
      • 如果 l <= curR:说明有重叠(或刚好相接),两段可以合并,新的右边界是 max(curR, r)
      • 如果 l > curR:说明彻底断开,上一段已经不可能再与后面重叠(因为后面起点只会更大),所以把 [curL,curR] 放进答案,然后开启新段。
  • 为什么一次扫描就够?:排序保证“不会出现后面区间突然跑到当前区间左边”的情况,所以每个区间只需处理一次。
  • 边界情况:输入为空直接返回空;只有一个区间直接返回它本身。
  • 复杂度:排序 (O(n\log n)) + 扫描 (O(n)),总时间 (O(n\log n)),空间 (O(1))(不算输出)。
类似题目(区间题通用套路:排序 + 贪心合并/选择)
arr.sort((a, b) => a.start - b.start)
let cur = arr[0]
for (const x of arr.slice(1)) {
if (overlap(cur, x)) cur = merge(cur, x)
else {
ans.push(cur)
cur = x
}
}
ans.push(cur)

189. 轮转数组

力扣原题:189. 轮转数组
Medium
数组
数学
双指针

给定一个整数数组 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

 

进阶:

  • 尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
  • 你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?
最优解(三次翻转:整体翻转 + 两段翻转)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)
  • 题意翻译:把数组整体向右挪 k 步,超出末尾的元素绕回开头,要求原地。
  • 朴素做法:右移一步做 k 次,每次都要挪动很多元素,会很慢。
  • 关键观察:右旋 k 等价于把数组分成两段
    • k 个元素会跑到最前面
    • n-k 个元素会整体往后挪
  • 三次翻转的推理(最重要的“魔法”):
    • 假设数组是 A | B(A 长度 n-k,B 长度 k),目标是 B | A
    • 先整体翻转:reverse(A|B) = reverse(B) | reverse(A)
    • 再分别把前 k、后 n-k 翻转回来:
      • reverse(reverse(B)) = B
      • reverse(reverse(A)) = A
    • 最终得到 B | A,正好是旋转结果。
  • 为什么是原地且最优:只做常数次翻转,每次翻转是交换,时间 (O(n)),空间 (O(1))。
  • k 的处理细节:一定要先做 k %= n,否则 k 可能很大;k 为 0 时翻转前 k 段会变成空区间,代码需天然兼容(本实现兼容)。
类似题目(翻转技巧:用 reverse 组合实现“切换顺序”)
// 目标:把 A|B 变成 B|A(或其它段交换)
reverse(whole)
reverse(part1)
reverse(part2)

238. 除了自身以外数组的乘积

给你一个整数数组 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) 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组 不被视为 额外空间。)

最优解(前缀积 + 后缀积:两趟扫描,O(1) 额外空间)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)
  • 题意翻译:对每个位置 i,输出“除了 nums[i] 之外所有数的乘积”,不能用除法。
  • 朴素思路:对每个 i 再扫一遍乘起来,(O(n^2))。
  • 关键分解:乘积可以拆成左边乘积 × 右边乘积
    • ans[i] = (nums[0..i-1] 的乘积) * (nums[i+1..n-1] 的乘积)
  • 怎么在 (O(n)) 求出所有左乘积/右乘积?
    • 左乘积:从左到右扫一遍,用 left 累乘,res[i] = left,再 left *= nums[i]
    • 右乘积:从右到左扫一遍,用 right 累乘,res[i] *= right,再 right *= nums[i]
  • 为什么不需要额外数组?res 先存左乘积,再原地乘上右乘积即可,所以额外变量只有 left/right 两个。
  • 0 的情况为什么也能处理?
    • 因为我们没有除法,纯粹用乘积拆分;如果某侧有 0,乘出来自然就是 0,符合题意。
  • 复杂度:两趟扫描,时间 (O(n)),额外空间 (O(1))(输出数组 res 不算额外)。
类似题目(前缀/后缀分解:把全局贡献拆成两边)
// ans[i] = leftAgg(i) * rightAgg(i)
let left = identity
for i=0..n-1: ans[i]=left; left=combine(left, nums[i])
let right = identity
for i=n-1..0: ans[i]=combine(ans[i], right); right=combine(nums[i], right)

41. 缺失的第一个正数

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

 

示例 1:

输入:nums = [1,2,0]
输出:3
解释:范围 [1,2] 中的数字都在数组中。

示例 2:

输入:nums = [3,4,-1,1]
输出:2
解释:1 在数组中,但 2 没有。

示例 3:

输入:nums = [7,8,9,11,12]
输出:1
解释:最小的正数 1 没有出现。

 

提示:

  • 1 <= nums.length <= 105
  • -231 <= nums[i] <= 231 - 1
最优解(原地哈希:把值 x 放到下标 x-1)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)
  • 题意翻译:找数组里缺失的最小正整数。
  • 这题的“最优”很苛刻:要 (O(n)) 时间 + (O(1)) 额外空间,所以不能用 Set/排序(排序 (O(n\log n)))。
  • 关键观察:答案一定在 ([1, n+1])
    • 如果 1 不在,答案就是 1。
    • 如果 1..n 都在,答案就是 n+1。
    • 所以我们只关心 1..n 这些值,其它负数/0/大于 n 的数都不影响答案。
  • 核心想法:把数组当成“哈希表”用
    让值 x 去它该去的位置:下标 x-1。这样如果所有值都摆对了,位置 i 上就应该是 i+1
  • 为什么要用 while 反复交换?
    • 因为交换后 nums[i] 变成了另一个值,它可能也需要被送到自己的位置。
    • 反复把当前格子里的值送走,直到这个格子里是“无关值/已就位/重复值”为止。
  • 如何拿到答案?
    • 第二趟从左到右扫,找到第一个 nums[i] != i+1 的位置,答案就是 i+1
  • 为什么是 (O(n))?
    • 每次交换都把至少一个元素放到了正确位置,元素不会无限交换;整体交换次数是线性的。
  • 最常见坑:没处理重复值会造成死循环,所以要用 if (nums[j] === x) break 兜住。
  • 复杂度:时间 (O(n)),空间 (O(1))。
类似题目(原地哈希模板:值映射到下标)
for (let i = 0; i < n; i++) {
while (needSwap(nums[i])) {
swap(nums, i, targetIndex(nums[i]))
}
}
// 再扫一遍找第一个不符合的位置