3.操作受限的线性结构:栈和队列

三、操作受限的线性结构:栈和队列

lc20、lc155、lc9

1. 栈的特点和使用场景

在这里插入图片描述

1.1 栈的特点

在这里插入图片描述
数组和链表都可以在两端进行操作,但是栈只能在一端操作
后进先出 Last In First Out,简称LIFO
或者是先进后出

1.2 系统方法调用栈

应用场景一:
在这里插入图片描述
在这里插入图片描述
应用场景二:浏览器的后退前进功能
一个栈实现后退功能:
在这里插入图片描述

百度,京东,淘宝,阿里要回到百度,要先出栈从阿里退回到百度

两个栈实现前进功能:
在这里插入图片描述

2. 栈的实现

2.1 抽象栈接口方法

public interface Stack<E> {

    /**
     * 查看栈中元素个数
     * @return
     */
    int getSize();

    /**
     * 判断栈是否为空
     * @return
     */
    boolean isEmpty();

    /**
     * 入栈
     * 将元素e压入栈中
     * @param e
     */
    void push(E e);

    /**
     * 出栈
     * 将栈顶的元素出栈
     * @return
     */
    E pop();

    /**
     * 查看栈顶的元素
     * @return
     */
    E peek();
}

2.2 数组实现栈:选择使用右端作为栈顶

如果使用左侧作为栈顶,每次push元素的时间复杂度为O(n),出栈的时间复杂度也为O(n)
使用右侧作为栈顶,push的时间复杂度为O(1),pop时间复杂度也为O(1)

2.3 数组实现栈代码实现

缺点在于数组不能动态扩容

public class ArrayStack<E> implements Stack<E> {

    private E[] data;
    private int size;   //栈中元素个数

    public ArrayStack(int capacity){
        data = (E[])new Object[capacity];
        this.size = 0;
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size==0;
    }

    @Override
    public void push(E e) {
        if(size == data.length){
            throw new RuntimeException("push failed,Stack is full");
        }
        data[size] = e;
        size++;
    }

    @Override
    public E pop() {
        if(isEmpty()){
            throw new NoSuchElementException("pop failed,stack is empty");
        }
        E ret = data[size-1];
        size--;
        return ret;
    }

    @Override
    public E peek() {
        if(isEmpty()){
            throw new NoSuchElementException("pop failed,stack is empty");
        }
        return data[size-1];
    }
}

2.4 动态数组实现栈

调用之前的ArrayList

public class DynamicArrayStack<E> implements Stack<E> {

    private ArrayList<E> data;

    public DynamicArrayStack(int capacity){
        this.data = new ArrayList<>();
    }

    @Override
    public int getSize() {
        return data.getSize();
    }

    @Override
    public boolean isEmpty() {
        return data.isEmpty();
    }

    @Override
    public void push(E e) {
        data.addLast(e);
    }

    @Override
    public E pop() {
        return data.removeLast();
    }

    @Override
    public E peek() {
        return data.getLast();
    }
}

2.5 数组实现栈的测试

给ArrayStack和DynamicStack增加toString方法

@Override
public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("Stack:[");
    for (int i = 0; i < size; i++) {
        sb.append(data[i]);
        if (i !=size-1){
            sb.append(",");
        }
    }
    sb.append("] top");
    return sb.toString();
}

测试

public class ArrayStackTest {
    public static void main(String[] args) {
        Stack<Integer> stack = new DynamicArrayStack<>(5);
        stack.push(10);
        System.out.println(stack);
        stack.push(20);
        System.out.println(stack);
        stack.push(30);
        System.out.println(stack);

        stack.pop();
        System.out.println(stack);
        stack.pop();
        System.out.println(stack);
    }
}

2.6 单向链表实现栈

链表是动态数据结构不受容量限制
首先要确定哪一端作为栈顶?
表头作为栈顶
在这里插入图片描述
表尾
在这里插入图片描述
选用链表的左端作为栈顶

2.7 单向链表实现栈代码实现

import com.douma.line.linkedList.LinkedList;

public class LinkedListStack<E> implements Stack<E> {

    private LinkedList<E> linkedList;

    public LinkedListStack(){
        linkedList = new LinkedList<>();
    }

