1. 问题理解与需求拆解
这道题目要求我们解决一个经典的数组元素查找问题:给定两个数组nums1和nums2,其中nums1是nums2的子集,我们需要为nums1中的每个元素找到它在nums2中对应位置右侧第一个比它大的元素。如果不存在这样的元素,则返回-1。
1.1 问题核心分析
理解这个问题的关键在于把握几个关键约束条件:
- nums1是nums2的子集,这意味着nums1中的所有元素都存在于nums2中
- 两个数组中的元素都是唯一的,没有重复值
- 我们需要在nums2中找到对应元素后,在其右侧寻找第一个更大的元素
举个例子,对于nums1 = [4,1,2]和nums2 = [1,3,4,2]:
- 数字4在nums2中的位置是索引2,其右侧只有数字2,比4小,所以返回-1
- 数字1在nums2中的位置是索引0,其右侧第一个比1大的数字是3
- 数字2在nums2中的位置是索引3,其右侧没有元素,所以返回-1
1.2 暴力解法思路
最直观的解法是暴力搜索:
- 遍历nums1中的每个元素
- 在nums2中找到该元素的位置
- 从该位置开始向右扫描,找到第一个更大的元素
这种解法的时间复杂度是O(n*m),其中n是nums1的长度,m是nums2的长度。虽然对于小规模数据可以工作,但显然不是最优解。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 单调栈解法详解
题目提示我们可以设计一个O(n+m)的解法,这就需要我们使用更高效的数据结构——单调栈。
2.1 单调栈原理
单调栈是一种特殊的栈结构,它保持栈内元素单调递增或单调递减。在这个问题中,我们使用单调递减栈:
- 从右向左遍历nums2
- 对于每个元素,弹出栈中所有比它小的元素
- 此时栈顶元素就是当前元素的"下一个更大元素"
- 将当前元素压入栈中
这种做法的精妙之处在于:
- 从右向左遍历保证了我们处理的是"右侧"元素
- 维护单调递减栈确保了我们找到的是"第一个"更大的元素
- 每个元素最多入栈和出栈一次,保证了线性时间复杂度
2.2 具体实现步骤
让我们详细解析给出的Java代码实现:
java复制class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
// 创建哈希表存储nums2中每个元素的下一个更大元素
HashMap<Integer, Integer> map = new HashMap<>();
// 使用单调栈来高效查找下一个更大元素
Stack<Integer> stack = new Stack<>();
// 从右向左遍历nums2
for(int i = nums2.length - 1; i >= 0; i--){
// 弹出栈中所有比当前元素小的元素
while(!stack.isEmpty() && stack.peek() <= nums2[i]){
stack.pop();
}
// 栈顶元素就是当前元素的下一个更大元素(如果栈不为空)
map.put(nums2[i], stack.isEmpty() ? -1 : stack.peek());
// 将当前元素压入栈中
stack.push(nums2[i]);
}
// 构建结果数组
int[] res = new int[nums1.length];
// 从哈希表中查询nums1中每个元素的结果
for(int i = 0; i < nums1.length; i++){
res[i] = map.get(nums1[i]);
}
return res;
}
}
2.3 时间复杂度分析
这个算法的时间复杂度确实是O(n+m):
- 构建哈希表的过程:O(m),每个元素最多入栈和出栈一次
- 构建结果数组的过程:O(n),每个查询都是O(1)的哈希表查找
- 总体时间复杂度:O(n+m)
空间复杂度是O(m),用于存储哈希表和栈。
3. 代码实现细节解析
3.1 关键数据结构选择
- 哈希表:使用HashMap来存储nums2中每个元素的下一个更大元素,这样可以在O(1)时间内查询结果
- 栈:使用Stack来实现单调递减栈,帮助我们高效找到下一个更大元素
3.2 边界条件处理
代码中已经考虑了以下边界条件:
- 当栈为空时,说明当前元素右侧没有更大的元素,返回-1
- nums1是nums2的子集,所以哈希表中一定包含nums1的所有元素
- 数组索引从0开始,符合题目要求
3.3 代码优化空间
虽然这个实现已经很高效,但仍有优化空间:
- 可以使用ArrayDeque代替Stack,因为Stack是线程安全的,而这里不需要线程安全
- 可以预先分配足够大的哈希表空间,避免扩容开销
优化后的代码可能如下:
java复制class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
Map<Integer, Integer> map = new HashMap<>(nums2.length);
Deque<Integer> stack = new ArrayDeque<>();
for(int i = nums2.length - 1; i >= 0; i--){
while(!stack.isEmpty() && stack.peek() <= nums2[i]){
stack.pop();
}
map.put(nums2[i], stack.isEmpty() ? -1 : stack.peek());
stack.push(nums2[i]);
}
int[] res = new int[nums1.length];
for(int i = 0; i < nums1.length; i++){
res[i] = map.get(nums1[i]);
}
return res;
}
}
4. 实际应用与变种问题
4.1 实际应用场景
这种"下一个更大元素"的问题在实际中有多种应用:
- 股票分析:找出某天后第一个更高的股价
- 温度预测:找出某天后第一个更高的温度
- 任务调度:找出下一个更适合执行的任务
4.2 相关变种问题
- 循环数组的下一个更大元素:将数组视为循环的,即最后一个元素的下一个是第一个元素
- 前一个更大元素:改为查找左侧第一个更大的元素
- 下一个更小元素:改为查找右侧第一个更小的元素
以循环数组为例,解决方案是遍历两次数组,使用取模运算处理索引:
java复制public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] res = new int[n];
Arrays.fill(res, -1);
Deque<Integer> stack = new ArrayDeque<>();
for(int i = 0; i < 2 * n; i++){
int num = nums[i % n];
while(!stack.isEmpty() && nums[stack.peek()] < num){
res[stack.pop()] = num;
}
if(i < n) stack.push(i);
}
return res;
}
5. 常见错误与调试技巧
5.1 常见错误类型
- 方向错误:从左向右遍历会导致找到的是左侧的更大元素
- 栈维护错误:没有正确处理等于的情况(如使用<而不是<=)
- 索引处理错误:在变种问题中容易忽略循环数组的处理
5.2 调试技巧
- 小规模测试:先用题目给的示例手动模拟算法执行过程
- 打印中间结果:在关键步骤打印栈和哈希表的状态
- 边界测试:测试空数组、单元素数组等特殊情况
例如,可以在代码中添加调试输出:
java复制for(int i = nums2.length - 1; i >= 0; i--){
System.out.println("Processing: " + nums2[i]);
while(!stack.isEmpty() && stack.peek() <= nums2[i]){
System.out.println("Popping: " + stack.peek());
stack.pop();
}
int nextGreater = stack.isEmpty() ? -1 : stack.peek();
System.out.println("Next greater for " + nums2[i] + " is " + nextGreater);
map.put(nums2[i], nextGreater);
stack.push(nums2[i]);
System.out.println("Current stack: " + stack);
}
6. 性能优化与进阶思考
6.1 进一步优化思路
- 空间优化:如果nums1和nums2的数值范围有限,可以用数组代替哈希表
- 并行处理:对于大规模数据,可以考虑并行处理不同区间的元素
- 预处理优化:如果nums2固定而nums1变化频繁,可以预处理nums2的结果
6.2 算法选择考量
选择单调栈解法是因为:
- 时间复杂度最优:O(n+m)是最佳可能的时间复杂度
- 空间复杂度合理:O(m)的空间使用是可以接受的
- 实现简洁:代码量少且逻辑清晰
对于特别大的数据集(如nums2长度超过10^6),可能需要考虑:
- 内存映射文件处理超大数组
- 分布式计算框架处理
- 更高效的数据结构如位图
7. 不同语言实现对比
虽然我们主要讨论了Java实现,但这个问题在其他语言中也有类似的解法:
7.1 Python实现
python复制def nextGreaterElement(nums1, nums2):
mapping = {}
stack = []
for num in reversed(nums2):
while stack and stack[-1] <= num:
stack.pop()
mapping[num] = stack[-1] if stack else -1
stack.append(num)
return [mapping[num] for num in nums1]
Python实现更简洁,利用了列表的灵活性和负索引特性。
7.2 JavaScript实现
javascript复制var nextGreaterElement = function(nums1, nums2) {
const map = new Map();
const stack = [];
for(let i = nums2.length - 1; i >= 0; i--) {
while(stack.length && stack[stack.length-1] <= nums2[i]) {
stack.pop();
}
map.set(nums2[i], stack.length ? stack[stack.length-1] : -1);
stack.push(nums2[i]);
}
return nums1.map(num => map.get(num));
};
JavaScript的实现与Java类似,但利用了数组的高阶函数map。
7.3 C++实现
cpp复制vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int, int> mapping;
stack<int> st;
for(int i = nums2.size() - 1; i >= 0; --i) {
while(!st.empty() && st.top() <= nums2[i]) {
st.pop();
}
mapping[nums2[i]] = st.empty() ? -1 : st.top();
st.push(nums2[i]);
}
vector<int> res;
for(int num : nums1) {
res.push_back(mapping[num]);
}
return res;
}
C++实现使用了STL的stack和unordered_map,性能通常比Java更好。
8. 单元测试与验证
为了确保我们的解法正确,应该编写全面的测试用例:
8.1 基础测试用例
java复制@Test
public void testBasicCases() {
Solution solution = new Solution();
assertArrayEquals(new int[]{-1,3,-1},
solution.nextGreaterElement(new int[]{4,1,2}, new int[]{1,3,4,2}));
assertArrayEquals(new int[]{3,-1},
solution.nextGreaterElement(new int[]{2,4}, new int[]{1,2,3,4}));
}
8.2 边界测试用例
java复制@Test
public void testEdgeCases() {
Solution solution = new Solution();
// 单元素数组
assertArrayEquals(new int[]{-1},
solution.nextGreaterElement(new int[]{1}, new int[]{1}));
// nums1和nums2相同
assertArrayEquals(new int[]{2,3,4,-1},
solution.nextGreaterElement(new int[]{1,2,3,4}, new int[]{1,2,3,4}));
// 所有元素相同
assertArrayEquals(new int[]{-1,-1},
solution.nextGreaterElement(new int[]{1,1}, new int[]{1,1,1}));
}
8.3 性能测试用例
java复制@Test(timeout = 1000)
public void testPerformance() {
Solution solution = new Solution();
int[] nums1 = new int[100000];
int[] nums2 = new int[1000000];
// 填充测试数据...
int[] result = solution.nextGreaterElement(nums1, nums2);
// 验证结果...
}
9. 实际工程应用建议
在实际工程项目中应用这种算法时,建议:
- 封装成工具类:将算法封装成独立的工具类,便于复用
- 添加详细注释:解释算法原理和关键步骤
- 性能监控:在大规模使用时监控执行时间和内存消耗
- 异常处理:添加对输入参数的校验和异常处理
例如,可以创建一个ArrayUtils类:
java复制public class ArrayUtils {
/**
* 查找nums1中每个元素在nums2中的下一个更大元素
* @param nums1 查询数组,必须是nums2的子集
* @param nums2 目标数组
* @return 结果数组,长度与nums1相同
* @throws IllegalArgumentException 如果nums1不是nums2的子集
*/
public static int[] nextGreaterElement(int[] nums1, int[] nums2) {
// 实现代码...
}
}
10. 学习资源与延伸阅读
为了更深入理解单调栈和相关算法,推荐以下资源:
-
书籍:
- 《算法导论》 - 基础算法理论
- 《编程珠玑》 - 算法设计技巧
- 《算法图解》 - 直观的算法解释
-
在线课程:
- LeetCode探索卡片 - 单调栈专题
- Coursera算法课程 - Princeton大学
- 牛客网算法精讲
-
相关LeetCode题目:
-
- 下一个更大元素 II(循环数组)
-
- 每日温度(温度变化问题)
-
- 柱状图中最大的矩形(单调栈应用)
-
理解这类问题的关键在于多练习、多思考。建议从简单题目开始,逐步挑战更复杂的问题,同时注意总结各种解法的优缺点和适用场景。
