栈和队列

  • 栈也是一种线性结构
  • 相比数组,栈对应的操作是数组的子集
  • 只能从一端添加元素,也只能从一端取出元素
  • 这一端称为栈顶
  • 栈是一种后进先出的数据结构(LIFO)

举一个例子,我们平常敲代码都经常撤销,而撤销操作的原理就是靠栈来实现的。

比如我们先打出 "沉迷"、"学习"、"不法",这个时候我们发现打错了无法两个字,而这个时候栈是这样的。

通过使用撤销功能,不法将从栈顶取出,然后删除不法两个字。这个时候栈中就只有两个元素。

再举一个深入的例子:程序调用的系统栈

假设我们有三个函数。

我们通过调用函数A,在执行第二行执行B函数的时候,暂停函数A。这个时候就会将暂停的函数A压入栈中。

然后调用函数B里的C函数时候,也会将函数B压入栈中,

当函数C执行完了以后,会首先取出栈中的B2,然后继续执行B2剩下的函数,执行完成以后执行A2。

栈的基本实现

定义一个Stack接口和实现类

 public interface Stack<E> {
        int getSize();
        boolean isEmpty();
        void push(E e);
        E pop();
        E peek();
}
 public class ArrayStack<E> implements Stack<E>{
        private Array<E> array;
        public ArrayStack(){
            array = new Array<>();
        }
        public ArrayStack(int capacity){
            array = new Array<>(capacity);
        }
        @Override
        public int getSize() {
            return array.getSize();
        }
        @Override
        public boolean isEmpty() {
            return array.isEmpty();
        }
        //添加到栈顶
        @Override
        public void push(E e) {
            array.addLast(e);
        }
        //取出并移除栈顶元素
        @Override
        public E pop() {
            return array.removeLast();
        }
        //查看栈顶元素
        @Override
        public E peek() {
            return array.get(array.getSize() - 1);
        }
        public int getCapacity(){
            return array.getCapacity();
        }
        @Override
        public String toString() {
            StringBuilder res = new StringBuilder();
            res.append("Stack: ");
            res.append("[");
            for (int i = 0; i < array.getSize(); i++) {
                res.append(array.get(i));
                if (i != array.getSize() - 1) {
                    res.append(", ");
                }
            }
            res.append("] top");
            return res.toString();
        }
}

创建测试类

 public class Main {
        public static void main(String[] args) {
            ArrayStack<Integer> arrayStack = new ArrayStack<>();
            for (int i = 0; i < 5; i++) {
                arrayStack.push(i);
                System.out.println(arrayStack);
            }
            System.out.println("取出栈顶元素"+arrayStack.pop());
            System.out.println(arrayStack);
        }
}
    Stack: [0] top
    Stack: [0, 1] top
    Stack: [0, 1, 2] top
    Stack: [0, 1, 2, 3] top
    Stack: [0, 1, 2, 3, 4] top
            取出栈顶元素4
    Stack: [0, 1, 2, 3] top

栈的复杂度分析

ArrayStack<E>

  • void push(E)     O(1)
  • E pop() O(1)
  • E peek() O(1)
  • int getSize() O(1)
  • boolean isEmpty() O(1)

栈的应用-括号匹配器

引用 LeetCode 上 20. 有效的括号

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

有效字符串需满足:

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

具体思路就是判断当前是左侧括号还是右侧,如果是左侧就压入栈,当是右侧的时候就取出栈顶元素进行依次匹配。

class Solution {
        public boolean isValid(String s) {
            if(s.isEmpty()) {
                return true;
            }
            if (s.length() % 2 != 0){
                return false;
            }
            Stack<Character> stack=new Stack<>();
            for(char c:s.toCharArray()){
                if(c=='(') {
                    stack.push(')');
                } else if(c=='{') {
                    stack.push('}');
                } else if(c=='[') {
                    stack.push(']');
                } else if(stack.empty()||c!=stack.pop()) {
                    return false;
                }
            }
            return stack.empty();
        }
}

队列的基本实现

  • 队列也是一种线性结构
  • 相比数组,队列对应的操作是数组的子集
  • 只能从队尾添加元素,只能从队首取出元素
  • 队列是一种先进先出的数据结构(FIFO)

很容易理解,和生活中的排队是一样的。