    @Override
    public int getSize() {
        return linkedList.getSize();
    }

    @Override
    public boolean isEmpty() {
        return linkedList.isEmpty();
    }

    @Override
    public void push(E e) {
        linkedList.addFirst(e);
    }

    @Override
    public E pop() {
        return linkedList.removeFirst();
    }

    @Override
    public E peek() {
        return linkedList.getFirst();
    }

    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        res.append("Stack:[");
        for (int i = linkedList.getSize()-1; i >=0; i--) {
            res.append(linkedList.get(i));
            if (i !=0){
                res.append(",");
            }
        }
        res.append("] top");
        return res.toString();
    }
}

2.8 动态数组实现的栈对比链表实现的栈

public class PerformanceTest {
    private static Random random = new Random();

    private static double testStack(Stack<Integer> stack, int cnt) {
        long startTime = System.nanoTime();

        for (int i = 0; i < cnt; i++) {
            stack.push(random.nextInt());
        }
        for (int i = 0; i < cnt; i++) {
            stack.pop();
        }

        return (System.nanoTime() - startTime) / 1000_000_000.0;
    }

    public static void main(String[] args) {
        int cnt = 100000000;

        Stack<Integer> stack = new DynamicArrayStack<>(10);
        double time1 = testStack(stack, cnt);
        System.out.println("DynamicArrayStack 花费的时间:" + time1);

        stack = new LinkedListStack<>();
        double time2 = testStack(stack, cnt);
        System.out.println("LinkedListStack 花费的时间:" + time2);
    }
}

结论:
当数据量cnt比较小的时候:

  • LinkedList实现的栈可能比动态数组实现的栈的性能好,因为动态数组扩容缩容的时候,存在数据迁移,会影响性能

当数据量cnt越来越大的时候:

  • 动态数组实现的栈性能要比LinkedList性能要好,在使用LinkedList的时候会创建很多的node,创建的这个操作比较耗时,并且如果数据量太大还有可能会造成内存溢出(一个节点还要存放下一个节点的索引)

2.9 Java的内置的栈结构

是基于数组实现的,并且加入了synchronized,是线程安全的,但是性能会较低

3. lc20:有效的括号

给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。
在这里插入图片描述

3.1 暴力解法

在这里插入图片描述
每遍历一趟消除一对括号,只需要遍历s.length/2趟

//时间复杂度为O(n²)
public boolean isValid1(String s) {
    StringBuilder sb = new StringBuilder(s);
    int count = sb.length() / 2;
    for (int i = 0; i < count; i++) {
        for (int j = 0; j < sb.length() - 1; j++) {
            char c1 = sb.charAt(j);
            char c2 = sb.charAt(j + 1);
            // 如果相邻的字符符合要求,则删除
            if (isMatched(c1, c2)) {
                //  [)
                sb.delete(j, j + 2);
                break;
            } 
        }
    }
    return sb.length() == 0;
}

private boolean isMatched(char c1, char c2) {
    if (c1 == '(')
        return c2 == ')';
    else if (c1 == '[')
        return c2 == ']';
    else if (c1 == '{')
        return c2 == '}';
    else
        return false;
}

来源:力扣(LeetCode)链接:https://leetcode.cn/problems/valid-parentheses著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

3.2 暴力解法优化

导致时间复杂度为O(n2)的原因是左括号访问了s.length/2次,降低左括号的访问次数(避免左括号访问一次后又被访问),可以空间换时间
在这里插入图片描述
可以将左括号存放在一个特定的数据结构中,遍历到右括号的时候去数据结构中判断是否为一对,如果匹配则继续遍历
栈中数据的特点为后进先出,并且查看栈顶的元素时间复杂度为O(1),一个遍历就可以完成,可以使用栈临时存储左括号,从而降低时间复杂度

3.3 辅助栈解法

