深入解析 LeetCode 数组经典问题:删除每行中的最大值与找出峰值

引入

在算法刷题的广阔领域中,数组类问题始终占据着重要地位,它们常常围绕"元素选择""条件筛选"等核心逻辑展开。这类问题不仅考察开发者对数组遍历、排序、条件判断等基础操作的掌握程度,更注重解题思路的合理性与效率的最优化。本文将聚焦 LeetCode 上两个典型的数组问题------"2500. 删除每行中的最大值"与"2951. 找出峰值",从问题本质出发,细致拆解解题逻辑,深入解析代码实现的每一处细节,并拓展相关的知识点与进阶应用场景。无论你是刚踏入算法领域的初学者,还是希望巩固数组操作基础的开发者,通过本文的学习,都能进一步提升对数组类问题的掌控力,掌握"排序优化"与"条件筛选"的核心思想,为攻克更复杂的算法难题积累宝贵经验。

一、2500. 删除每行中的最大值

1.1 问题描述与分析

1.1.1 问题描述

给你一个 m x n 大小的矩阵 grid,由若干正整数组成。执行下述操作,直到 grid 变为空矩阵:

从每一行删除值最大的元素。如果存在多个这样的值,删除其中任何一个。

将删除元素中的最大值与答案相加。

注意每执行一次操作,矩阵中列的数据就会减 1。返回执行上述操作后的答案。

1.1.2 示例与分析

示例(假设输入)

grid = [[1,2,4],[3,3,1]]

操作过程:

第一次操作:每行删除最大值(第一行删 4,第二行删 3),删除元素的最大值是 4,sum = 4;矩阵变为 [[1,2],[3,1]]。

第二次操作:每行删除最大值(第一行删 2,第二行删 3),删除元素的最大值是 3,sum = 4 + 3 = 7;矩阵变为 [[1],[1]]。

第三次操作:每行删除最大值(两行都删 1),删除元素的最大值是 1,sum = 7 + 1 = 8;矩阵变为空。

最终返回 8。

1.1.3 核心需求与约束

核心需求:通过多次"每行删最大值,累加删除元素的最大值"的操作,直到矩阵为空,最终得到累加和。

关键观察:每次操作需要获取每行的当前最大值,再从这些最大值中取最大的进行累加。若对每行进行排序,每行的最大值会处于固定位置(如升序排序后每行的最后一位),后续按列遍历可高效获取每行当前最大值。

约束条件:矩阵由正整数组成,操作次数等于矩阵的列数(每次操作列数减 1,直到列数为 0)。

1.2 解题思路演进

1.2.1 排序 + 按列遍历策略

核心思路:先对矩阵的每一行进行升序排序,使每行的最大值位于行的末尾。之后按列遍历矩阵,每一列中取各行的元素(即每行当前的最大值),将这些元素的最大值累加到结果中。

java 复制代码
class Solution {

public int deleteGreatestValue(int[][] grid) {

for (int i = 0; i < grid.length; i++) {

Arrays.sort(grid[i]);

}

int sum = 0;

for (int c = 0; c < grid[0].length; c++) {

int currentMax = 0;

for (int r = 0; r < grid.length; r++) {

currentMax = Math.max(currentMax, grid[r][c]);

}

sum += currentMax;

}

return sum;

}

}

1.2.2 复杂度分析

时间复杂度:,其中 m是矩阵的行数,n 是矩阵的列数。对每一行排序的时间复杂度为 O(n log n),共 m 行,排序总时间为;按列遍历矩阵,外层循环n次,内层循环m次,时间复杂度是 。整体由排序操作主导。

空间复杂度O(log n),源于排序过程的辅助空间开销(Java 中 Arrays.sort 对基本类型数组采用双轴快速排序,空间复杂度为 O(log n))。

该方法逻辑清晰高效,通过排序将"每行找最大值"的操作从 O(n) 优化为 O(1),大幅降低时间开销。

1.3 代码逐句解析

java 复制代码
class Solution {

public int deleteGreatestValue(int[][] grid) {

// 遍历每一行,对行内元素进行升序排序

for (int i = 0; i < grid.length; i++) {

Arrays.sort(grid[i]);

}

// 初始化累加和 sum

int sum = 0;

// 按列遍历,c 表示当前列索引

for (int c = 0; c < grid[0].length; c++) {

// 初始化当前列的最大值 currentMax

int currentMax = 0;

// 遍历每一行,r 表示当前行索引

for (int r = 0; r < grid.length; r++) {

// 更新当前列的最大值

currentMax = Math.max(currentMax, grid[r][c]);

}

// 将当前列的最大值累加到 sum

sum += currentMax;

}

// 返回最终累加和

return sum;

}

}

