当前位置: 首页 > news >正文

数据结构算法学习:LeetCode热题100-普通数组篇(最大子数组和、合并区间、轮转数组、除自身以外数组的乘积、缺失的第一个正常数)

文章目录

  • 简介
  • 53. 最大子数组和
    • 问题描述
    • 解题方法
      • 动态规划求解
      • 分治求解
  • 56. 合并区间
    • 问题描述
    • 解题方法
      • 排序求解
  • 238. 除自身以外数组的乘积
    • 问题描述
    • 解题方法
  • 41. 缺失的第一个正数
    • 问题描述
    • 解题方法
      • 哈希表求解
  • 个人学习总结

简介

本篇博客将深入剖析 LeetCode 中四道极具代表性的数组难题:53. 最大子数组和、56. 合并区间、238. 除自身以外数组的乘积以及 41. 缺失的第一个正数。通过本文,我希望能够系统性地梳理解决复杂数组问题的核心方法论,展示如何根据问题特性选择最优解,并深入分析不同方案在时间与空间复杂度上的权衡。无论您是正在准备面试的算法初学者,还是寻求思维突破的进阶者,相信都能从中获得启发与收获。

53. 最大子数组和

问题描述

给你一个整数数组 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. 初始化:
    • ans = nums[0]:初始最大和设为第一个元素
    • pre = 0:表示前一个位置的子数组和(初始为0)
  2. 遍历数组:
    • 对每个元素 x 执行:
    • 更新当前子数组和:pre = max(pre + x, x)
      • 若 pre + x > x,则延续前一个子数组
      • 否则,以当前元素开始新子数组
    • 更新全局最大值:ans = max(ans, pre)
  3. 返回结果:
    • 遍历结束后,ans 即为整个数组的最大子数组和

实现代码

class Solution {public int maxSubArray(int[] nums) {int ans = nums[0], pre = 0;for(int x : nums){pre = Math.max(pre + x, x);ans = Math.max(ans, pre);}return ans;}
}

复杂度分析
时间复杂度:O(n)

  • 仅需一次遍历数组,n 为数组长度

  • 每个元素处理时间为 O(1)

空间复杂度:O(1)

  • 仅使用常数个额外变量(ans 和 pre)
  • 无需存储 dp 数组,空间优化显著

分治求解

解题思路
采用分治策略求解最大子数组和问题,核心思想是将问题分解为三个子问题:

  • 左子数组的最大子数组和
  • 右子组的最大子数组和
  • 跨越中点的最大子数组和

最终取三者中的最大值作为全局解。递归分解问题直到子问题规模为1(单个元素),然后合并子问题的解。
解题步骤

  1. 递归终止条件:当子数组只有一个元素时(left == right),直接返回该元素值
  2. 划分中间节点:计算中间位置 mid = left + (right - left) / 2
  3. 递归求解子问题:
    • 递归求解左子数组最大和:leftsum = divided(nums, left, mid)
    • 递归求解右子数组最大和:rightsum = divided(nums, mid + 1, right)
  4. 求解跨中点子数组和:
    • 左扩展:从mid向左遍历,计算包含mid的最大子数组和leftmax
    • 右扩展:从mid+1向右遍历,计算包含mid+1的最大子数组和rightmax
    • 跨中点和:crosssum = leftmax + rightmax
  5. 合并结果:返回三者中的最大值 Math.max(Math.max(leftsum, rightsum), crosssum)

实现代码

class Solution {public int divided(int[] nums, int left, int right){if(left == right){return nums[left];}// 划分中间节点int mid = left + (right - left) / 2;// 递归求解左右子数组int leftsum = divided(nums, left, mid);int rightsum = divided(nums, mid + 1, right);// 求解跨中间节点子数组和int crosssum = crossSum(nums, mid, left, right);return Math.max(Math.max(leftsum, rightsum), crosssum);}public int crossSum(int[] nums, int mid, int left, int right){// 左扩展int leftmax = Integer.MIN_VALUE;int sum = 0;for(int i = mid; i >= left; i --){sum += nums[i];leftmax = Math.max(sum, leftmax);}// 右扩展int rightmax = Integer.MIN_VALUE;sum = 0;for(int i = mid + 1; i <= right; i ++){sum += nums[i];rightmax = Math.max(sum, rightmax);}return leftmax + rightmax;}public int maxSubArray(int[] nums) {return divided(nums, 0, nums.length - 1);}
}

复杂度分析
时间复杂度