public boolean isValid(String s) {
    if (s == null) return true;
    // 代码优化:如果字符串的长度为奇数的话,那么肯定不是合法的
    if (s.length() % 2 == 1) return false;
    Stack<Character> stack = new Stack<>();
    for (char c : s.toCharArray()) {
        if (c == ' ') continue;
        if (c == '(' || c == '{' || c == '[') {
            // 如果是左括号,则直接入栈
            stack.push(c);
        } else {
            //特殊情况考虑
            if (stack.isEmpty()) {
                return false;
            }
            // 取出栈顶元素然后判断是否等于左括号对应的右括号
            char topElement = stack.pop();
            char matched = '#';
            if (c == ')')
                matched = '(';
            else if (c == ']')
                matched = '[';
            else
                matched = '{';

            if (matched != topElement)
                return false;
        }
    }
    // 如果栈不为空,那么也算没有匹配好
    return stack.isEmpty();
}

几个特殊情况:
此时遍历到右括号,栈已经空了,说明也不是匹配的
在这里插入图片描述
正常流程: 判断到右括号不匹配左括号
在这里插入图片描述
字符串遍历完,但是此时栈不为空,说明也不是有效的字符串在这里插入图片描述

4. lc155:最小栈

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:

  • MinStack() 初始化堆栈对象。
  • void push(int val) 将元素val推入堆栈。
  • void pop() 删除堆栈顶部的元素。
  • int top() 获取堆栈顶部的元素。
  • int getMin() 获取堆栈中的最小元素。
    在这里插入图片描述

4.1 辅助栈解决方案

基于Java提供的stack来实现最小栈,遍历栈中每一个元素获取最小值,但是这种方法的时间复杂度为O(n)

/**
 * 基于Java提供的stack实现最小栈,遍历栈中每一个元素获取最小值,时间复杂度为O(n)
 */
class MinStack1 {
    //使用Java内置的栈数据结构实现 线程安全,效率低
    private Stack<Integer> stack;

    public MinStack1() {
        stack = new Stack<>();
    }

    public void push(int x) {
        stack.push(x);
    }

    public void pop() {
        stack.pop();
    }

    public int top() {
        return stack.peek();
    }

    //遍历栈,获取最小值,时间复杂度为O(n)
    public int getMin() {
        int minValue = stack.peek();
        for (Integer i : stack) {
            minValue = Math.min(minValue, i);
        }
        return minValue;
    }
}

可以在push的过程中维护一个变量min,但是如果此时将栈顶元素出栈,最小值仍存储着栈顶元素,返回最小值就不对了
在这里插入图片描述
min里面可以存取多个最小值,把每次push进去的最小值都放到min中
在这里插入图片描述
min可以借用辅助栈来存储,每次push的时候将此时栈中的最小值同时push到辅助栈中
在这里插入图片描述
需要注意:
dataStack -2 0 -2
minStack -2 -2
如果push到dataStack的元素等于minStack的栈顶元素的话同样需要push到minStack中,如果不这样做,当pop掉dataStack的栈顶元素时,minStack的栈中就会变空,在getMin时就会报错。

4.2 辅助栈解决方案代码实现

/**
 * 借用辅助栈实现最小栈
 */
class MinStack2 {
    private Stack<Integer> dataStack;
    private Stack<Integer> minStack;

    public MinStack2() {
        dataStack = new Stack<>();
        minStack = new Stack<>();
    }

    //在向栈中push的过程中同时将最小值push到辅助栈中
    public void push(int x) {
        dataStack.push(x);
        // bug 修复:少了 = ,= 号是需要加上的
        // 如果去掉等于的话,可能会出现 dataStack 不为空,但是 minStack 为空了
        // 这样下面的 getMin 就会出现异常了
        if (minStack.isEmpty() || x <= minStack.peek()) {
            minStack.push(x);
        }
    }

    public void pop() {
        int top = dataStack.pop();
        if (top == minStack.peek()) {
            minStack.pop();
        }
    }

    public int top() {
        return dataStack.peek();
    }

    public int getMin() {
        return minStack.peek();
    }
}

4.3 使用一个栈实现

栈中每个元素是一个节点,一个当前值,一个最小值

/**
 * 使用一个栈实现
 */
class MinStack3 {
    private Stack<Node> stack;

    public MinStack3() {
        stack = new Stack<>();
    }

    public void push(int x) {
        Node node = new Node();
        node.val = x;
        int min = x;
        if (!stack.isEmpty() && stack.peek().min < x) {
            min = stack.peek().min;
        }
        node.min = min;
        stack.push(node);
    }

