Java数据结构——栈和队列

目录

栈和队列

队列

循环队列

双端队列Deque


栈和队列

线性表:一次保存单个同类型元素,多个元素之间逻辑上连接

栈和队列其实是操作受限的线性表。之前的数组、链表可以在任意位置插入和删除,但栈和队列只能在一端插入和删除元素

  1. 只能从一段插入元素,也只能从这一端取出元素(栈顶):添加和删除元素的一端称为栈顶,另一端称为栈底。最先添加的元素在栈的最底端(栈底),最后添加的元素在栈的最顶端(栈顶)。而从栈中取出元素的顺序和从栈中添加元素的顺序恰好相反,即最后添加的元素最先取出(Last In First Out, LIFO)

  2. 在操作一个栈时,只能操作这个栈的栈顶元素,其他元素对于我们来说是不可见的

  3. 栈的特点:先进后出,后进先出的线性表。LIFO

栈在现实生活中的应用(无处不再):

  1. 无处不在的undo(撤销)操作:

    e.g. 撤销相当于从栈顶取出错误的元素,然后读取当前栈顶元素

    在任何一个编辑器中输错了一个内容使用ctrl+z就返回到了上一次输入的内容。

    在任何一个浏览器中点击<-就能返回上一次浏览的网站

  2. 操作系统栈:程序在执行过程中,从A函数调用B函数,从B函数调用C函数,返回执行时,如何得知从哪开始继续执行呢,其实背后就是栈这个结构

    e.g. 记录程序中,函数A调用函数B的行数,压入栈。执行结束后逐一出栈。

     

栈的实现:

  1. 基于数组实现的栈--顺序栈(大部分情况)

                栈只能在栈顶插入元素,在栈顶删除元素,即只能在数组的末尾插入和删除元素

        2. 基于链表实现的栈--链式栈

核心操作:

        记得size属性!!!用于记录当前栈中元素个数

        push(E e): 向栈中添加元素 入栈/压栈

        E pop()出栈操作,弹出栈顶元素

        E peek()查看栈顶元素,但不返回

import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
​
/**
 *基于数组实现的顺序栈
 * @param <E>
 */
public class MyStack<E> {
​
    private int size;
​
    private List<E> myStack = new ArrayList<E>();
​
    /**
     * 向栈中添加元素
     * @param val
     */
    public void push(E val){
        myStack.add(val);
        size++;
    }
​
    /**
     * 弹出当前栈顶元素,返回栈顶元素的值
     * @return
     */
    public E pop(){
        // 注意要先判空,栈为空无法弹出
        if(isEmpty()){
            throw new NoSuchElementException("Stack is empty, can't pop!");
        }
        E val = myStack.remove(size-1);
        size --;
        return val;
//        return myStack.remove(size--);
    }
​
    private boolean isEmpty() {
        return size == 0;
    }
​
    /**
     * 查看当前栈顶元素值,但不弹出该元素
     * @return
     */
    public E peek(){
        if(isEmpty()){
            throw new NoSuchElementException("Stack is empty, can't peek!");
        }
        return myStack.get(size-1);
    }
​
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        for (int i = 0; i < size; i++) {
            sb.append(myStack.get(i));
            // 未走到最后一个
            if(i != size-1){
                sb.append(", ");
            }
        }
        sb.append("] top");
        return sb.toString();
    }
}

队列

栈和队列其实是一码事,都是只能在线性表的一端进行插入和删除操作,因此二者可以相互转换

队列:FIFO,先进先出的数据结构,元素从队尾添加到队列中,元素队首出队列。元素的出队顺序和入队顺序保持一致

现实生活中的应用:各种“排队”操作

队列中结构较多:普通FIFO队列、双端队列Deque、循环队列LoopQueue和优先队PriortyQueue

java中内置队列:java.util.Queue 类似 java.util.LinkedList

队列的实现:

        1.基于数组实现的队列(顺序队列)——常用于搭建循环队列

        2.基于链表实现的队列(链式队列)——常用于搭建普通的队列

由于出队操作只能在队列的头部进行,若采用数组的方案,每次出队一个元素就得搬移剩下的所有元素向前移动一个单位。此时采用链表的方案更适合队列的结构

操作:

        push(E val) 出队列

        pop()弹出队首元素

        peek()查看队首元素

import seqlist.queue.Queue;

import java.util.NoSuchElementException;

class Node<E> {
    E val;
    Node<E> next;

    public Node(E val) {
        this.val = val;
    }
}