  • 递归关系:T(n) = 2T(n/2) + O(n)
    • 递归处理左右两个子数组:2T(n/2)
    • 计算跨中点子数组和:O(n)(两个循环各O(n/2))
  • 根据主定理(Master Theorem):
    • a=2, b=2, f(n)=O(n)
    • n^logb(a) = n^1 = n
    • 情况2:f(n) = Θ(n^logb(a)) → T(n) = Θ(n log n)
  • 最终时间复杂度:O(n log n)

空间复杂度

  • 递归栈深度:O(log n)(每次递归将问题规模减半)
  • 每层递归使用常数空间(几个局部变量)
  • 最终空间复杂度:O(log n)(不考虑输入数组)

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. 排序预处理:将区间按起始位置升序排序,确保重叠区间相邻
  2. 单次遍历合并:维护一个合并结果列表,依次检查每个区间:
    • 若当前区间与结果列表最后一个区间重叠,则合并
    • 否则,将当前区间加入结果列表
  3. 动态更新:合并时只更新结束位置,保持结果列表有序

解题步骤

  1. 边界处理:检查输入是否为空或只有一个区间
  2. 排序区间:使用Arrays.sort按起始位置升序排序
  3. 初始化结果列表:将第一个区间加入合并列表
  4. 遍历合并:
    • 获取当前区间和结果列表最后一个区间
    • 检查重叠:current[0] <= last[1]
    • 重叠则合并:last[1] = Math.max(current[1], last[1])
    • 不重叠则添加当前区间到结果列表
  5. 转换结果:将列表转换为二维数组返回

实现代码

class Solution {public int[][] merge(int[][] intervals) {// 边界判断if(intervals == null || intervals.length <= 1){return intervals;}// 按区间排序Arrays.sort(intervals, (a,b) -> a[0] -  b[0]);// 使用list存储合并结果List<int[]> merged = new ArrayList<>();// 添加第一个区间merged.add(intervals[0]);// 遍历剩余区间for(int i = 1; i < intervals.length; i++){int[] current = intervals[i];int[] last = merged.get(merged.size() - 1);// 检查重叠if(current[0] <= last[1]){last[1] = Math.max(current[1], last[1]);}else{// 不重叠,便添加merged.add(current);}} // 返回值,得转化为二维数组return merged.toArray(new int[merged.size()][]);}
}

复杂度分析
时间复杂度 O(n log n)
排序占主导:O(n log n) + 遍历合并:O(n) → 总体 O(n log n)
空间复杂度 O(n)
存储合并结果:最坏情况需要存储所有区间(无重叠时)
排序空间 O(log n) Java的Arrays.sort使用双轴快速排序,递归栈空间为O(log n)

补充
Arrays.sort(intervals, (a, b) -> a[0] - b[0]) 原理详解

  1. 方法基本功能
    Arrays.sort() 是 Java 中用于对数组进行排序的核心方法。当传入二维数组 intervals 和一个比较器时,它会对二维数组中的每个一维数组(即每个区间)进行排序。
  2. Lambda 表达式解析
    (a, b) -> a[0] - b[0] 是一个 Lambda 表达式,实现了 Comparator 接口:
    • a 和 b:代表两个待比较的区间(即两个一维数组)
    • a[0]:获取区间 a 的起始位置
    • b[0]:获取区间 b 的起始位置
    • a[0] - b[0]:计算两个区间起始位置的差值

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]

标签提示: 数组、前缀和

解题方法

解题思想
本题核心思想是分解乘法计算,将每个位置的结果拆分为前缀乘积和后缀乘积的乘积:

  • 前缀乘积:当前元素左侧所有元素的乘积
  • 后缀乘积:当前元素右侧所有元素的乘积
  • 最终结果:result[i] = 前缀乘积[i] × 后缀乘积[i]

关键优化点在于空间复用:

  • 利用结果数组存储前缀乘积
  • 使用单变量动态计算后缀乘积
  • 原地更新结果数组,避免额外空间开销

解题步骤