    public void pop() {
        stack.pop();
    }

    public int top() {
        return stack.peek().val;
    }

    public int getMin() {
        return stack.peek().min;
    }
}

/**
 * 存储值和最小值
 */
class Node {
    int val;
    int min;

    public Node() {

    }
    public Node(int val, int min) {
        this.val = val;
        this.min = min;
    }
}

4.4 自定义栈实现

使用链表作为栈,左侧为栈顶
在这里插入图片描述
在这里插入图片描述

/**
 * 自定义栈实现
 */
class MinStack4 {
    private Node dummyHead;

    public MinStack4() {
        dummyHead = new Node();
    }

    public void push(int x) {
        int min = x;
        Node head = dummyHead.next;
        if (head != null && head.min < x) {
            min = head.min;
        }
        Node node = new Node(x, min);
        node.next = dummyHead.next;
        dummyHead.next = node;
    }

    public void pop() {
        Node head = dummyHead.next;
        if (head != null) {
            dummyHead.next = head.next;
            head.next = null;
        }
    }

    public int top() {
        return dummyHead.next.val;
    }

    public int getMin() {
        return dummyHead.next.min;
    }
}

/**
 * 存储值和最小值
 */
class Node {
    int val;
    int min;
    Node next;

    public Node() {

    }
    public Node(int val, int min) {
        this.val = val;
        this.min = min;
    }
}

5. 队列的特点和实现

在这里插入图片描述

5.1 队列的特点

排队
在这里插入图片描述
在这里插入图片描述

5.2 队列抽象接口

public interface Queue<E> {

    /**
     * 查看队列中元素个数
     * @return
     */
    int getSize();

    /**
     * 判断队列是否为空
     * @return
     */
    boolean isEmpty();

    /**
     * 入队
     * @param e
     */
    void enqueue(E e);

    /**
     * 出队
     * @return
     */
    E dequeue();

    /**
     * 查看队首的元素
     * @return
     */
    E getFront();
}

5.3 数组实现队列分析

左边做队尾,入队时间复杂度为O(n),出队时间复杂度为O(1)
右边做队尾,入队时间复杂度为O(1),出队时间复杂度为O(n)
在这里插入图片描述
不管数组哪一端作为队尾,效果都是一样的

5.4 数组实现队列

使用动态数组实现,选用右边作为队尾

public class ArrayQueue<E> implements Queue<E> {

    private ArrayList<E> data;

    public ArrayQueue() {
        data = new ArrayList<>();
    }

    @Override
    public int getSize() {
        return data.getSize();
    }

    @Override
    public boolean isEmpty() {
        return data.isEmpty();
    }

    @Override
    //时间复杂度为O(1)
    public void enqueue(E e) {
        data.addLast(e);
    }

    //出队时间复杂度为O(n)
    @Override
    public E dequeue() {
        return data.removeFirst();
    }

    @Override
    public E getFront() {
        return data.getFirst();
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Queue: 队首 [");
        for (int i = 0; i < data.getSize(); i++) {
            sb.append(data.get(i));
            if (i !=data.getSize()-1){
                sb.append(",");
            }
        }
        sb.append("]");
        return sb.toString();
    }
}

5.5 单向链表实现队列

在这里插入图片描述
对于链表来说,无论哪一端作为队尾或者队首时间复杂度都一样
此处选用表尾作为队尾

public class LinkedListQueue<E> implements Queue<E> {

    private LinkedList<E> data;

    public LinkedListQueue(){
        data = new LinkedList<>();
    }

    @Override
    public int getSize() {
        return data.getSize();
    }

    @Override
    public boolean isEmpty() {
        return data.isEmpty();
    }

    //时间复杂度为O(n)
    @Override
    public void enqueue(E e) {
        data.addLast(e);
    }

    //时间复杂度为O(1)
    @Override
    public E dequeue() {
        return data.removeFirst();
    }

