难度参考
难度:困难
难度与分类由我所参与的培训课程提供,但需 要注意的是,难度与分类仅供参考。且所在课程未提供测试平台,故实现代码主要为自行测试的那种,以下内容均为个人笔记,旨在督促自己认真学习。
题目
给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字x的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出-1。
示例1:
输入:[1,2,1]
输出:[2,-1,2]
解释:第一个1的下一个更大的数是2;数字2找不到下一个更大的数;第二个1的下一个最大的数需要循环搜索,结果也是 2。
提示:1<= nums.length <=10^4
-10^9 <= nums[i] <= 10~9
思路
1. 初始化:创建一个结果数组res,大小与输入数组nums相同,初始值全部设为-1。这是因为如果某个元素没有下一个更大的元素,其对应的结果应该保持为`-1`。
2. 单调栈:使用一个栈来存储数组索引。栈中的索引对应的元素值是单调递减的。这意味着栈顶元素是目前遍历到的、尚未找到下一个更大元素的、最小的元素。
3. 循环遍历:由于数组是循环的,我们需要遍历数组两倍的长度,以确保每个元素都能够在循环中找到其下一个更大的元素(如果存在的话)。实际上,对于索引的处理,我们使用`i % n`来保证索引不会越界,其中`i`是当前遍历到的位置,n是数组的长度。
4. 处理栈:对于当前遍历到的元素nums[i % n],如果栈不为空且当前元素大于栈顶元素对应的值,那么我们找到了栈顶索引对应元素的下一个更大元素。此时,将栈顶元素弹出,并在结果数组中更新该位置的值为当前元素。重复此过程直到栈为空或者当前元素不再大于栈顶元素对应的值。
5. 入栈:在第一次遍历数组时(即i < n),将当前元素的索引入栈。这是为了保证每个元素都能够被考虑到,同时避免在第二次遍历时重复处理。
6. 返回结果:完成上述步骤后,res数组中存储的就是每个元素对应的下一个更大元素。返回res作为结果。
示例
假设我们有一个数组nums = [1, 2, 3, 4, 3],我们要找到每个元素的下一个更大元素。
初始状态
- nums = [1, 2, 3, 4, 3]
- res = [-1, -1, -1, -1, -1] (初始时,假设没有元素有下一个更大的元素)
- 栈 = [] (空栈)
第一轮遍历
1. 索引0 (nums[0] = 1)
- 栈 = [0]
- res未改变
2. 索引1 (nums[1] = 2)
- 栈顶元素对应nums[0] < nums[1],找到了nums[0]的下一个更大元素,弹出索引0,res[0] = 2
- 栈 = [1]
- res = [2, -1, -1, -1, -1]
3. 索引2 (nums[2] = 3)
- 重复上述过程,nums[1] < nums[2],弹出索引1,res[1] = 3
- 栈 = [2]
- res = [2, 3, -1, -1, -1]
4. 索引3 (nums[3] = 4)
- 同样,nums[2] < nums[3],弹出索引2,res[2] = 4
- 栈 = [3]
- res = [2, 3, 4, -1, -1]
5. 索引4 (nums[4] = 3)
- nums[3] > nums[4],无法弹出元素,直接将索引4入栈
- 栈 = [3, 4]
- res未改变
第二轮遍历(为了处理循环数组)
6. 索引0 (nums[0] = 1)
- nums[4] > nums[0],但索引4已经处理过,我们继续。
- 栈 = [3, 4, 0] (实际上,第二轮不再添加索引,这里只是为了说明过程)
7. 索引1 (nums[1] = 2)
- 同上,继续。
8. 索引2 (nums[2] = 3)
- 同上,继续。
9. 索引3 (nums[3] = 4)
- nums[3]是最大元素,继续。
10. 索引4 (nums[4] = 3)
- 这时,我们实际上已经处理完毕,因为栈中剩余的元素(如果有)在第一轮遍历时就已经找不到更大的元素了,第二轮遍历主要是为了循环数组的处理。
结果
- 最终,res = [2, 3, 4, -1, 3]
- nums[0]的下一个更大元素是2
- nums[1]的下一个更大元素是3
- nums[2]的下一个更大元素是4
- nums[3]没有下一个更大元素,保持为-1
- nums[4]的下一个更大元素回到数组开头,是1后面的2
这个例子展示了如何通过使用单调栈和循环两倍数组长度的方法来找到循环数组中每个元素的下一个更大元素。希望这个具体的步骤和示例能帮助你更好地理解这个算法的工作原理。
梳理
这种方法之所以能够实现,是因为单调栈和循环两遍数组的策略共同解决了查找每个元素的下一个更大元素这一问题的关键点。下面分别解释这两个策略的作用:
单调栈的作用
1. 保持单调性:单调栈通过仅在特定条件下(本例中为遇到更大元素时)弹出元素,保持了栈内元素的单调递减(或递增)顺序。这意味着栈顶元素始终是当前考虑范围内最小(或最大)的元素。因此,当遍历数组时,我们可以立即知道一个元素的下一个更大元素是什么——只需查看它在栈中的位置。
2. 高效查找:利用栈的特性,我们可以在O(1)的时间复杂度内访问和弹出栈顶元素,同时,由于栈的单调性,我们可以确保每个元素最多只被压入和弹出栈一次,这使得整个算法的时间复杂度保持在O(n)。
循环两遍数组的作用
1. 处理循环数组:循环数组意味着数组的尾部和头部是相连的。在单遍遍历中,我们只能找到元素右侧的下一个更大元素,但对于靠近数组末尾的元素,其下一个更大元素可能出现在数组的前部。通过循环两遍数组,我们确保了每个元素都有机会与其后的所有元素(包括循环回来的前部元素)进行比较。
2. 保证完整性:即使是非循环数组,单遍遍历也可能遗漏一些元素的下一个更大元素(尤其是当这些元素的下一个更大元素位于它们自身之后较远的位置时)。循环两遍数组确保了每个元素都被充分比较,从而找到所有可能的下一个更大元素。
结合使用单调栈和循环两遍数组的策略,我们可以有效地解决寻找循环数组中每个元素的下一个更大元素的问题,无论这个更大的元素位于当前元素的右侧还是通过循环回到数组前部的情况。这种方法的高效性和完整性使其成为解决此类问题的强大工具。
代码
#include <iostream> // 引入输入输出流库
#include <vector> // 引入向量库
#include <stack> // 引入栈库
using namespace std; // 使用标准命名空间
vector<int> nextGreaterElements(vector<int>& nums) {
int n = nums.size(); // 获取数组大小
vector<int> res(n, -1); // 初始化结果数组,默认值为-1
stack<int> s; // 使用栈来存储元素的索引
// 由于是循环数组,所以遍历两次数组,模拟循环的效果
for (int i = 0; i < n * 2; i++) {
int num = nums[i % n]; // 获取当前元素的索引(由于循环遍历,需要对n取模)
// 如果栈不为空,并且当前元素大于栈顶元素对应的值
// 则找到了栈顶元素的下一个更大元素
while (!s.empty() && nums[s.top()] < num) {
res[s.top()] = num; // 更新栈顶元素对应的结果
s.pop(); // 弹出栈顶元素
}
// 只有在第一次遍历时才将索引入栈
// 避免重复处理同一个元素
if (i < n) {
s.push(i); // 将索引入栈
}
}
return res; // 返回结果向量
}
int main() {
vector<int> nums = {1, 2, 1}; // 初始化输入数组
vector<int> res = nextGreaterElements(nums); // 调用函数获取结果
cout << "["; // 输出结果
for (int i = 0; i < res.size(); i++) {
cout << res[i]; // 输出元素
if (i < res.size() - 1) cout << ", "; // 输出逗号(除了最后一个元素)
}
cout << "]" << endl; // 输出最后的中括号并换行
return 0; // 返回主函数结束
}
时间复杂度: O(n)
。
空间复杂度: O(n)
。