  • 初始化结果数组:创建长度为n的数组result,设置result[0] = 1(首个元素左侧无元素)
  • 计算前缀乘积:从索引1开始正向遍历数组
    • 执行 result[i] = result[i-1] * nums[i-1]
    • 存储当前元素左侧所有元素的乘积
  • 初始化后缀变量:创建变量hz并初始化为1(用于累积后缀乘积)
  • 计算后缀乘积并更新结果:从末尾开始反向遍历数组(索引n-1到0)
    • 执行 result[j] = result[j] * hz(前缀乘积 × 后缀乘积)
    • 更新 hz = hz * nums[j](累积当前元素到后缀乘积)
  • 返回结果数组:此时result中存储除自身外的乘积结果

实现代码

class Solution {public int[] productExceptSelf(int[] nums) {// 记录前缀乘积与后缀乘积int n = nums.length;int[] result = new int[n];result[0] = 1;for(int i = 1; i < n; i ++){result[i] = result[i - 1] * nums[i - 1];}int hz = 1;for(int j = n - 1; j >= 0; j --){result[j] = result[j] * hz;hz *= nums[j];}return result;}
}

复杂度分析
时间复杂度 O(n)
仅需两次线性遍历(正向计算前缀 + 反向计算后缀)
空间复杂度 O(1)
除结果数组外,仅使用常数个额外变量(hz)

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开始的。

  • 关键洞察:对于一个长度为 n 的数组,其缺失的第一个正数一定在 [1, n+1] 这个范围内。

    • 原因:如果数组包含了 1 到 n 的所有正整数,那么缺失的就是 n+1。
    • 否则,缺失的第一个正数必然是 1 到 n 中的某一个。
  • 策略选择:代码采用“空间换时间”的策略。它不直接在原数组上操作,而是引入一个辅助数据结构 HashSet 来快速判断一个数字是否存在。

    • 第一步:遍历数组,将所有正整数(且只存一次)存入 HashSet。HashSet 的优势在于其 contains 操作的平均时间复杂度为 O(1),非常适合快速查找。
    • 第二步:按照从小到大的顺序(从 1 开始),依次检查 [1, n] 范围内的每个数字是否在 HashSet 中。
    • 第三步:第一个在 HashSet 中找不到的数字,就是我们要找的答案。如果 1 到 n 都在 HashSet
      中,那么根据关键洞察,答案就是 n+1。

解题步骤

  • 初始化:

    • 创建一个 HashSet,命名为 set,用于存放数组中出现的正整数。
  • 收集正整数:

    • 遍历输入数组 nums 中的每一个元素 nums[i]。

    • 如果 nums[i] 是一个正整数(nums[i] > 0),就将其添加到 set 中。

    • HashSet 会自动处理重复值,确保集合中每个正整数只出现一次。

  • 顺序查找缺失数:

    • 启动一个循环,变量 i 从 1 开始,一直到 nums.length(包含)。

    • 在循环中,检查当前的数字 i 是否存在于 set 中(!set.contains(i))。

    • 如果 i 不存在于 set 中,说明这是第一个缺失的正整数,立即 return i 作为结果。

  • 处理边界情况:

    • 如果上面的循环正常结束(没有触发 return),这意味着 set 中包含了从 1 到 nums.length 的所有整数。

    • 因此,缺失的第一个正数就是 nums.length + 1。

    • 将 nums.length + 1 赋值给 result 并返回。
      实现代码

class Solution {public int firstMissingPositive(int[] nums) {int result = -1;Set<Integer> set = new HashSet<>();for(int i = 0; i < nums.length; i ++){if(nums[i] > 0 && !set.contains(nums[i])){set.add(nums[i]);}}for(int i = 1; i <= nums.length; i ++){if(!set.contains(i)){return i;}}result = nums.length + 1;return result;}
}

复杂度分析
时间复杂度: O(n)

  • 第一个 for 循环遍历了 n 个元素,每次 set.add() 操作的平均时间复杂度为 O(1)。所以这部分是 O(n)。
  • 第二个 for 循环最多执行 n 次,每次 set.contains() 操作的平均时间复杂度为 O(1)。所以这部分也是 O(n)。

空间复杂度: O(n)