    @Override
    public E getFront() {
        return data.getFirst();
    }
}

6. 循环队列

6.1 循环队列解决的问题

优化数组实现队列
选择右边作为队尾,每次出队调用的是removeFirst,时间复杂度为O(n),每移除一个剩下的都要往前挪,这样就会遍历整个数组导致时间复杂度为O(n)
在这里插入图片描述
数据出队,但是不移动剩余元素,但是又不知道谁是队首?这时可以引入两个指针head和tail

tail执行队尾元素·的下一个空位置

这时可以移动指针,而不是移动数据
在这里插入图片描述
出队只是单纯的移动指针head,时间复杂度为O(1)
在这里插入图片描述
入队的话把值放到tail指向,然后tail++
在这里插入图片描述
当tail到数组的最后一个位置,tail往回走
在这里插入图片描述
这样就叫做循环队列

6.2 循环队列基础实现

为了tail能够回到第一个元素,应该和数组长度取模这样就可以保证tail不会越界,只能是在0到数组长度-1之间
tail+1是tail要向后移动一位
在这里插入图片描述
tail应该与数组的长度取模,保证tail不会越界,始终在0到数组长度-1内
在这里插入图片描述
head同样于要保证不会越界

public class LoopQueue<E> implements Queue<E> {
    private E[] data;
    private int head;
    private int tail;   //指向队尾元素的下一个位置

    private int size;

    public LoopQueue(int capacity) {
        data = (E[])new Object[capacity];
        head = tail = 0;
        size = 0;
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public void enqueue(E e) {
        data[tail] = e;
        size++;
        tail = (tail + 1) % data.length;    //保证tail始终在0到数组长度-1内,不会越界
    }

    @Override
    public E dequeue() { // O(1)
        if (isEmpty()) {
            throw new RuntimeException("dequeue error, No Element for dequeue");
        }
        E res = data[head];
        data[head] = null;
        size--;
        head = (head + 1) % data.length;    //保证head始终在0到数组长度-1内,不会越界
        return res;
    }

    @Override
    public E getFront() {
        if (isEmpty()) {
            throw new RuntimeException("dequeue error, No Element for dequeue");
        }
        return data[head];
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("Queue:size = %d, capacity = %d\n", size, getCapacity()));
        sb.append(" 队首 [");
        for (int i = head; i != tail ; i = (i + 1) % data.length) {
            sb.append(data[i]);
            if ((i + 1) % data.length != tail) {
                sb.append(",");
            }
        }
        sb.append("]");
        return sb.toString();
    }
}

6.3 如何判断循环队列满了

判断队列是否为空:
在这里插入图片描述
当head和tail相等的时候说明队列为空

@Override
public boolean isEmpty() {
    // 这里也可以使用 size 来判断队列是否空的,参考 issue :https://gitee.com/douma_edu/douma-algo/issues/I4WGCE
    return head == tail;
}

入队的时候需要判断队列是否满了
head == tail用来判空和用来判满冲突了,无法判断到底是满了还是空的,因此可以牺牲一个存储单元,如果tail+1==head,则表队列满了,如果tail==head则表示队列空的
需要注意:tail+1可能会越界,因此tail+1需要取模,确保范围在数组长度内
在这里插入图片描述
可以牺牲数组中的一个存储单元
在这里插入图片描述

@Override
public void enqueue(E e) {
    // 这里也可以使用 size 来判断队列是否满了,参考 issue :https://gitee.com/douma_edu/douma-algo/issues/I4WGCE
    if ((tail + 1) % data.length == head) {
        // 队列满了
    }
    data[tail] = e;
    size++;
    tail = (tail + 1) % data.length;
}

6.4 计算循环队列的容量

因为为了区分head == tail是表明队列为空还是队列为满,牺牲了一个存储单元,因此当前队列的容量应该-1

// 当前队列的容量
public int getCapacity() {
    return data.length - 1;
}

6.5 循环队列的扩缩容

在这里插入图片描述
扩容两倍

@Override
public void enqueue(E e) {
    // 这里也可以使用 size 来判断队列是否满了,参考 issue :https://gitee.com/douma_edu/douma-algo/issues/I4WGCE
    if ((tail + 1) % data.length == head) {
        // 队列满了
        resize(getCapacity() * 2);
    }
    data[tail] = e;
    size++;
    tail = (tail + 1) % data.length;
}

//扩容
private void resize(int newCapacity) {
    E[] newData = (E[])new Object[newCapacity + 1];     //只有一个牺牲的,所以需要加回来
    for (int i = 0; i < size; i++) {
        newData[i] = data[(i + head) % data.length];
    }
    data = newData;
    head = 0;
    tail = size;
}

在扩容时调用的是getCapacity() * 2这样会牺牲掉两个存储单元,因此要在扩容的方法中补回一个存储单元
出队可能会缩容:

@Override
public E dequeue() { // O(1)
    if (isEmpty()) {
        throw new RuntimeException("dequeue error, No Element for dequeue");
    }
    E res = data[head];
    data[head] = null;
    size--;
    head = (head + 1) % data.length;
    if (size == getCapacity() / 4) {
        resize(getCapacity() / 2);
    }
    return res;
}

6.6 循环队列的测试

重写toString()

@Override
public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append(String.format("Queue:size = %d, capacity = %d\n", size, getCapacity()));
    sb.append(" 队首 [");
    for (int i = head; i != tail ; i = (i + 1) % data.length) {
        sb.append(data[i]);
        if ((i + 1) % data.length != tail) {
            sb.append(",");
        }
    }
    sb.append("]");
    return sb.toString();
}