public class LinkedQueue<E> implements Queue<E> {
    // 当前队列中的元素个数
    private int size;
    // 当前队列的队首元素
    private Node<E> head;
    // 当前队列的尾部元素
    private Node<E> tail;
    @Override
    public void offer(E val) {
        // 产生一个新节点
        Node<E> node = new Node<>(val);
        if (head == null) {
            head = tail = node;
        }else {
            // 链表的尾插
            tail.next = node;
            tail = node;
        }
        size ++;
    }
    @Override
    public E poll() {
        if (isEmpty()) {
            throw new NoSuchElementException("queue is empty!cannot poll!");
        }
        // 删除当前的队首元素,即head
        E val = head.val;
        Node<E> node = head;
        head = head.next;
        node.next = node = null;
        size --;
        return val;
    }

    @Override
    public E peek() {
        if (isEmpty()) {
            throw new NoSuchElementException("queue is empty!cannot peek!");
        }
        return head.val;
    }

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

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("front [");
        for (Node x = head;x != null;x = x.next) {
            sb.append(x.val);
            if (x.next != null) {
                sb.append(", ");
            }
        }
        sb.append("] tail");
        return sb.toString();
    }
}

循环队列

  • 循环队列就是使用长度固定的数组实现,数组头部就是队首(head),数组的尾部就是队尾(tail),即head永远指向循环队列的第一个元素,tail永远指向循环队列有效元素的后一个位置, 故数组[head,tail)是循环队列的有效元素。
  • 所谓的循环队列指的就是当head或者tail引用走到数组末尾时,下一次再继续向后移动,其实返回数组的头部继续操作
  • 使用两个引用head和tail实现出队入队操作,添加元素在数组尾部添加,删除元素只需要移动head引用所指向的地址即可(逻辑删除)。避免了从数组头部删除元素时,需要频繁移动元素的情况。即循环队列在删除元素时,不需要进行数据的搬移,当有新的元素在添加时就会覆盖掉之前的元素
  • 用途:操作系统的生产消费者模型,MySQL数据库的InnoDB存储引擎中的redo日志

要点:

  1. 数组为空,循环队列为空,此时head==tail

  2. 在循环队列中浪费一个空间,用于判断队列是否已满。当循环队列已满时,(tail+1)%n ==head

    head和tail的移动不能简单的+1,因为可能数组越界,应使用取模操作,即head和tail走到数组最后一个索引位置时,想要下一次返回数组头部,需要用tail+1对数组长度n取模

基于数组的实现:

import seqlist.queue.Queue;

import java.util.NoSuchElementException;

public class LoopQueue implements Queue<Integer> {
    // 定长数组
    private Integer[] data;
    // 指向队首元素
    private int head;
    // 指向队尾元素的下一个索引
    private int tail;

    public LoopQueue(int size) {
        // 因为循环队列中要浪费一个空间判断是否已满
        data = new Integer[size + 1];
    }


    @Override
    public void offer(Integer val) {
        if (isFull()) {
            throw new ArrayIndexOutOfBoundsException("loopQueue is full,cannot offer");
        }
        data[tail] = val;
        tail = (tail + 1) % data.length;
    }

    @Override
    public Integer poll() {
        if (isEmpty()) {
            throw new NoSuchElementException("loopQueue is empty!cannot poll");
        }
        // 移动队首元素
        Integer val = data[head];
        // 移动引用一定是 + 1 % n
        head = (head + 1) % data.length;
        return val;
    }

    @Override
    public Integer peek() {
        if (isEmpty()) {
            throw new NoSuchElementException("loopQueue is empty!cannot peek");
        }
        return data[head];
    }

    @Override
    public boolean isEmpty() {
        return head == tail;
    }

    public boolean isFull() {
        return (tail + 1) % data.length == head;
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("front [");
        int lastIndex = tail == 0 ? data.length - 1 : tail - 1;
        for (int i = head; i != tail;) {
            sb.append(data[i]);
            // 最后一个有效元素的索引是多少?
            if (i != lastIndex) {
                sb.append(", ");
            }
            i = (i + 1) % data.length;
        }
        sb.append("] tail");
        return sb.toString();
    }
}

双端队列Deque

Queue的子接口,这个队列既可以尾插头出,也可以头插尾出

以后无论使用的时栈还是接口,统一使用双端队列接口。stack类效率低,被时代抛弃

Deque<E> stack = new LinkedList<>();
Deque<E> queue = new LinkedList<>();

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值