public interface Queue<E> {
        int getSize();
        boolean isEmpty();
        void enqueue(E e);
        E dequeue();
        E getFront();
}
public class ArrayQueue<E> implements Queue<E> {
        private Array<E> array;
        public ArrayQueue() {
            array = new Array<>();
        }
        public ArrayQueue(int capacity) {
            array = new Array<>(capacity);
        }
        @Override
        public int getSize() {
            return array.getSize();
        }
        @Override
        public boolean isEmpty() {
            return array.isEmpty();
        }
        //插入队尾
        @Override
        public void enqueue(E e) {
            array.addLast(e);
        }
        //拿出队首元素
        @Override
        public E dequeue() {
            return array.removeFirst();
        }
        //查看队首元素
        @Override
        public E getFront() {
            return array.get(0);
        }
        public int getCapacity(){
            return array.getCapacity();
        }
        @Override
        public String toString() {
            StringBuilder res = new StringBuilder();
            res.append("Queue: ");
            res.append("front [");
            for (int i = 0; i < array.getSize(); i++) {
                res.append(array.get(i));
                if (i != array.getSize() - 1) {
                    res.append(", ");
                }
            }
            res.append("] tail");
            return res.toString();
        }
}

数组队列的复杂度分析

ArrayQueue<E>

  • void enqueue(E) O(1)
  • E dequeue() O(n)
  • E front() O(1)
  • int getSize() O(1)
  • boolean isEmpty O(1)

循环队列

由于数组队列的出队操作是O(n)的,是存在一定的局限性,使用循环队列来解决这个问题。

由于我们在删除索引为0的数据时候,会导致后面的数据前移,所以是O(n)级别。

循环队列其实就是在队首移除的时候,我们不进行后面数据的移动,而是使用front变量进行自增,然后指向下一个元素,tail变量来维护队尾的位置。

一开始的时候,front和tail都指向索引0的位置。

当新增一个元素后,tail进行自增然后指向下一个元素的位置,而front保持不变。

而移除一个队首元素的时候,front要进行自增然后指向下一个元素的位置,tail保持不变,这样的话我们不需要移动对象,只需要更改front的指向即可。

那为什么叫循环队列呢,因为我们当队列满了的时候,我们需要进行(tail+1) % capacity操作,然后将tail指向新的位置。

当我们在0的位置再新增一个元素的时候,我们如何判断队列是否满了呢,因为我们之前说过 front == tail的时候说明数组为空,而我们使用循环队列的话,我们需要使用 (tail + 1) % capacity == front 就说明队列满了需要进行扩容。需要说明循环队列会浪费一个空间。

循环队列的实现

public interface Queue<E> {
        int getSize();
        boolean isEmpty();
        void enqueue(E e);
        E dequeue();
        E getFront();
}
public class LoopQueue<E> implements Queue<E> {
        private E[] data;
        private int front, tail;
        private int size;
        public LoopQueue(int capacity) {
            //因为循环队列需要浪费一个空间,所以要将数组大小+1
            data = (E[]) new Object[capacity + 1];
            front = 0;
            tail = 0;
            size = 0;
        }
        public LoopQueue() {
            this(10);
        }
        public int getCapacity() {
            return data.length - 1;
        }
        @Override
        public int getSize() {
            return size;
        }
        @Override
        public boolean isEmpty() {
            return front == tail;
        }
        @Override
        public void enqueue(E e) {
            if ((tail + 1) % data.length == front) {
                resize(getCapacity() * 2);
            }
            data[tail] = e;
            tail = (tail + 1) % data.length;
            size++;
        }
        @Override
        public E dequeue() {
            if (isEmpty()) {
                throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
            }
            E ret = data[front];
            data[front] = null;
            front = (front + 1) % data.length;
            size--;
            if (size == getCapacity() / 4 && getCapacity() / 2 != 0) {
                resize(getCapacity() / 2);
            }
            return ret;
        }
        @Override
        public E getFront() {
            if (isEmpty()) {
                throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
            }
            return data[front];
        }
        private void resize(int newCapacity) {
            E[] newData = (E[]) new Object[newCapacity + 1];
            for (int i = 0; i < size; i++) {
                newData[i] = data[(i + front) % data.length];
            }
            data = newData;
            front = 0;
            tail = size;
        }
        @Override
        public String toString() {
            StringBuilder res = new StringBuilder();
            res.append(String.format("Queue: size = %d , capacity = %d\n", size, getCapacity()));
            res.append("front [");
            for(int i = front ; i != tail ; i = (i + 1) % data.length){
                res.append(data[i]);
                if((i + 1) % data.length != tail) {
                    res.append(", ");
                }
            }
            res.append("] tail\n");
            res.append("front = "+front);
            res.append(", tail = "+tail);
            res.append(", data.length = "+data.length);
            return res.toString();
        }
}

循环队列的复杂度分析

LoopQueue<E>

  • void enqueue(e) O(1)
  • E dequeue() O(1)
  • E getFront() O(1)
  • int getSize() O(1)
  • boolean isEmpty() O(1)

神秘杰克
768 声望392 粉丝

Be a good developer.