6.7 最终代码

public class LoopQueue<E> implements Queue<E> {
    private E[] data;
    private int head;
    private int tail;   //指向队尾元素的下一个位置

    private int size;

    public LoopQueue() {
        this(20);
    }

    public LoopQueue(int capacity) {
        data = (E[])new Object[capacity];
        head = tail = 0;
        size = 0;
    }

    // 当前队列的容量
    public int getCapacity() {
        return data.length - 1;
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public void enqueue(E e) {
        // 这里也可以使用 size 来判断队列是否满了,参考 issue :https://gitee.com/douma_edu/douma-algo/issues/I4WGCE
        if ((tail + 1) % data.length == head) {
            // 队列满了
            resize(getCapacity() * 2);
        }
        data[tail] = e;
        size++;
        tail = (tail + 1) % data.length;
    }

    @Override
    public E dequeue() { // O(1)
        if (isEmpty()) {
            throw new RuntimeException("dequeue error, No Element for dequeue");
        }
        E res = data[head];
        data[head] = null;
        size--;
        head = (head + 1) % data.length;
        if (size == getCapacity() / 4) {
            resize(getCapacity() / 2);
        }
        return res;
    }

    //扩容
    private void resize(int newCapacity) {
        E[] newData = (E[])new Object[newCapacity + 1];
        for (int i = 0; i < size; i++) {
            newData[i] = data[(i + head) % data.length];
        }
        data = newData;
        head = 0;
        tail = size;
    }

    @Override
    public E getFront() {
        if (isEmpty()) {
            throw new RuntimeException("dequeue error, No Element for dequeue");
        }
        return data[head];
    }

    @Override
    public boolean isEmpty() {
        // 这里也可以使用 size 来判断队列是否空的,参考 issue :https://gitee.com/douma_edu/douma-algo/issues/I4WGCE
        return head == tail;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("Queue:size = %d, capacity = %d\n", size, getCapacity()));
        sb.append(" 队首 [");
        for (int i = head; i != tail ; i = (i + 1) % data.length) {
            sb.append(data[i]);
            if ((i + 1) % data.length != tail) {
                sb.append(",");
            }
        }
        sb.append("]");
        return sb.toString();
    }
}

7. 各种队列实现方式的比较和优化

7.1 对比循环队列数组队列性能

public class PerformanceTest {
    private static Random random = new Random();

    private static double testQueue(Queue<Integer> queue, int cnt) {
        long startTime = System.nanoTime();

        for (int i = 0; i < cnt; i++) {
            queue.enqueue(random.nextInt());
        }
        for (int i = 0; i < cnt; i++) {
            queue.dequeue();
        }

        return (System.nanoTime() - startTime) / 1000_000_000.0;
    }

    public static void main(String[] args) {
        int cnt = 100000;

        Queue<Integer> queue = new ArrayQueue<>();
        double time1 = testQueue(queue, cnt);
        System.out.println("ArrayQueue 花费的时间:" + time1);

        queue = new LoopQueue<>();
        double time3 = testQueue(queue, cnt);
        System.out.println("LoopQueue 花费的时间:" + time3);

    }
}

