栈与队列理论基础
队列是先进先出,栈是先进后出。
栈先进后出,如图所示:
栈提供 push
和 pop
等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。
deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。
队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。
232. 用栈实现队列 - 1.28
解析:维护一个主栈1,添加元素和取队首元素时 判断栈2是否为空,如果为空,则先把栈1的元素转移到栈2,再对栈2进行取值返回。
class MyQueue {
Stack<Integer> stack1;
Stack<Integer> stack2;
public MyQueue() {
stack1 = new Stack<Integer>(); //主栈
stack2 = new Stack<Integer>(); //辅助栈
}
public void push(int x) {
stack1.push(x); //入栈
}
public int pop() {
dumpStackIn();
return stack2.pop();
}
public int peek() {
dumpStackIn();
return stack2.peek();
}
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
private void dumpStackIn(){
// 如果stack2栈为空,则将stack1栈全部弹出并压入stack2栈中
if(stack2.isEmpty()){
while(!stack1.isEmpty()){
stack2.push(stack1.pop());
}
}
}
}
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = new MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* boolean param_4 = obj.empty();
*/
225. 用队列实现栈 - 1.28
解析:使用两个队列a和b,添加元素时,先添加到b中,然后把a中元素添加到b中,再交换b和a,剩下的操作关注a队列即可。
class MyStack {
Queue<Integer> a;
Queue<Integer> b;
public MyStack() {
a = new LinkedList<>();
b = new LinkedList<>();
}
public void push(int x) {
//暂时放到b队列
b.offer(x);
//将a队列中元素全部转给b队列
while(!a.isEmpty()){
b.offer(a.poll());
}
//交换a和b,将元素都放到a中
Queue<Integer> temp;
temp = a;
a = b;
b = temp;
}
public int pop() {
return a.poll(); // 因为a中的元素和栈中的保持一致,所以这个和下面两个的操作只看queue1即可
}
public int top() {
return a.peek();
}
public boolean empty() {
return a.isEmpty();
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/
20. 有效的括号 - 1.28
解析:创建栈来存储和匹配,遍历获取当前字符,如果是 ( [ { 就直接添加进栈;如果当前字符是’)’,先判断栈是否为空,不为空时取出栈顶元素进行匹配,匹配上就弹出对应的字符,否则返回false。下面的情况类似。
class Solution {
public boolean isValid(String s) {
//创建栈来存储和匹配
Stack<Character> stack = new Stack<>();
for(int i=0; i<s.length(); i++){
char c = s.charAt(i); //获取当前字符
switch(c){
case '(':
case '[':
case '{':
stack.push(c); //如果是 ( [ { 就直接添加进栈
break;
case ')': //如果当前字符是')',先判断栈是否为空,不为空时取出栈顶元素进行匹配,匹配上就弹出对应的字符,否则返回false。下面的情况类似。
if(!stack.isEmpty()){
if(stack.peek() == '(') stack.pop();
else return false;
}else{
return false;
}
break;
case ']':
if(!stack.isEmpty()){
if(stack.peek() == '[') stack.pop();
else return false;
}else{
return false;
}
break;
case '}':
if(!stack.isEmpty()){
if(stack.peek() == '{') stack.pop();
else return false;
}else{
return false;
}
break;
}
}
//如果栈为空,说明都匹配成功了,返回true;否则返回false
return stack.isEmpty();
}
}
1047. 删除字符串中的所有相邻重复项 - 2.5
解法一:当栈不为空时,判断当前字符与栈顶字符是否相等,若相等就出栈,不相等就入栈;栈为空时,直接入栈。最后转为字符串反转后返回。
class Solution {
public String removeDuplicates(String s) {
Stack<Character> stack = new Stack<>();
char[] ch = s.toCharArray();
for(char c : ch){
if(!stack.isEmpty()){
//当栈不为空时,判断当前字符与栈顶字符是否相等,若相等就出栈
if(c - 'a' == stack.peek() - 'a'){
stack.pop();
}else{
stack.push(c); //不相等就入栈
}
}else{
stack.push(c); //栈为空时,直接入栈
}
}
StringBuilder sb = new StringBuilder();
while(!stack.isEmpty()){
sb.append(stack.pop()); //转为字符串
}
return sb.reverse().toString(); //反转后
}
}
150. 逆波兰表达式求值 - 2.7
解析:如果是算法运算符则取栈顶的两个元素进行运算,否则就直接入栈
class Solution {
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new LinkedList();
for(String s : tokens){
//如果是算法运算符则取栈顶的两个元素进行运算,否则就直接入栈
if(s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/")){
int a = stack.pop();
int b = stack.pop();
int res = 0;
switch(s){
case "+": res = b+a; break;
case "-": res = b-a; break;
case "*": res = b*a; break;
case "/": res = b/a; break;
}
stack.push(res); //计算后的结果放入栈中
}else{
stack.push(Integer.valueOf(s)); //入栈
}
}
return stack.pop(); //将结果取出后返回
}
}
239. 滑动窗口最大值 - 2.8
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
解析:
遍历数组,将 数 存放在双向队列中,并用 L,R 来标记窗口的左边界和右边界。队列中保存的并不是真的 数,而是该数值对应的数组下标位置,并且数组中的数要从大到小排序。如果当前遍历的数比队尾的值大,则需要弹出队尾值,直到队列重新满足从大到小的要求。刚开始遍历时,L 和 R 都为 0,有一个形成窗口的过程,此过程没有最大值,L 不动,R 向右移。当窗口大小形成时,L 和 R 一起向右移,每次移动时,判断队首的值的数组下标是否在 [L,R] 中,如果不在则需要弹出队首的值,当前窗口的最大值即为队首的数。
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums == null || nums.length < 2) return nums;
// 双向队列 保存当前窗口最大值的数组位置 保证队列中数组位置的数值按从大到小排序
LinkedList<Integer> queue = new LinkedList();
// 结果数组
int[] result = new int[nums.length-k+1];
// 遍历nums数组
for(int i = 0;i < nums.length;i++){
// 保证从大到小 如果前面数小则需要依次弹出,直至满足要求
while(!queue.isEmpty() && nums[queue.peekLast()] <= nums[i]){
queue.pollLast();
}
// 添加当前值对应的数组下标
queue.addLast(i);
// 判断当前队列中队首的值是否有效
if(queue.peek() <= i-k){
queue.poll();
}
// 当窗口长度为k时 保存当前窗口中最大值
if(i+1 >= k){
result[i+1-k] = nums[queue.peek()];
}
}
return result;
}
}
*剑指 Offer 09. 用两个栈实现队列 - 12.27
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
解析:先初始化两个栈,appendTail方法负责添加元素到栈1;调用deleteHead方法时,先把栈1的值转移到栈2中,然后取栈2 栈顶值出栈返回,就是结果,注意记录后还需把栈2的值转移回栈1中去。
class CQueue {
Stack<Integer> stack1;
Stack<Integer> stack2;
public CQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
public void appendTail(int value) {
stack1.push(value); //添加元素
}
public int deleteHead() {
//当队列中没有元素,返回-1
if(stack1.isEmpty() && stack2.isEmpty()) return -1;
//将栈1中的元素转移到栈2中
while(!stack1.isEmpty()){
int temp = stack1.pop();
stack2.push(temp);
}
//记录要删除的值
int result = stack2.pop();
//把栈2的值再转移到栈1中
while(!stack2.isEmpty()){
int temp = stack2.pop();
stack1.push(temp);
}
return result;
}
}
/**
* Your CQueue object will be instantiated and called as such:
* CQueue obj = new CQueue();
* obj.appendTail(value);
* int param_2 = obj.deleteHead();
*/
解法二:优化上面解法
class CQueue {
Stack<Integer> stack1;
Stack<Integer> stack2;
public CQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
public void appendTail(int value) {
stack1.push(value); //添加元素
}
public int deleteHead() {
//当队列中没有元素,返回-1
if(stack1.isEmpty() && stack2.isEmpty()) return -1;
//如果栈2不为空,则可直接返回;否则,需要将栈1的元素转移到栈2中;
if(!stack2.isEmpty()){
return stack2.pop();
}else {
while(!stack1.isEmpty()){
int temp = stack1.pop();
stack2.push(temp);
}
}
return stack2.pop();
}
}
/**
* Your CQueue object will be instantiated and called as such:
* CQueue obj = new CQueue();
* obj.appendTail(value);
* int param_2 = obj.deleteHead();
*/
剑指 Offer 30. 包含min函数的栈 - 12.27
剑指 Offer 30. 包含min函数的栈
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
解析:借助辅助栈来存放递减元素
class MinStack {
private Stack<Integer> stack;
private Stack<Integer> minStack;
/** initialize your data structure here. */
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int x) {
stack.push(x);
//如果最小栈为空 或 最小栈的栈顶元素大于等于x,就添加元素
if(minStack.isEmpty() || minStack.peek() >= x){
minStack.push(x);
}
}
public void pop() {
//主栈出栈,然后比较,如果主栈出栈的元素 等于 最小栈的栈顶元素,那么最小栈也要出栈
if(stack.pop().equals(minStack.peek())){
minStack.pop();
}
}
public int top() {
return stack.peek();
}
public int min() {
return minStack.peek();
}
}
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(x);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.min();
*/
*剑指 Offer 59 - I. 滑动窗口的最大值
解法一:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 0) return nums;
int left = 0, right = left+k-1; //滑动窗口左右指针
List<Integer> list = new ArrayList<>();
while(right <= nums.length-1){
int max = Integer.MIN_VALUE; //记录最大值
for(int i=left; i<=right; i++){
if(nums[i] > max) max = nums[i]; //找到当前窗口的最大值
}
list.add(max); //添加进集合
left++; //移动左指针
right++; //移动右指针
}
//集合赋值给数组返回
int res[] = new int[list.size()];
for(int i=0; i<list.size(); i++){
res[i] = list.get(i);
}
return res;
}
}
解法二:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
//单调队列
//下面是要注意的点:
//队列按从大到小放入
//如果首位值(即最大值)不在窗口区间,删除首位
//如果新增的值小于队列尾部值,加到队列尾部
//如果新增值大于队列尾部值,删除队列中比新增值小的值,如果在把新增值加入到队列中
//如果新增值大于队列中所有值,删除所有,然后把新增值放到队列首位,保证队列一直是从大到小
if (nums.length == 0) return nums;
Deque<Integer> deque = new LinkedList<>();
int[] arr = new int[nums.length - k + 1];
int index = 0; //arr数组的下标
//未形成窗口区间
for (int i = 0; i < k; i++) {
//队列不为空时,当前值与队列尾部值比较,如果大于,删除队列尾部值
//一直循环删除到队列中的值都大于当前值,或者删到队列为空
while (!deque.isEmpty() && nums[i] > deque.peekLast()) deque.removeLast();
//执行完上面的循环后,队列中要么为空,要么值都比当前值大,然后就把当前值添加到队列中
deque.addLast(nums[i]);
}
//窗口区间刚形成后,把队列首位值添加到队列中
//因为窗口形成后,就需要把队列首位添加到数组中,而下面的循环是直接跳过这一步的,所以需要我们直接添加
arr[index++] = deque.peekFirst();
//窗口区间形成
for (int i = k; i < nums.length; i++) {
//i-k是已经在区间外了,如果首位等于nums[i-k],那么说明此时首位值已经不再区间内了,需要删除
if (deque.peekFirst() == nums[i - k]) deque.removeFirst();
//删除队列中比当前值大的值
while (!deque.isEmpty() && nums[i] > deque.peekLast()) deque.removeLast();
//把当前值添加到队列中
deque.addLast(nums[i]);
//把队列的首位值添加到arr数组中
arr[index++] = deque.peekFirst();
}
return arr;
}
}
*剑指 Offer 59 - II. 队列的最大值
解法一:集合
class MaxQueue {
List<Integer> list;
public MaxQueue() {
list = new ArrayList<>();
}
public int max_value() {
if(list.size() == 0) return -1;
int max = Integer.MIN_VALUE;
for(int i=0; i<list.size(); i++){
if(list.get(i) > max) max = list.get(i); //找出最大值
}
return max;
}
public void push_back(int value) {
list.add(value);
}
public int pop_front() {
if(list.size() == 0) return -1;
int res = list.get(0);
list.remove(0); //删除首元素
return res;
}
}
/**
* Your MaxQueue object will be instantiated and called as such:
* MaxQueue obj = new MaxQueue();
* int param_1 = obj.max_value();
* obj.push_back(value);
* int param_3 = obj.pop_front();
*/
解法二:队列与双向队列
class MaxQueue {
Queue<Integer> queue;
Deque<Integer> deque;
public MaxQueue() {
queue = new LinkedList<>();
deque = new LinkedList<>();
}
public int max_value() {
//如果双向队列不为空,就返回双向队列首元素
return deque.isEmpty() ? -1 : deque.peekFirst();
}
public void push_back(int value) {
queue.offer(value); //队列正常入队
while(!deque.isEmpty() && deque.peekLast() < value){
deque.pollLast(); //如果双向队列的尾元素小于要添加的值,就移除该尾元素
}
deque.offerLast(value); //添加进尾部
}
public int pop_front() {
if(queue.isEmpty()) return -1;
if(queue.peek().equals(deque.peekFirst())){
deque.pollFirst(); //如果两个队列的首元素相等,则双向队列也要弹出首元素
}
return queue.poll(); //弹出队列的元素
}
}
/**
* Your MaxQueue object will be instantiated and called as such:
* MaxQueue obj = new MaxQueue();
* int param_1 = obj.max_value();
* obj.push_back(value);
* int param_3 = obj.pop_front();
*/
单调栈
347. 前 K 个高频元素 - 2.8
解析:
class Solution {
public int[] topKFrequent(int[] nums, int k) {
int[] result = new int[k];
HashMap<Integer, Integer> map = new HashMap<>();
//构建map映射
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
//将map实体化
Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
//根据map的value值正序排,相当于一个小顶堆
PriorityQueue<Map.Entry<Integer, Integer>> queue = new PriorityQueue<>((o1, o2) -> o1.getValue() - o2.getValue());
for (Map.Entry<Integer, Integer> entry : entries) {
queue.offer(entry); //添加元素
if (queue.size() > k) {
queue.poll();
}
}
for (int i = k - 1; i >= 0; i--) {
//从后往前取出 频率前 k 高的元素
result[i] = queue.poll().getKey();
}
return result;
}
}
739. 每日温度 - 2.9
解析:单调递减栈
遍历整个数组,如果栈不空,且当前数字大于栈顶元素,那么如果直接入栈的话就不是 递减栈 ,所以需要取出栈顶元素,由于当前数字大于栈顶元素的数字,而且一定是第一个大于栈顶元素的数,直接求出下标差就是二者的距离。
继续看新的栈顶元素,直到当前数字小于等于栈顶元素停止,然后将数字入栈,这样就可以一直保持递减栈,且每个数字和第一个大于它的数的距离也可以算出来。
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
Stack<Integer> stack = new Stack<>();
int len = temperatures.length;
int[] res = new int[len];
for(int i=0; i<len; i++){ //遍历整个数组
//如果栈不空,且当前数字大于栈顶元素
while(!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]){
int preIndex = stack.pop(); //取出栈顶元素
res[preIndex] = i - preIndex; //由于当前数字大于栈顶元素的数字,而且一定是第一个大于栈顶元素的数,直接求出下标差就是二者的距离。
}
stack.push(i); //入栈
}
return res;
}
}
496. 下一个更大元素 I - 2.9
解析:HashMap + 单调栈
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
Stack<Integer> stack = new Stack<>();
HashMap<Integer,Integer> map = new HashMap<>();
int len1 = nums1.length;
int len2 = nums2.length;
for(int i=0; i<len2; i++){
//如果栈不空,且当前数字大于栈顶元素,取出栈顶元素,再映射到map中,方便后面查找
while(!stack.isEmpty() && nums2[i] > nums2[stack.peek()]){
int index = stack.pop();
map.put(nums2[index],nums2[i]); //此时 nums2[i] > nums2[index]
}
stack.push(i); //下标入栈
}
int[] res = new int[len1];
for(int i=0; i<len1; i++){
res[i] = map.getOrDefault(nums1[i], -1); //查找元素是否在num1中出现
}
return res;
}
}
503. 下一个更大元素 II - 2.9
解析:循环其实可以遍历的过程中模拟走了两边nums。
class Solution {
public int[] nextGreaterElements(int[] nums) {
//边界判断
if(nums == null || nums.length <= 1){
return new int[]{-1};
}
int len = nums.length; //数组长度
int[] res = new int[len];
Arrays.fill(res, -1); //默认全部初始化为-1
Stack<Integer> stack = new Stack<>(); //栈中存放的是元素下标
for(int i=0; i<2*len; i++){ // i小于两倍的数组长度
while(!stack.isEmpty() && nums[i % len] > nums[stack.peek()]){
int index = stack.pop();
res[index] = nums[i % len]; // i取余,更新res
}
stack.push(i % len); //i取余后入栈
}
return res;
}
}
堆
NC119 最小的K个数 - 3.11
解析:创建大顶堆,先将前k个数存入堆中,再比较后面的元素,如果当前元素小于堆顶元素,就将堆顶元素弹出,然后再添加当前元素进入堆。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.PriorityQueue;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> res = new ArrayList<>();
if(input.length < k || k == 0){ //特殊处理
return res;
}
//大顶堆
PriorityQueue<Integer> pq = new PriorityQueue<>((o1,o2) -> o2-o1);
//先将前k个数存入堆中
for(int i=0; i<k; i++){
pq.offer(input[i]);
}
//再比较后面的元素
for(int i=k; i<input.length; i++){
//如果当前元素小于堆顶元素,就将堆顶元素弹出,然后再添加当前元素进入堆
if(input[i] < pq.peek()){
pq.poll();
pq.offer(input[i]);
}
}
//用集合来接收
for(int i=0; i<k; i++){
res.add(pq.poll());
}
return res;
}
}