栈(Stack)又名堆栈,它是一种重要的数据结构。从数据结构角度看,栈也是线性表,其特殊性在于栈的基本操作是线性表操作的子集,它是操作受限的线性表,因此,可称为限定性的数据结构。
栈被限定仅在表尾进行插入或删除操作。表尾称为栈顶,相应地,表头称为栈底。所以栈具有“后进先出”(LIFO)的特点。
栈的基本操作除了在栈顶进行插入(入栈,push)和删除(出栈,pop)外,还有栈的初始化,判断是否为空以及取栈顶元素等。

队列(Queue)是一种先进先出(FIFO,First-In-First-Out)的线性表。
在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为 rear)进行插入操作,在前端(称为 front)进行删除操作。
队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。

双端队列,是限定插入和删除操作在表的两端进行的线性表。
队列的每一端都能够插入数据项和移除数据项。
相对于普通队列,双端队列的入队和出队操作在两端都可进行。所以,双端队列同时具有队列和栈的性质。

优先队列不再遵循先入先出的原则,而是分为两种情况:
最大优先队列,无论入队顺序,当前最大的元素优先出队。
最小优先队列,无论入队顺序,当前最小的元素优先出队。
比如有一个最大优先队列,它的最大元素是8,那么虽然元素8并不是队首元素,但出队的时候仍然让元素8首先出队:

要满足以上需求,利用线性数据结构并非不能实现,但是时间复杂度较高,需要遍历所有元素,最坏时间复杂度O(n),并不是最理想的方式。
因此,一般是用大顶堆(Max Heap,有时也叫最大堆)来实现最大优先队列,每一次入队操作就是堆的插入操作,每一次出队操作就是删除堆顶节点。
入队操作:
1. 插入新节点5

2. 新节点5上浮到合适位置。

出队操作:
1.把原堆顶节点10“出队”

2.最后一个节点1替换到堆顶位置

3.节点1下沉,节点9成为新堆顶

