栈的特点是「先进后出」。队列的特点是「先进先出」。现在要求使用后进先出 的栈实现一个先进先出 的队列。也就是要保证栈顶的元素始终是队首的元素。
方法一:
我们可以用两个栈来实现。其中一个栈用来存储数据,我们称为「数据栈」,另外一个栈用来辅助将数据倒序push
进第一个栈的,我们称为「辅助栈」。因为,这样可以让数据栈的数据保持新来的数一直在栈顶,这样可以实现「先进先出」。
画图如下:
class MyQueue : def __init__ (self ): """ Initialize your data structure here. """ self.stack1 = [] self.stack2 = [] self.size = 0 def push (self, x: int ) -> None : """ Push element x to the back of queue. """ while self.stack1: self.stack2.append(self.stack1.pop()) self.stack2.append(x) while self.stack2: self.stack1.append(self.stack2.pop()) self.size += 1 def pop (self ) -> int: """ Removes the element from in front of queue and returns that element. """ self.size -= 1 return self.stack1.pop() def peek (self ) -> int: """ Get the front element. """ if self.stack1 != []: return self.stack1[self.size - 1 ] else : return None def empty (self ) -> bool: """ Returns whether the queue is empty. """ return self.stack1 == []
复杂度分析:
时间复杂度:入队O(n),出队O(1),取队首元素O(1),判空O(1)。
空间复杂度:入队O(n),出队O(1),取队首元素O(1),判空O(1)。
方法二:
还是使用两个栈,只不过现在两个栈中都存放数据。两个栈的区别是,只有当出栈pop
时,才把stack1
的数据放到stack2
中,然后return stack2.pop()
。为了实现peek
方法,同时还要维护一个front
变量,使其一直指向stack1
的栈底元素,因为只是peek一下,所以不用像pop
那样将stack1
的数据移动到stack2
中,这样可在O(1)的复杂度实现peek
方法。
画图如下:
class MyQueue : def __init__ (self ): """ Initialize your data structure here. """ self.stack1 = [] self.stack2 = [] self.sizeOfStack2 = 0 self.front = None def push (self, x: int ) -> None : """ Push element x to the back of queue. """ if not self.stack1: self.front = x self.stack1.append(x) def pop (self ) -> int: """ Removes the element from in front of queue and returns that element. """ if not self.stack2: while self.stack1: self.stack2.append(self.stack1.pop()) self.sizeOfStack2 += 1 self.sizeOfStack2 -= 1 return self.stack2.pop() def peek (self ) -> int: """ Get the front element. """ if self.stack2: return self.stack2[self.sizeOfStack2 - 1 ] else : return self.front def empty (self ) -> bool: """ Returns whether the queue is empty. """ return self.stack1 == [] and self.stack2 == []
复杂度分析:
时间复杂度:入队O(1),出队均摊复杂度O(1),取队首元素O(1),判空O(1)。
空间复杂度:入队O(n),出队O(1),取队首元素O(1),判空O(1)。
要用队列来实现栈的特性,就是要保证队首的元素始终是栈顶的元素。
可以用两个队列来实现,一个是数据队列,一个是辅助队列。辅助队列的作用是在将新元素入数据队列前,先将数据队列数据移动到辅助队列,然后数据队列入队新元素,最后将辅助队列里的数据移动回数据队列。
但是,仔细观察整个过程可以发现,只需要一个队列就可以实现。首先将新元素入队,并且假设新元素所在位置为n
。然后将前n-1
个元素出队再入队。
画图如下:
class MyStack : def __init__ (self ): """ Initialize your data structure here. """ self.l1 = collections.deque() def push (self, x: int ) -> None : """ Push element x onto stack. """ n = len (self.l1) self.l1.append(x) for _ in range (n): self.l1.append(self.l1.popleft()) def pop (self ) -> int: """ Removes the element on top of the stack and returns that element. """ return self.l1.popleft() def top (self ) -> int: """ Get the top element. """ return self.l1[0 ] def empty (self ) -> bool: """ Returns whether the stack is empty. """ return not self.l1
复杂度分析:
时间复杂度:入栈O(n),出栈O(1),取栈顶元素O(1),判空O(1)。
空间复杂度:入栈O(n),出栈O(1),取栈顶元素O(1),判空O(1)。
为了能在常数时间内检索到栈内的最小元素,我们需要借助另外一个辅助栈来存储每次栈操作(push、pop、peek)后的最小值。
每次入栈都要检查辅助栈的栈顶元素和新入栈的元素谁更小,然后将小值入辅助栈。出栈只需要将两个栈同时出栈一个元素即可。
画图如下:
class MinStack { Deque<Integer> xStack; Deque<Integer> minStack; public MinStack () { xStack = new LinkedList<Integer>(); minStack = new LinkedList<Integer>(); minStack.push(Integer.MAX_VALUE); } public void push (int x) { xStack.push(x); minStack.push(Math.min(minStack.peek(), x)); } public void pop () { xStack.pop(); minStack.pop(); } public int top () { return xStack.peek(); } public int getMin () { return minStack.peek(); } }
复杂度分析:
时间复杂度:O(1),每个操作最多调用两次栈操作。
空间复杂度:O(n)。
可以借助栈的先进后出特性来解此题。
只要是左括号
就入栈,如果是右括号
则出栈一个元素,并且判断两者是否是一对,如果是一对,则继续处理下一个字符,否则括号不匹配return false
。处理完所有括号以后,如果栈不空,则说明还有左括号没有配对return false
。否则return true
。
画图如下:
class Solution { public boolean isValid (String s) { Stack <Character> stack = new Stack<Character>(); for (char c : s.toCharArray()) { if (c == '(' || c == '{' || c == '[' ) { stack.push(c); } else { if (stack.isEmpty()) { return false ; } char cTop = stack.pop(); boolean r1 = c == ')' && cTop != '(' ; boolean r2 = c == '}' && cTop != '{' ; boolean r3 = c == ']' && cTop != '[' ; if (r1 || r2 || r3) { return false ; } } } return stack.isEmpty(); } }
复杂度分析:
时间复杂度:O(n)。
空间复杂度:O(n)。
这题可以用「单调栈」求解。所谓单调栈就是,通过一些巧妙地处理,使每次入栈操作后栈内元素都保持有序(单调递增、递减的)。单调栈通常用来处理Next Greater Element
问题。
假设数据为[2, 4, 1, 3, 5]。我们可以将这组数据想象成五个不同身高的人并列站立
,他们的身高分别为2
,4
,1
,3
,5
。如下图:
矮个子右边第一个高个子会挡住矮个子的视线。我们现在想求2
的Next Greater Element
,该怎么办呢?只需要看2
右边谁第一个个子比他高那就是谁。显然第一个高个子是4
。所以2
的Next Greater Element
就是4
。同理,4
的Next Greater Element
就是5
。
class Solution { public int [] nextGreaterElement(int [] T) { Stack<Integer> s = new Stack<>(); int [] ret = new int [T.length]; for (int i = T.length - 1 ; i >= 0 ; -- i) { while (!s.isEmpty() && s.peek() <= T[i]) { s.pop(); } ret[i] = s.isEmpty() ? -1 : s.peek(); s.push(T[i]); } return ret; } }
别急接着往下看。
为了提高代码执行效率,我们应该「从右往左」处理数据,这好像有点「预测未来」的感觉,就是提前知道了谁比你高,不用去一个个的和下一个比。
代码中我们借助了栈这种先进后出的数据结构。并且将数据倒着入栈。while循环可以帮助我们将两个高个子之间的矮个子排除掉。通过这些可以保证栈里的数据按照「高个在下,矮个在上」排序。
画图如下:
可以按照上图一步一步的走一遍,加深理解。
对于本题,思路和上面一样,就是入栈的不是数值,而是下标,这点要注意区分。
class Solution { public int [] dailyTemperatures(int [] T) { Stack<Integer> s = new Stack<>(); int [] ret = new int [T.length]; for (int i = T.length - 1 ; i >= 0 ; -- i) { while (!s.isEmpty() && T[s.peek()] <= T[i]) { s.pop(); } ret[i] = s.isEmpty() ? 0 : s.peek() - i; s.push(i); } return ret; } }
复杂度分析:
时间复杂度:O(n)。不要看到这里有嵌套循环就觉得应该是O(n^2)的复杂度。其实对于内层的while循环,最多执行数组长度
遍,每个元素入栈一次,但是出栈最多一次。
空间复杂度:O(n)。
这题大概的思路还是用单调栈来处理,但是对于循环数组,该怎么处理呢?比如对于循环数组[1, 2, 1],末尾的1
通过绕一圈才找到Next Greater Element
2。
对于这题,可以想到将数组长度翻倍变为[1, 2, 1, 1, 2, 1],然后直接套用上题解法得到结果数组ret=[2, -1, 2, 2, -1, -1]
然后取前3位即可。
然而一般我们对于循环数组,可以用「取余」操作来处理循环,从而模拟了数组长度翻倍的效果。
class Solution { public int [] nextGreaterElements(int [] nums) { int l = nums.length; int [] ret = new int [l]; Stack<Integer> s = new Stack<>(); for ( int i = 2 * l - 1 ; i >= 0 ; -- i) { while (!s.isEmpty() && s.peek() <= nums[i % l]) { s.pop(); } ret[i % l] = s.isEmpty() ? -1 : s.peek(); s.push(nums[i % l]); } return ret; } }
复杂度分析:
时间复杂度:O(n)。
空间复杂度:O(n)。