在这里插入图片描述
结论:使用循环队列性能要比数组队列性能好很多,一个是O(n)级别,一个是O(1)级别

7.2 优化单向链表实现队列

在这里插入图片描述
都需要从表头一步步找到最后一个节点,时间复杂度为O(n)
解决问题:找到最后一个节点时间复杂度为O(1)
可以使用tail指向最后一个元素
在这里插入图片描述
如果使用表头作为队尾,表尾作为队首,如果要出队,仍然要遍历找到最后一个元素的前一个节点prev,将prev.next置为null,但是由于此处使用的是单向链表,无法使用tail.prev,因此即使设置了tail也没用,时间复杂度仍为O(n)。
如果使用表头作为队首,表尾作为队尾,如果要入队,可以使用tail.next,addLast的时间复杂度就为O(1)了,这时的性能是最好的,出队入队的时间复杂度都为O(1)

7.3 优化链表实现队列的入队操作

一个节点没有的情况:
在这里插入图片描述
本身已经
在这里插入图片描述

//时间复杂度为O(1)
@Override
public void enqueue(E e) {
    Node newNode = new Node(e);
    //一个节点没有的情况
    if(tail == null){
        tail = newNode;
        head = tail;
    }else{
        //表尾入队
        tail.next = newNode;
        tail = newNode;
    }
    size++;
}

7.4 优化链表实现队列的出队操作

表头做队首,只需要删除第一个节点:
在这里插入图片描述
当只有一个节点的时候:
在这里插入图片描述
在这里插入图片描述

//时间复杂度为O(1)
@Override
public E dequeue() {
    if (isEmpty()) {
        throw new RuntimeException("dequeue error, no element");
    }
    //队首出队,删除第一个节点
    Node delNode = head;
    head = head.next;
    delNode.next = null;

    //当只有一个节点的时候
    if (head == null) {
        tail = null;
    }

    size--;
    return delNode.e;
}

优化后的单向链表实现队列

package com.xiaoyi.line.queue;

/**
 * 优化单向链表   入队和出队时间复杂度都为O(1)
 */
public class LinkedListQueue2<E> implements Queue<E> {

    private class Node {
        E e;
        Node next;

        public Node(E e, Node next) {
            this.e = e;
            this.next = next;
        }

        public Node(E e) {
            this(e, null);
        }

        public Node() {
            this(null, null);
        }

        @Override
        public String toString() {
            return e.toString();
        }
    }

    private Node head;
    private Node tail;

    private int size;

    public LinkedListQueue2(){
        head = null;
        tail = null;
        size = 0;
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size==0;
    }

    //时间复杂度为O(1)
    @Override
    public void enqueue(E e) {
        Node newNode = new Node(e);
        //一个节点没有的情况
        if(tail == null){
            tail = newNode;
            head = tail;
        }else{
            //表尾入队
            tail.next = newNode;
            tail = newNode;
        }
        size++;
    }

    //时间复杂度为O(1)
    @Override
    public E dequeue() {
        if (isEmpty()) {
            throw new RuntimeException("dequeue error, no element");
        }
        //队首出队,删除第一个节点
        Node delNode = head;
        head = head.next;
        delNode.next = null;

        if (head == null) {
            tail = null;
        }

        size--;
        return delNode.e;
    }

    @Override
    public E getFront() {
        if (isEmpty()) {
            throw new RuntimeException("dequeue error, no element");
        }
        return head.e;
    }

    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        res.append("Queue:队首 [");
        Node curr = head;
        while (curr != null) {
            res.append(curr + "->");
            curr = curr.next;
        }
        res.append("null]");
        return res.toString();
    }
}

7.5 对比数组循环队列和链表队列性能

数据量小的时候:10000

数组循环队列要比优化后的链表队列时间稍微长一点,因为数组要resize链表是天然的动态数据结构,不需要resize

数据量大的时候:1000000

数据量大的时候数组循环队列的性能要比链表队列的性能好链表需要不断的去创建node,每插入一个元素就要new一个node,并且很占用内存空间,但是它不用去动态扩容

public class PerformanceTest {
    private static Random random = new Random();