二叉堆节点上浮和下沉,操作次数不会超过数的深度,所以时间复杂度都是O(logn)。那么优先队列,入队和出队的时间复杂度,也是O(logn)。
使用队列实现栈的下列操作:
注意:
这道题目涉及到栈和队列两种数据结构。它们的共同特点是,数据元素以线性序列的方式存储;区别在于,元素进出的方式不同。
队列本身对数据元素的保存,是完全符合数据到来次序的,同时也保持这个顺序依次出队。而弹栈操作的实现,是要删除最后进入的数据,相当于反序弹出。
实现的基本思路是,我们可以用一个队列保存当前所有的数据,以它作为栈的物理基础;而为了保证后进先出,我们在数据入队之后,就把它直接移动到队首。
可以增加一个队列来做辅助。我们记原始负责存储数据的队列为queue1,新增的辅助队列为queue2。
queue1:a b
queue2:x
queue1:
queue2:x a b
queue1:x a b
queue2:
而对于弹栈操作,只要直接让queue1执行出队操作,删除队首元素就可以了。
- package com.webcode.stack_and_queue;
-
- import java.util.LinkedList;
- import java.util.Queue;
- import java.util.Stack;
-
- // 使用两个队列实现自定义栈
- public class MyStack {
- // 定义两个队列
- Queue
queue1; - Queue
queue2; -
- public MyStack() {
- queue1 = new LinkedList<>();
- queue2 = new LinkedList<>();
- }
-
- // 入栈方法
- public void push(int x){
- // 1. 把x保存到queue2中
- queue2.offer(x);
-
- // 2. 将queue1中所有元素依次出队,然后放入queue2
- while (!queue1.isEmpty()){
- queue2.offer( queue1.poll() );
- }
-
- // 3. 交换两个队列
- Queue
temp = queue1; - queue1 = queue2;
- queue2 = temp;
- }
-
- // 出栈操作
- public int pop(){
- // queue1出队就是出栈
- return queue1.poll();
- }
-
- // 获取栈顶元素
- public int top(){
- return queue1.peek();
- }
-
- // 判断为空
- public boolean empty(){
- return queue1.isEmpty();
- }
- }
复杂度分析
时间复杂度:入栈操作 O(n),其余操作都是 O(1)。
push:入栈操作,需要将queue1中的 n 个元素出队,并入队 n+1 个元素到 queue2,总计 2n+1 次操作。每次出队和入队操作的时间复杂度都是 O(1),因此入栈操作的时间复杂度是 O(n)。
pop:出栈操作,只是将queue1的队首元素出队,时间复杂度是 O(1)。
top:获得栈顶元素,对应获得queue1的队首元素,时间复杂度是 O(1)。
isEmpty:判断栈是否为空,只需要判断queue1是否为空,时间复杂度是 O(1)。
空间复杂度:O(n),其中 n 是栈内的元素。需要使用两个队列存储栈内的元素。
当一个新的元素x压栈时,其实我们可以不借助辅助队列,而是让它直接入队queue1,它会添加在队尾。然后接下来,只要将之前的所有数据依次出队、再重新入队添加进queue1,就自然让x移动到队首了。
- // 用一个队列实现自定义栈
- public class MyStack2 {
- // 定义一个队列
- Queue
queue; -
- public MyStack2() {
- queue = new LinkedList<>();
- }
-
- public void push(int x){
- // 1. 首先记录当前队列长度
- int l = queue.size();
-
- // 2. 把x入队
- queue.offer(x);
-
- // 3. 把queue中原先的所有元素依次出队,然后再入队
- for (int i = 0; i < l; i++)
- queue.offer( queue.poll() );
- }
-
- public int pop(){
- return queue.poll();
- }
-
- public int top(){
- return queue.peek();
- }
-
- public boolean empty(){
- return queue.isEmpty();
- }
- }
复杂度分析
时间复杂度:入栈操作 O(n),其余操作都是 O(1)。
push:入栈操作,需要将queue1中的 n 个元素出队,并入队 n+1 个元素到 queue2,总计 2n+1 次操作。每次出队和入队操作的时间复杂度都是 O(1),因此入栈操作的时间复杂度是 O(n)。
pop:出栈操作。只是将queue1的队首元素出队,时间复杂度是 O(1)。
top:获得栈顶元素,对应获得queue1的队首元素,时间复杂度是 O(1)。
isEmpty:判断栈是否为空,只需要判断queue1是否为空,时间复杂度是 O(1)。
空间复杂度:O(n),其中 n 是栈内的元素。需要使用两个队列存储栈内的元素。
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列的支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
说明:
进阶:
你能否实现每个操作均摊时间复杂度为 O(1) 的队列?换句话说,执行 n 个操作的总时间复杂度为 O(n) ,即使其中一个操作可能花费较长时间。
示例:
输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]
解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false
提示:
我们要用栈来实现队列。一个队列是 FIFO 的,但一个栈是 LIFO 的。为了满足队列的 FIFO 的特性,我们需要将入栈的元素次序进行反转,这样在出队时就可以按照入队顺序依次弹出了。
想要反转,简单的想法是只要把所有元素依次弹出,并压入另一个栈,自然就变成本来栈底元素到了栈顶了。所以我们的实现,需要用到两个栈。
一种直观的思路是,最终的栈里,按照“自顶向下”的顺序保持队列。也就是说,栈顶元素是最先入队的元素,而最新入队的元素要压入栈底。
我们可以用一个栈来存储元素的最终顺序(队列顺序),记作stack1;用另一个进行辅助反转,记作stack2。
最简单的实现,就是直接用stack2,来缓存原始压栈的元素。每次调用push,就把stack1中的元素先全部弹出并压入stack2,然后把新的元素也压入stack2;这样stack2就是完全按照原始顺序入栈的。最后再把stack2中的元素全部弹出并压入stack1,进行反转。

- package com.webcode.stack_and_queue;
-
- import java.util.Stack;
-
- // 用两个栈实现队列:入队时翻转
- public class MyQueue {
- // 定义两个栈
- Stack
stack1; - Stack
stack2; -
- public MyQueue() {
- stack1 = new Stack<>();
- stack2 = new Stack<>();
- }
-
- // 入队方法
- public void push(int x){
- // 1. 首先将stack1中所有元素弹出,压入stack2
- while (!stack1.isEmpty())
- stack2.push( stack1.pop() );
-
- // 2. 将新元素压入stack1
- stack1.push(x);
-
- // 3. 再将stack2中所有元素弹出,压入stack
- while (!stack2.isEmpty())
- stack1.push( stack2.pop() );
- }
-
- // 出队方法
- public int pop(){
- return stack1.pop();
- }
-
- // 获取队首元素
- public int peek(){
- return stack1.peek();
- }
-
- // 判空
- public boolean empty(){
- return stack1.isEmpty();
- }
-
- public static void main(String[] args) {
- MyQueue myQueue = new MyQueue();
- myQueue.push(1); // queue is: [1]
- myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
- myQueue.peek(); // return 1
- myQueue.pop(); // return 1, queue is [2]
- myQueue.empty(); // return false
- }
- }
复杂度分析
时间复杂度:O(n)
除新元素之外,所有元素都会被压入两次,弹出两次。新元素被压入两次,弹出一次。(当然,我们可以稍作改进,在stack1清空之后把新元素直接压入,就只压入一次了)
这个过程产生了4n+3 次操作,其中 n 是队列的大小。由于入栈操作和弹出操作的时间复杂度为 O(1), 所以时间复杂度为 O(n)。
空间复杂度:O(n)
需要额外的内存来存储队列中的元素。
时间复杂度:O(1)
空间复杂度:O(1)
可以不要在入队时反转,而是在出队时再做处理。