1.3.1 关键细节说明

Arrays.sort(grid[i]):对每行升序排序,确保每行最大值位于行的最后一位(grid[i][n-1]),后续按列遍历时,grid[r][c] 即为第 r 行第 c 次操作时的最大值。

按列遍历次数:等于矩阵的列数 grid[0].length,因每次操作删除一列元素,操作次数与列数一致。

列内最大值获取:通过内层循环遍历每一行的当前列元素,用 Math.max 逐步更新得到该列最大值,此值为本次操作需累加的数值。

1.4 排序与数组遍历知识拓展

1.4.1 排序算法的选择与适用场景

本题使用 Java 内置的 Arrays.sort(底层双轴快速排序)对每行排序。快速排序平均时间复杂度 O(n log n),空间复杂度 O(log n),在多数场景下是数组排序的最优选择之一。

适用场景:需对数组全排序且数据规模适中(如本题每行的列数 n)时,快速排序性能高效。

其他排序算法对比:

冒泡、插入排序:时间复杂度 O(n^2),n 较大时性能极差,不适用于本题。

归并排序:时间复杂度 O(nlog n),但空间复杂度 O(n),相比快速排序空间开销更大,本题无需额外空间优化,故快速排序更优。

1.4.2 二维数组的遍历方式

本题采用"按列遍历"方式,需确保每一行长度相同(矩阵规整)。按列遍历外层循环控制列,内层循环控制行,虽缓存友好性稍差,但逻辑上贴合"每次操作处理一列"的需求。

按行遍历 vs 按列遍历:

按行遍历:外层循环控制行,内层循环控制列,缓存友好性更好(数组在内存中按行连续存储)。

按列遍历:外层循环控制列,内层循环控制行,本题因逻辑需要采用,需保证矩阵规整。

1.5 测试用例分析

测试用例输入 grid 输出 说明
[[1,2,4],[3,3,1]] 8 三次操作累加 4、3、1
[[5]] 5 只有一行一列,一次操作直接累加 5
[[1,1,1],[1,1,1],[1,1,1]] 3 每次操作删除的最大值都是 1,共三列,累加 1+1+1=3
[[10,8,6],[5,4,3],[2,1,0]] 24 排序后每行升序为 [6,8,10][3,4,5][0,1,2];按列累加 10+5+2=24

二、2951. 找出峰值

2.1 问题描述与分析

2.1.1 问题描述

给你一个下标从 0 开始的数组 mountain。你的任务是找出数组 mountain 中的所有峰值。以数组形式返回给定数组中峰值的下标,顺序不限。

注意:

峰值是指一个严格大于其相邻元素的元素。

数组的第一个和最后一个元素不是峰值。

2.1.2 示例与分析

示例:

输入:mountain = [2,4,1,5,3]

输出:[1,3]

解释:下标 1(元素 4,左右是 2 和 1,严格大于)、下标 3(元素 5,左右是 1 和 3,严格大于)是峰值。

2.1.3 核心需求与约束

核心需求:筛选出数组中所有"严格大于左右相邻元素"的元素下标,且排除数组首尾元素。