    private static double testQueue(Queue<Integer> queue, int cnt) {
        long startTime = System.nanoTime();

        for (int i = 0; i < cnt; i++) {
            queue.enqueue(random.nextInt());
        }
        for (int i = 0; i < cnt; i++) {
            queue.dequeue();
        }

        return (System.nanoTime() - startTime) / 1000_000_000.0;
    }

    public static void main(String[] args) {
        int cnt = 10000000;

        //循环队列
        Queue<Integer> queue = new LoopQueue<>();
        double time1 = testQueue(queue, cnt);
        System.out.println("LoopQueue 花费的时间:" + time1);

        //优化后的链表队列
        queue = new LinkedListQueue2<>();
        double time2 = testQueue(queue, cnt);
        System.out.println("LinkedListQueue2 花费的时间:" + time2);
    }
}

8. Java中的队列

8.1 补充知识:队列容量

在这里插入图片描述
有容量限制的队列要比没有容量限制的队列使用更广泛,因为没有容量限制的队列很容易导致内存溢出,具有一定的风险有容量限制的队列:比如阻塞队列实现:在入队的时候进行判断是否达到容量限制

8.2 补充知识:双端队列

在这里插入图片描述

8.3 Java内置队列的继承实现体系

ArrayDeque是实现了Deque,循环双端队列
LinkedList同样实现了Deque
在这里插入图片描述
在这里插入图片描述

8.4 Java Queue中方法讲解

在这里插入图片描述

add和offer方法的作用都是入队,它们的区别是:

  1. 如果队列的容量是没有限制的,那么使用add和offer效果一样
  2. 如果队列的容量是有限制的,那么:
    a.当队列还没有满的时候,add和offer效果一样
    b.当队列已经满了,调用add的时候会抛IllegalStateException异常,而offer不会抛异常,只是返回false

remove和poll方法的作用都是出队,它们的区别在于队列为空的时候:

  1. 对空队列进行remove会抛出NoSuchElementException异常
  2. 对空队列进行poll则返回null

element和peek方法的作用都是出队,它们的区别在于队列为空的时候:

  1. 对空队列进行element会抛出NoSuchElementException异常
  2. 对空队列进行peek则返回null

8.5 Java Deque中方法讲解

在这里插入图片描述
在这里插入图片描述

8.6 双端队列实现栈的功能

Java提供的stack是加了synchronized的,单线程的情况下效率较低。
双端队列ArrayDeque是提供了没有加synchronized的栈的操作push和pop,推荐使用双端队列来代替Stack

9. 剑指9:用两个栈实现队列

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

class CQueue {

    private Stack<Integer> stack1;
    private Stack<Integer> stack2;

    public CQueue() {
        this.stack1 = new Stack<>();
        this.stack2 = new Stack<>();
    }

    //时间复杂度为O(n),添加尾部元素
    public void appendTail(int value) {
        while (!stack2.isEmpty()){    //先把第二个栈中的元素搬到第一个栈里面去
            stack1.push(stack2.pop());
        }
        stack1.push(value);
    }

    //时间复杂度为O(n),删除头部元素
    public int deleteHead() {
        while (!stack1.isEmpty()){    //把第一个栈里面的元素搬到第二个栈里面去
            stack2.push(stack1.pop());
        }
        if(stack2.isEmpty()){
            return -1;
        }
        return stack2.pop();
    }
}

优化stack2在添加队尾元素时不需要再将stack2中的元素挪到stack1中了,删除队首时直接pop即可

class CQueue {

    private Stack<Integer> stack1;
    private Stack<Integer> stack2;

    public CQueue() {
        this.stack1 = new Stack<>();
        this.stack2 = new Stack<>();
    }

    //时间复杂度为O(1)
    public void appendTail(int value) {
        stack1.push(value);
    }

    //时间复杂度为O(n)
    public int deleteHead() {
        if(stack2.isEmpty()){
            while (!stack1.isEmpty()){
                stack2.push(stack1.pop());
            }
        }
        if(stack2.isEmpty()){
            return -1;
        }else {
            return stack2.pop();
        }
    }
}

10.学习分享

在这里插入图片描述

  • 31
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

懿所思

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值