执行出队操作时,我们想要弹出的是stack1的栈底元素。所以需要将stack1中所有元素弹出,并压入stack2,然后弹出stack2的栈顶元素。

我们观察可以发现,stack2中的元素,其实就是保持着队列顺序的,所以完全没必要将它们再压回stack1,下次出队时,我们只要直接弹出stack2中的栈顶元素就可以了。
- package com.webcode.stack_and_queue;
-
- import java.util.Stack;
-
- // 用两个栈实现队列:入队时翻转
- public class MyQueue2 {
- // 定义两个栈
- Stack
stack1; - Stack
stack2; -
- public MyQueue2() {
- stack1 = new Stack<>();
- stack2 = new Stack<>();
- }
-
- // 入队方法
- public void push(int x){
- stack1.push(x);
- }
-
- // 出队方法
- public int pop(){
- stack2 = new Stack<>();
- while (!stack1.isEmpty()){
- stack2.push(stack1.pop());
- }
- return stack2.pop();
- }
-
- // 获取队首元素
- public int peek(){
- stack2 = new Stack<>();
- while (!stack1.isEmpty()){
- stack2.push(stack1.pop());
- }
- return stack1.peek();
- }
-
- // 判空
- public boolean empty(){
- return stack1.isEmpty();
- }
-
- public static void main(String[] args) {
- MyQueue2 myQueue = new MyQueue2();
- myQueue.push(1); // queue is: [1]
- myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
- myQueue.peek(); // return 1
- myQueue.pop(); // return 1, queue is [2]
- myQueue.empty(); // return false
- }
- }
复杂度分析
时间复杂度:O(1)。向栈压入元素的时间复杂度为O(1)
空间复杂度:O(n)。需要额外的内存(stack1和stack2共同存储)来存储队列元素。
时间复杂度: 摊还复杂度 O(1),最坏情况下的时间复杂度 O(n)
在最坏情况下,stack2 为空,算法需要执行while循环进行反转。具体过程是从 stack1 中弹出 n 个元素,然后再把这 n 个元素压入 stack2,在这里n代表队列的大小。这个过程产生了 2n 步操作,时间复杂度为 O(n)。
但当 stack2 非空时,只需要直接弹栈,算法就只有 O(1) 的时间复杂度。均摊下来,摊还复杂度为O(1)。
空间复杂度 :O(1)
时间复杂度:O(1)
空间复杂度:O(1)
摊还分析(Amortized Analysis,均摊法),用来评价某个数据结构的一系列操作的平均代价。
对于一连串操作而言,可能某种情况下某个操作的代价特别高,但总体上来看,也并非那么糟糕,可以形象的理解为把高代价的操作“分摊”到其他操作上去了,要求的就是均摊后的平均代价。
摊还分析的核心在于,最坏情况下的操作一旦发生了一次,那么在未来很长一段时间都不会再次发生,这样就会均摊每次操作的代价。
摊还分析与平均复杂度分析的区别在于,平均情况分析是平均所有的输入。而摊还分析是平均操作。在摊还分析中,不涉及概率,并且保证在最坏情况下每一个操作的平均性能。
所以摊还分析,往往会用在某一数据结构的操作分析上。
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
注意空字符串可被认为是有效字符串。
示例 1:
输入: "()"
输出: true
示例 2:
输入: "()[]{}"
输出: true
示例 3:
输入: "(]"
输出: false
示例 4:
输入: "([)]"
输出: false
示例 5:
输入: "{[]}"
输出: true
判断括号的有效性,这是一个非常经典的问题。
由于给定字符串中只包含 '(',')','{','}','[',']' ,所以我们不需要额外考虑非法字符的问题。
对于合法的输入字符,关键在于遇到一个“左括号”时,我们会希望在后续的遍历中,遇到一个相同类型的“右括号”将其闭合。
由于规则是:后遇到的左括号,要先闭合,因此我们想到,利用一个栈可以实现这个功能,将左括号放入栈顶,遇到右括号时弹出就可以了。
代码实现非常简单:我们可以创建一个栈,然后遍历字符串。遇到左括号,就压栈;遇到右括号,就判断和当前栈顶的左括号是否匹配,匹配就弹栈,不匹配直接返回false。
- public class ValidParentheses {
- // 使用栈
- public boolean isValid(String s){
- Deque
stack = new LinkedList<>(); -
- // 遍历字符串中所有字符,依次判断
- for (int i = 0; i < s.length(); i++){
- // 获取当前字符
- char ch = s.charAt(i);
-
- // 判断当前字符是左括号还是右括号
- // 如果是左括号,直接将对应的右括号入栈
- if ( ch == '(' ){
- stack.push(')');
- } else if ( ch == '[' ){
- stack.push(']');
- } else if ( ch == '{' ){
- stack.push('}');
- } else {
- // 如果是右括号,弹栈判断是否匹配
- if (stack.isEmpty() || stack.pop() != ch) return false;
- }
- }
-
- return stack.isEmpty();
- }
-
- public static void main(String[] args) {
- ValidParentheses validParentheses = new ValidParentheses();
-
- System.out.println(validParentheses.isValid("()[]{}"));
- System.out.println(validParentheses.isValid("(]"));
- System.out.println(validParentheses.isValid("([)]"));
- System.out.println(validParentheses.isValid("{[]}"));
- }
- }
复杂度分析
时间复杂度:O(n),其中n是字符串s的长度。只需要遍历一次字符串。
空间复杂度:O(n)。栈中最多会保存字符串中所有的左括号,数量为O(n)。
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。

