[刷题技巧] 栈和队列相关知识点汇总

发布时间 2023-12-24 15:59:39作者: Ac_c0mpany丶

栈主要考察单调栈,队列主要考察优先队列(堆)。

栈和队列(ArrayDeque)

数据结构

ArrayDeque类是双端队列Deque接口的实现类。
Deque的含义是"double ended queue",即双端队列,它既可以当作栈使用,性能优于Stack,也可以当作队列使用,性能优于LinkedList。

ArrayDeque和LinkedList是Deque的两个通用实现。

具有以下特征:

  1. ArrayDeque是采用数组方式实现的双端队列。
  2. ArrayDeque的出队入队是通过头尾指针循环,利用数组实现的。
  3. ArrayDeque容量不足时是会扩容的,每次扩容容量增加一倍。
  4. ArrayDeque可以直接作为栈使用。当用作栈时,性能优于Stack,当用于队列时,性能优于LinkedList。
  5. 非线程安全队列,无同步策略,不支持多线程安全访问。
  6. 具有fail-fast特性,不能存储null值,支持双向迭代器遍历。

类图

成员变量

transient Object[] elements;
存储元素的数组

transient int head;
队列头位置

transient int tail;
队列尾位置

private static final int MIN_INITIAL_CAPACITY = 8;
一个新创建的队列的最小容量

ArrayDeque从名字看出底层是通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,要求数组必须是循环的,即循环数组。也就是数组的任何一点都可能被看做起点或者终点。另外,该容器不允许存放null元素。底层实现是数组+双指针

构造函数

  • public ArrayDeque(Collection<? extends E> c):将现有集合元素C加入队列进行构造
  • public ArrayDeque(int numElements):创建一个指定大小的ArrayDeque
  • public ArrayDeque():无参构造方法,创建一个容量为16的ArrayDeque

方法

类型 方法 作用
添加元素 public void addFirst(E e) 在数组前面添加元素
public void addLast(E e) 在数组后面添加元素
public boolean offerFirst(E e) 在数组前面添加元素,并返回是否添加成功
public boolean offerLast 在数组后面添加元素,并返回是否添加成功
删除元素 public E pollFirst() 删除第一个元素,并返回删除元素的值,如果元素为null,将返回null
public E removeFirst() 删除第一个元素,并返回删除元素的值,如果元素为null,将抛出异常
public E pollLast() 删除最后一个元素,并返回删除元素的值,如果为null,将返回null
public E removeLast() 删除最后一个元素,并返回删除元素的值,如果为null,将抛出异常
public boolean removeFirstOccurrence(Object o) 删除第一次出现的指定元素
public boolean removeLastOccurrence(Object o) 删除最后一次出现的指定元素
获取元素 public E getFirst() 获取第一个元素,如果没有将抛出异常
public E getLast() 获取最后一个元素,如果没有将抛出异常
队列操作 public boolean add(E e) 在队列尾部添加一个元素
public boolean offer(E e) 在队列尾部添加一个元素,并返回是否成功
public E remove() 删除队列中第一个元素,并返回该元素的值,如果元素为null,将抛出异常(其实底层调用的是removeFirst())
public E poll() 删除队列中第一个元素,并返回该元素的值,如果元素为null,将返回null(其实调用的是pollFirst())
public E element() 获取第一个元素,如果没有将抛出异常
public E peek() 获取第一个元素,如果没有返回null
栈操作 public void push(E e) 栈顶添加一个元素
public E pop() 移除栈顶元素,如果栈顶没有元素将抛出异常
其他 public int size() 获取队列中元素个数
public boolean isEmpty() 判断队列是否为空
public Iterator iterator() 迭代器,从前向后迭代
public Iterator descendingIterator() 迭代器,从后向前迭代
public boolean contains(Object o) 判断队列中是否存在该元素
public Object[] toArray() 转成数组
public T[] toArray(T[] a) 转成a数组常
public void clear() 清空队列
public ArrayDeque clone() 克隆(复制)一个新的队列

单调栈O(n)

题目引入

先讲解LeetCode1475. 商品折扣后的最终价格

解法一:暴力法

遍历整个数组,再遍历每个数字的右边所有的数,找到第一个小于或等于当前元素值的元素,减掉即可。

class Solution {
    public int[] finalPrices(int[] prices) {
        // 题目最终转化为:找到右边第一个比其小的数
        for (int i = 0; i < prices.length; i ++) {
            for (int j = i + 1; j < prices.length; j ++) {
                if (prices[j] <= prices[i]) {
                    prices[i] -= prices[j];
                    break;
                }
            } 
        }
        return prices;
    }
}

很明显上述代码的时间复杂度是O(n2),空间复杂度是O(n)。

现在有一个极限数据:[1, 2, 3, 4, ....., 9999999, 0],那么这个暴力法将会超时。
我们能不能想出一种时间复杂度为O(n)级别的算法呢?

单调栈的定义

单调栈算法是一种借助"栈"实现的算法,特征为栈内元素按照某种规则(一般为数字大小)单调

  • 单调递增栈:栈中元素从栈底到栈顶是递增的;
  • 单调递减栈:栈中元素从栈底到栈顶是递减的;

单调栈是一种算法,不是数据结构。

☆单调栈的应用场景

对于数组中的某个元素,在数组中找到它左侧(或右侧)离它最近的一个比它大(或者比它小)的数字

单调栈的模板

// 枚举每一个元素
for (int i = 0; i < n; i ++) {
	// 每当一个元素即将入栈时,判断一下栈中单调性是否是存在的
	// 如果单调性被破坏,当前元素入栈之前更新答案
	while (栈不空 && 单调性不存在) {
		// 对于单调性不存在的情况,我们需要不断的弹栈
		记录此时的答案(更新答案)
		stack.pop()
	}
	// 如果单调性没被破坏,当前元素直接入栈
	// 实际存放的是下标,不是元素值
	stack.push(i);
}




解法二:单调栈

class Solution {
    public int[] finalPrices(int[] prices) {
        // 单调递增栈,栈中存放的是索引下标
        Deque<Integer> stack = new ArrayDeque<>();
        for (int i = 0; i < prices.length; i ++) {
            while (!stack.isEmpty() && prices[stack.peek()] >= prices[i]) {
                prices[stack.peek()] -= prices[i];
                stack.pop();
            }
            stack.push(i);
        }
        return prices;
    }
}

时间复杂度分析:由于每个元素都最多入栈、出栈各一次。
总时间复杂度:O(n + n) = O(2n) ≈ O(n)
开辟一个数组用于返回结果,空间复杂度:O(n)

单调栈的特点:时空复杂度O(n)

相关题目

  • LeetCode1475. 商品折扣后的最终价格 单调递增栈
  • LeetCode739. 每日温度 单调递减栈
  • LeetCode42. 接雨水 单调递减栈
  • LeetCode84. 柱状图中最大的矩形