数据结构 —— 栈、队列和双端队列|8月更文挑战

329 阅读5分钟

这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战

1 栈(Stack)

栈是一种先进后出FILO,First in last out)或后进先出LIFO,Last in first out)的数据结构。

数据结构-stack.png

  • 单向链表:可以利用一个单链表来实现栈的数据结构。而且,因为我们都只针对栈顶元素进行操作,所以借用单链表的头就能让所有栈的操作在 O(1) 的时间内完成。
  • Stack:是Vector的子类,比Vector多了几个方法
 public class Stack<E> extends Vector<E> {
         // 把元素压入栈顶
         public E push(E item) {
             addElement(item);
             return item;
         }
     
         // 弹出栈顶元素
         public synchronized E pop() {
             E obj;
             int len = size();
             obj = peek();
             removeElementAt(len - 1);
             return obj;
         }
     
         // 访问当前栈顶元素,但是不拿走栈顶元素
         public synchronized E peek() {
             int len = size();
             if (len == 0)
                 throw new EmptyStackException();
             return elementAt(len - 1);
         }
     
     // 测试堆栈是否为空
         public boolean empty() {
             return size() == 0;
         }
         
         // 返回对象在堆栈中的位置,以1为基数
         public synchronized int search(Object o) {
             int i = lastIndexOf(o);
             if (i >= 0) {
                 return size() - i;
             }
             return -1;
         }
 }

基本操作(失败时:add/remove/element为抛异常,offer/poll/peek为返回false或null)

  • E push(E):把元素压入栈
  • E pop():把栈顶的元素弹出
  • E peek():取栈顶元素但不弹出
  • boolean empty():堆栈是否为空测试
  • int search(o):返回对象在堆栈中的位置,以 1 为基数

应用场景

在解决某个问题的时候,只要求关心最近一次的操作,并且在操作完成了之后,需要向前查找到更前一次的操作。

Stack.png

案例一:判断字符串是否有效

给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合
  • 左括号必须以正确的顺序闭合
  • 空字符串可被认为是有效字符串

解题思路:利用一个栈,不断地往里压左括号,一旦遇上了一个右括号,我们就把栈顶的左括号弹出来,表示这是一个合法的组合,以此类推,直到最后判断栈里还有没有左括号剩余。

Stack判断字符串是否有效.gif

案例二:每日温度

根据每日气温列表,请重新生成一个列表,对应位置的输入是你需要再等待多久温度才会升高超过该日的天数。如果之后都不会升高,请在该位置用 0 来代替。

解题思路

  • 思路 1:最直观的做法就是针对每个温度值向后进行依次搜索,找到比当前温度更高的值,这样的计算复杂度就是 O(n2)。

  • 思路 2:可以运用一个堆栈 stack 来快速地知道需要经过多少天就能等到温度升高。从头到尾扫描一遍给定的数组 T,如果当天的温度比堆栈 stack 顶端所记录的那天温度还要高,那么就能得到结果。

Stack每日温度.gif

2 队列(Queue)

队列(Queue)和栈不同,队列的最大特点是先进先出FIFO),就好像按顺序排队一样。对于队列的数据来说,我们只允许在队尾查看和添加数据,在队头查看和删除数据。

  • :采用后进先出LIFO
  • 队列:采用 先进先出(First in First Out,即FIFO

数据结构-queue.png

实现方式

可借助双链表来实现队列。双链表的头指针允许在队头查看和删除数据,而双链表的尾指针允许在队尾查看和添加数据。

基本操作(失败时:add/remove/element为抛异常,offer/poll/peek为返回false或null)

  • int size():获取队列长度
  • boolean add(E)/boolean offer(E):添加元素到队尾
  • E remove()/E poll():获取队首元素并从队列中删除
  • E element()/E peek():获取队首元素但并不从队列中删除

应用场景

当需要按照一定的顺序来处理数据,而该数据的数据量在不断地变化的时候,则需要队列来处理。

3 双端队列(Deque)

双端队列和普通队列最大的不同在于,它允许我们在队列的头尾两端都能在 O(1) 的时间内进行数据的查看、添加和删除。

实现方式

双端队列(Deque)与队列相似,可以利用一个双链表实现双端队列。

应用场景

双端队列最常用的地方就是实现一个长度动态变化的窗口或者连续区间,而动态窗口这种数据结构在很多题目里都有运用。

案例一:滑动窗口最大值

给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口 k 内的数字,滑动窗口每次只向右移动一位,就返回当前滑动窗口中的最大值。

解题思路

  • 思路 1:移动窗口,扫描,获得最大值。假设数组里有 n 个元素,算法复杂度就是 O(n)。这是最直观的做法。

  • 思路 2:利用一个双端队列来保存当前窗口中最大那个数在数组里的下标,双端队列新的头就是当前窗口中最大的那个数。通过该下标,可以很快知道新窗口是否仍包含原来那个最大的数。如果不再包含,就把旧的数从双端队列的头删除。

因为双端队列能让上面的这两种操作都能在 O(1) 的时间里完成,所以整个算法的复杂度能控制在 O(n)。

Deque滑动窗口最大值.gif