  • 额外的空间主要来自于 HashSet。
  • 在最坏的情况下,如果数组中的所有 n 个元素都是不同的正整数,那么 set 将会存储 n 个元素。

个人学习总结

通过对这四道经典数组题的深入钻研,我深刻体会到,算法学习的核心在于从“解决问题”到“理解问题本质”的思维跃迁。这次学习之旅,让我收获了以下几点关键认知:

  1. 思维模型的重要性:面对一个问题,首先要建立正确的思维模型。例如,最大子数组和让我领悟到动态规划“化整为零,聚零为整”的精髓,通过定义pre状态,将全局最优解巧妙地转化为局部最优解的迭代。而分治法则提供了另一种“分解-解决-合并”的宏观视角,虽然实现更复杂,但拓展了解决问题的思路。
  2. 预处理与排序的力量:合并区间问题完美诠释了“磨刀不误砍柴工”。当数据杂乱无章时,直接处理往往寸步难行。而通过一次排序,将无序变为有序,就能让重叠的区间“自动靠拢”,将复杂问题简化为一次线性遍历。这让我认识到,排序是解决区间、序列类问题的强大预处理工具。
  3. 空间优化的精妙之处:除自身以外数组的乘积这道题是空间优化的典范。最初的想法可能是用两个数组分别存储前缀和后缀乘积,但这需要 O(n)的额外空间。而最优解通过复用输出数组,并用一个变量动态追踪后缀乘积,将空间复杂度降至O(1)。这种“变量滚动”和“原地操作”的思想,是写出高质量代码的关键。
  4. 问题约束的洞察力:缺失的第一个正数教会我如何挖掘题目中的隐藏信息。关键洞察在于“长度为 n 的数组,其缺失的第一个正数必然在 [1,n+1] 的范围内”。这一下就将无限的搜索空间缩小到了有限的 O(n) 范围,使得哈希表这种“空间换时间”的策略变得可行且高效。学会分析问题边界和约束,是找到突破口的重要一步。

总而言之,这次学习让我不再满足于仅仅写出能通过的代码,而是开始主动思考:这个问题最优解是什么?为什么它是最优的?它背后体现了哪种算法思想?还有没有其他解法,它们各自的优劣何在?这种刨根问底的学习方式,虽然耗时,但带来的思维成长是巨大的。未来,我将继续保持这种探索精神,在算法的世界里不断求索。

http://www.dtcms.com/a/428378.html

相关文章:

  • JAVA爬虫实战项目——OKX解析
  • 解除网站开发合同 首付款是否退长沙公司网站设计报价
  • DOM CSS:深入理解与高效运用
  • 闵行网站建设外包微信营销策略有哪些
  • 创建网站怎么赚钱的如何做一个好网站
  • Elasticsearch - Linux下使用Docker对Elasticsearch容器设置账号密码
  • 10. Spring AI + RAG
  • wordpress做自建站上海高品质网站建设公司
  • 网站开发如何找甲方网站后台维护费用
  • 智能化企业级CRM系统开发实战飞算JavaAI全流程体验
  • Matlab通过GUI实现点云的PCA配准(附最简版)
  • 10.17 上海 Google Meetup:从数据出发,解锁 AI 助力增长的新边界
  • 免费成品网站下载上海网站设计多少钱
  • 外管局网站上做预收登记制作公司网页图片
  • 【DockerFile+Nginx+DockerCompose】前后端分离式项目部署(docker容器化方式)
  • 快速傅里叶变换简介及python实现
  • 网站的实现怎么写重庆网站seo方法
  • 公司建设网站费用会计分录哈尔滨建筑工程招聘信息
  • 猫眼网站建设适合小县城开的加盟店
  • 网站开发 系统需求文档个性化定制网站
  • IDEA+SpringBoot实现远程DEBUG到本机
  • 网站建设与维护 目录开发公司前期手续流程
  • 物品奖励系统介绍
  • 广州站西手表公司彩页设计制作
  • sat4j中参数作用
  • 网站建设课程有哪些收获西安注册公司虚拟地址
  • 哪家做网站性价比高朋友圈自己做的网站
  • 建设网站需要掌握什么编程语言川菜餐馆网站建设模板美食餐厅企业建站php源码程序
  • 网上商城公司网站建设方案被网上教开网店的骗了怎么办
  • 网站域名无法访问国外建站工具