以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。

图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。
示例:
输入: [2,1,5,6,2,3]
输出: 10
题目要求计算最大矩形面积,我们可以发现,关键其实就在于确定矩形的“宽”和“高”(即矩形面积计算中的长和宽)。
而宽和高两者间又有制约条件:一定宽度范围内的高,就是最矮那个柱子的高度。
一个简单的思路,就是遍历所有可能的宽度。也就是说,以每个柱子都作为矩形的左右边界进行计算,取出所有面接中最大的那个。
- // 方法一:暴力法(遍历所有可能的宽度)
- public int largestRectangleArea1(int[] heights){
- // 定义变量保存最大面积
- int largestArea = 0;
-
- // 遍历数组,作为矩形左边界
- for (int left = 0; left < heights.length; left ++){
- // 定义变量保存当前矩形高度
- int currHeight = heights[left];
-
- // 遍历数组,选取矩形右边界
- for (int right = left; right < heights.length; right ++){
- // 确定当前矩形的高度
- currHeight = (heights[right] < currHeight) ? heights[right] : currHeight;
-
- // 计算当前矩形面积
- int currArea = (right - left + 1) * currHeight;
-
- // 更新最大面积
- largestArea = (currArea > largestArea) ? currArea : largestArea;
- }
- }
-
- return largestArea;
- }
复杂度分析
时间复杂度:O(N^2)。很明显,代码中用到了双重循环,需要耗费平方时间复杂度来做遍历计算。这个复杂度显然是比较高的。
空间复杂度:O(1)。只用到了一些辅助变量。
我们可以首先遍历数组,以当前柱子的高度,作为考察的矩阵“可行高度”。然后定义左右两个指针,以当前柱子为中心向两侧探寻,找到当前高度的左右边界。
左右边界的判断标准,就是出现了比当前高度矮的柱子,或者到达了数组边界。
- // 方法二:双指针法(遍历所有可能的高度)
- public int largestRectangleArea2(int[] heights){
- // 定义变量保存最大面积
- int largestArea = 0;
-
- // 遍历数组,以每个柱子高度作为最终矩形的高度
- for (int i = 0; i < heights.length; i++){
- // 保存当前高度
- int height = heights[i];
-
- // 定义左右指针
- int left = i, right = i;
-
- // 寻找左边界,左指针左移
- while (left >= 0){
- if (heights[left] < height) break;
- left --;
- }
- // 寻找右边界,右指针右移
- while (right < heights.length){
- if (heights[right] < height) break;
- right ++;
- }
-
- // 计算当前宽度
- int width = right - left - 1;
-
- // 计算面积
- int currArea = height * width;
- largestArea = (currArea > largestArea) ? currArea : largestArea;
- }
-
- return largestArea;
- }
复杂度分析
时间复杂度:O(N^2)。尽管少了一重循环,但在内部依然要去暴力寻找左右边界,这个操作最好情况下时间复杂度为O(1),最坏情况下为O(N),平均为O(N)。所以整体的平均时间复杂度仍然是O(N^2)。
空间复杂度:O(1)。只用到了一些辅助变量。
在双指针法寻找左右边界的过程中我们发现,如果当前柱子比前一个柱子高,那么它的左边界就是前一个柱子;如果比前一个柱子矮,那么可以跳过之前确定更高的那些柱子,直接从前一个柱子的左边界开始遍历。
这就需要我们记录下每一个柱子对应的左边界,这可以单独用一个数组来保存。
- // 方法三:双指针法改进
- public int largestRectangleArea3(int[] heights){
- // 定义变量保存最大面积
- int largestArea = 0;
-
- // 定义两个数组,保存每个柱子对应的左右边界
- int n = heights.length;
- int[] lefts = new int[n];
- int[] rights = new int[n];
-
- // 遍历数组,计算左边界
- for (int i = 0; i < n; i++) {
- // 保存当前高度
- int height = heights[i];
-
- // 定义左指针
- int left = i - 1;
-
- // 左指针左移,寻找左边界
- while (left >= 0){
- if (heights[left] < height) break;
- left = lefts[left]; // 如果左边柱子更高,就直接跳到它的左边界柱子再判断
- }
-
- lefts[i] = left;
- }
-
- // 遍历数组,计算右边界
- for (int i = n - 1; i >= 0; i--) {
- // 保存当前高度
- int height = heights[i];
-
- // 定义右指针
- int right = i + 1;
-
- // 右指针右移,寻找右边界
- while (right < n){
- if (heights[right] < height) break;
- right = rights[right]; // 如果右边柱子更高,就直接跳到它的右边界柱子再判断
- }
-
- rights[i] = right;
- }
-
- // 遍历所有柱子,计算面积
- for (int i = 0; i < n; i++){
- int currArea = (rights[i] - lefts[i] - 1) * heights[i];
- largestArea = (currArea > largestArea) ? currArea : largestArea;
- }
-
- return largestArea;
- }
复杂度分析
时间复杂度:O(N)。我们发现,while循环内的判断比对总体数量其实是有限的。每次比对,或者是遍历到一个新元素的时候,或者是之前判断发现当前柱子较矮,需要继续和前一个柱子的左边界进行比较。所以总的时间复杂度是O(N)。
空间复杂度:O(N)。用到了长度为n的数组来保存左右边界。
从上面的算法中我们可以发现,“找左边界”最重要的,其实就是排除左侧不可能的那些元素,跳过它们不再遍历。
所以我们可以考虑用这样一个数据结构,来保存当前的所有“候选左边界”。
当遍历到一个高度时,就让它和“候选列表”中的高度比较:如果发现它比之前的候选大,可以直接追加在后面;而如果比之前的候选小,就应该删除之前更大的候选。最终,保持一个按照顺序、单调递增的候选序列。
过程中应该按照顺序,先比对最新的候选、再比对较老的候选。显然,我们可以用一个栈来实现这样的功能。
栈中存放的元素具有单调性,这就是经典的数据结构单调栈了。
我们用一个具体的例子 [6,7,5,2,4,5,9,3] 来理解单调栈。
我们需要求出每一根柱子的左侧且最近的小于其高度的柱子。初始时的栈为空。
(1)我们枚举 6,因为栈为空,所以 6 左侧的柱子是“哨兵”,位置为 -1。随后我们将 6 入栈。
栈:[6(0)]。(这里括号内的数字表示柱子在原数组中的位置索引)
(2)我们枚举 7,由于 6<7,因此不会移除栈顶元素,所以 7 左侧的柱子是 6,位置为 0。随后我们将 7 入栈。
栈:[6(0), 7(1)]
(3)我们枚举 5,由于 7≥5,因此移除栈顶元素 7。同样地, 6≥5,再移除栈顶元素 6。此时栈为空,所以 5 左侧的柱子是「哨兵」,位置为−1。随后我们将 5 入栈。
栈:[5(2)]
(4)接下来的枚举过程也大同小异。我们枚举 2,移除栈顶元素 5,得到 2 左侧的柱子是「哨兵」,位置为 −1。将 2 入栈。
栈:[2(3)]
(5)我们枚举 4,5 和 9,都不会移除任何栈顶元素,得到它们左侧的柱子分别是2,4 和 5,位置分别为 3,4 和 5。将它们入栈。
栈:[2(3), 4(4), 5(5), 9(6)]
(6)我们枚举 3,依次移除栈顶元素 9,5 和 4,得到 3 左侧的柱子是 2,位置为 3。将 3 入栈。
栈:[2(3), 3(7)]
这样一来,我们得到它们左侧的柱子编号分别为 [−1,0,−1,−1,3,4,5,3]。
用相同的方法,我们从右向左进行遍历,也可以得到它们右侧的柱子编号分别为 [2,2,3,8,7,7,7,8],这里我们将位置 8 看作右侧的“哨兵”。
在得到了左右两侧的柱子之后,我们就可以计算出每根柱子对应的左右边界,并求出答案了。
- // 方法四:单调栈
- public int largestRectangleArea4(int[] heights){
- // 定义变量保存最大面积
- int largestArea = 0;
-
- // 定义两个数组,保存每个柱子对应的左右边界
- int n = heights.length;
- int[] lefts = new int[n];
- int[] rights = new int[n];
-
- // 定义一个栈
- Stack
stack = new Stack<>(); -
- // 遍历所有柱子,作为当前高度,先找左边界
- for (int i = 0; i < n ; i ++){
- while ( !stack.isEmpty() && heights[stack.peek()] >= heights[i] ){
- stack.pop();
- }
-
- // 所有大于等于当前高度的元素全部弹出,找到了左边界
- lefts[i] = stack.isEmpty() ? -1 : stack.peek();
-
- stack.push(i);
- }
-
- stack.clear();
-
- // 遍历所有柱子,作为当前高度,寻找右边界
- for (int i = n - 1; i >= 0; i --){
- while ( !stack.isEmpty() && heights[stack.peek()] >= heights[i] ){
- stack.pop();
- }
-
- // 所有大于等于当前高度的元素全部弹出,找到了左边界
- rights[i] = stack.isEmpty() ? n : stack.peek();
-
- stack.push(i);
- }
-
- // 遍历所有柱子,计算面积
- for (int i = 0; i < n; i++){
- int currArea = (rights[i] - lefts[i] - 1) * heights[i];
- largestArea = (currArea > largestArea) ? currArea : largestArea;
- }
-
- return largestArea;
- }
复杂度分析
时间复杂度:O(N)。每一个位置元素只会入栈一次(在枚举到它时),并且最多出栈一次。因此当我们从左向右/从右向左遍历数组时,对栈的操作的次数就为 O(N)。所以单调栈的总时间复杂度为 O(N)。
空间复杂度:O(N)。用到了单调栈,大小为O(N)。
当一个柱子高度比栈顶元素小时,我们会弹出栈顶元素,这就说明当前柱子就是栈顶元素对应柱子的右边界。所以我们可以只遍历一次,就求出答案。
- // 方法五:单调栈优化
- public int largestRectangleArea(int[] heights){
- // 定义变量保存最大面积
- int largestArea = 0;
-
- // 定义两个数组,保存每个柱子对应的左右边界
- int n = heights.length;
- int[] lefts = new int[n];
- int[] rights = new int[n];
-
- // 初始化rights为右哨兵n
- for (int i = 0; i < n; i ++) rights[i] = n;
-
- // 定义一个栈
- Stack
stack = new Stack<>(); -
- // 遍历所有柱子,作为当前高度,先找左边界
- for (int i = 0; i < n ; i ++){
- while ( !stack.isEmpty() && heights[stack.peek()] >= heights[i] ){
- // 栈顶元素如果小于当前元素,那么它的右边界就是当前元素
- rights[stack.peek()] = i;
- stack.pop();
- }
-
- // 所有大于等于当前高度的元素全部弹出,找到了左边界
- lefts[i] = stack.isEmpty() ? -1 : stack.peek();
-
- stack.push(i);
- }
-
- // 遍历所有柱子,计算面积
- for (int i = 0; i < n; i++){
- int currArea = (rights[i] - lefts[i] - 1) * heights[i];
- largestArea = (currArea > largestArea) ? currArea : largestArea;
- }
-
- return largestArea;
- }
复杂度分析
时间复杂度:O(N)。只有一次遍历,同样每个位置入栈一次、最多出栈一次。
空间复杂度:O(N)。用到了单调栈,大小为O(N)。