关键观察:对于下标 i(1 ≤ i ≤ mountain.length - 2`),只需判断 mountain[i] > mountain[i-1] 且 mountain[i] > mountain[i+1] 即可确定是否为峰值。

约束条件:数组长度至少为 3 才可能存在峰值(否则首尾占满,无中间元素),若数组长度小于 3,直接返回空数组。

2.2 解题思路演进

2.2.1 线性遍历筛选策略

核心思路:遍历数组中所有可能的下标(从 1 到 mountain.length - 2),对每个下标 i 判断是否满足"严格大于左右相邻元素"的条件,若满足则将下标加入结果列表。

java 复制代码
class Solution {

public List findPeaks(int[] mountain) {

ArrayList peaks = new ArrayList<>();

for (int i = 1; i < mountain.length - 1; i++) {

if (mountain[i] > mountain[i-1] && mountain[i] > mountain[i+1]) {

peaks.add(i);

}

}

return peaks;

}

}

2.2.2 复杂度分析

时间复杂度:O(n),其中 n 是数组 mountain 的长度。只需遍历一次数组的中间元素(从下标 1 到 n-2),每次判断为 O(1)操作,总时间复杂度为线性级别。

空间复杂度:O(1)(不考虑结果列表的存储,若考虑则为 O(p),其中 p 是峰值的数量p ≤ n/2)。

该方法逻辑直观、实现简单,且时间复杂度最优,因要判断每个中间元素是否为峰值,必须至少遍历一次所有中间元素,线性时间复杂度是该问题的下限。

2.3 代码逐句解析

java 复制代码
class Solution {

public List findPeaks(int[] mountain) {

ArrayList peaks = new ArrayList<>();

for (int i = 1; i < mountain.length - 1; i++) {

if (mountain[i] > mountain[i-1] && mountain[i] > mountain[i+1]) {

peaks.add(i);

}

}

return peaks;

}

}

2.3.1 关键细节说明

循环范围:i 的取值范围是 [1, mountain.length - 2],确保只判断中间元素,排除首尾元素(题目规定首尾不是峰值)。

严格大于的条件:必须同时满足 mountain[i] > mountain[i-1] 和 mountain[i] > mountain[i+1],"严格大于"是题目明确要求,若写成"大于等于"会错误包含非峰值元素。

结果列表的顺序:题目允许返回顺序不限,直接按遍历顺序添加即可,无需额外排序。

2.4 条件筛选与数组边界知识拓展

2.4.1 边界条件的重要性

在数组类问题中,边界条件的处理是出错高频点。本题中:

若数组长度小于 3(如 mountain.length < 3),循环不会执行,直接返回空列表,符合"首尾不是峰值,无中间元素"的逻辑。

若数组长度等于 3(如 [1,3,2]),只需判断下标 1 的元素是否为峰值,逻辑正确。

实际开发中处理数组时需时刻关注"索引越界"问题,确保循环的起始和终止条件严格符合逻辑需求。

2.4.2 类似问题的条件变种

本题核心是"严格大于左右"的条件,实际场景中存在多种变种,需根据需求调整判断逻辑:

大于等于左,严格大于右:适用于"山脉数组"中找峰顶的场景(如 LeetCode 852. 山脉数组的峰顶索引)。

严格大于左,大于等于右:可用于某些"下降沿"前的峰值判断。

大于左右其中一个:适用于找"局部极大值"(不要求严格大于两者)的场景。

2.5 测试用例分析

测试用例输入 mountain 输出 说明
[2,4,1,5,3] [1,3] 下标 1(4>2 且 4>1)、下标 3(5>1 且 5>3)是峰值
[1,2,3,4,5] [] 无元素严格大于右邻居,无峰值
[5,4,3,2,1] [] 无元素严格大于左邻居,无峰值
[1,5,3,6,4,7,2] [1,3,5] 下标 1(5>1 且 5>3)、下标 3(6>3 且 6>4)、下标 5(7>4 且 7>2)是峰值
[3] [] 数组长度小于 3,无峰值

三、两个问题的异同与总结

3.1 相同点分析

数组操作核心:均以数组(或二维数组)为基础数据结构,解题依赖数组的遍历操作,体现了数组作为线性结构在算法问题中的通用性。

条件判断关键:都需要通过明确的条件判断来筛选元素或确定操作逻辑("每行最大值的列最大值""严格大于左右元素"),条件判断的准确性直接影响代码正确性。

时间复杂度优化:均通过合理的遍历策略或预处理(如排序)将时间复杂度优化至线性或线性对数级别,避免了低效的嵌套循环(如 O(n^2) 复杂度)。

3.2 不同点分析

特征 删除每行中的最大值(2500) 找出峰值(2951)
数据结构维度 二维数组(矩阵) 一维数组
核心算法思想 排序优化 + 按列遍历 线性筛选 + 条件判断
时间复杂度主导因素 排序操作(\(O(m \times n \log n)\)) 线性遍历(\(O(n)\))
空间复杂度 \(O(\log n)\)(排序辅助空间) \(O(1)\)(或 \(O(p)\),p 为峰值数量)
适用场景拓展 矩阵元素的批量筛选与累加 一维序列的特征点(峰值)识别

3.3 编程思想提炼

排序的价值:在"删除每行中的最大值"问题中,排序将"动态找最大值"转化为"静态取固定位置元素",大幅降低后续遍历的时间开销,体现了"预处理优化后续操作"的编程思想。

条件筛选的严谨性:在"找出峰值"问题中,严格的条件判断(`mountain[i] > mountain[i-1] && mountain[i] > mountain[i+1]`)是确保结果正确的关键,要求开发者精准理解题目定义(如"严格大于""首尾排除")。

问题抽象能力:将实际问题抽象为数组的遍历、排序、条件判断等基础操作,是解决算法问题的核心能力。例如,将"矩阵操作"抽象为"每行排序后按列取最大值累加",将"找峰值"抽象为"遍历中间元素并判断左右大小"。

3.4 拓展练习与进阶思考

3.4.1 2500. 删除每行中的最大值 拓展

进阶问题:若矩阵中的元素可能为负数,且操作改为"删除每行中的最小值,累加删除元素中的最小值",如何调整解法?

思路:将"升序排序"改为"降序排序"(或升序排序后取行的首位元素),之后按列遍历取每行的当前最小值(即排序后行的首位元素),再累加这些最小值中的最小值。

代码示例:

java 复制代码
class Solution {

public int deleteSmallestValue(int[][] grid) {

for (int i = 0; i < grid.length; i++) {

Arrays.sort(grid[i]);

reverse(grid[i]);

}

int sum = 0;

for (int c = 0; c < grid[0].length; c++) {

int currentMin = Integer.MAX_VALUE;

for (int r = 0; r < grid.length; r++) {

currentMin = Math.min(currentMin, grid[r][c]);

}

sum += currentMin;

}

return sum;

}

private void reverse(int[] arr) {

int left = 0, right = arr.length - 1;

while (left < right) {

int temp = arr[left];

arr[left] = arr[right];

arr[right] = temp;

left++;

right--;

}

}

}

3.4.2 2951. 找出峰值 拓展

进阶问题:若数组是环形的(即第一个元素的左邻居是最后一个元素,最后一个元素的右邻居是第一个元素),如何找出所有峰值?

思路:调整边界条件,对于下标 0,其左邻居是 mountain.length - 1,右邻居是 1;对于下标 mountain.length - 1,其左邻居是 mountain.length - 2,右邻居是 0;中间下标仍判断左右相邻元素。

代码示例:

java 复制代码
class Solution {

public List findCircularPeaks(int[] mountain) {

ArrayList peaks = new ArrayList<>();

int n = mountain.length;

for (int i = 0; i < n; i++) {

int left = (i - 1 + n) % n;

int right = (i + 1) % n;

if (mountain[i] > mountain[left] && mountain[i] > mountain[right]) {

peaks.add(i);

}

}

return peaks;

}

}

3.4.3 进阶思考

数据规模与算法选择:当二维数组的行数m和列数n都达到 10^4 时,的排序操作是否可行?若不可行,是否有更高效的方法?

分析:,在时间限制严格的场景下可能超时。若数据规模极大,需结合问题其他特性(如元素范围)进行优化。

多条件峰值判断:若峰值的定义改为"大于左邻居或大于右邻居",如何高效统计此类"局部极大值"的数量?

思路:遍历数组时,对每个元素(除首尾外)判断 mountain[i] > mountain[i-1] || mountain[i] > mountain[i+1],统计满足条件的元素数量,时间复杂度仍为 O(n)。

3.5 总结

本文对"2500. 删除每行中的最大值"与"2951. 找出峰值"两个 LeetCode 数组问题从问题描述、思路演进、代码细节到知识拓展进行了全方位解析。

"删除每行中的最大值"展示了**排序优化**在数组操作中的强大作用,通过排序将动态操作转化为静态操作,大幅提升算法效率。"找出峰值"体现了线性筛选与条件判断的核心思想,通过遍历和严格条件判断精准筛选峰值元素。

两个问题均强调**问题本质拆解**与**边界条件处理**的重要性。只有准确理解问题核心需求与约束条件,才能选择最适合的算法,写出高效且健壮的代码。

希望读者通过本文学习,不仅掌握这两个问题的解题方法,更能培养"从问题到算法"的思维链路,在面对其他算法问题时能逐步推进,提升解决实际问题的能力。

相关推荐
计算衎2 小时前
.c .o .a .elf .a2l hex map 这些后缀文件的互相之间的联系和作用
开发语言·elf·gcc·c/c++·a2l
ysyxg2 小时前
设计模式-策略模式
java·开发语言
AI科技星2 小时前
宇宙的几何诗篇:当空间本身成为运动的主角
数据结构·人工智能·经验分享·算法·计算机视觉
Felix_XXXXL3 小时前
Spring Security安全框架原理与实战
java·后端
前端小L3 小时前
二分查找专题(二):lower_bound 的首秀——精解「搜索插入位置」
数据结构·算法
一抓掉一大把3 小时前
秒杀-StackExchangeRedisHelper连接单例
java·开发语言·jvm
升鲜宝供应链及收银系统源代码服务3 小时前
升鲜宝生鲜配送供应链管理系统--- 《多语言商品查询优化方案(Redis + 翻译表 + 模糊匹配)》
java·数据库·redis·bootstrap·供应链系统·生鲜配送·生鲜配送源代码
青山的青衫3 小时前
【JavaWeb】Tlias后台管理系统
java·web
星释3 小时前
Rust 练习册 :Minesweeper与二维数组处理
开发语言·后端·rust