栈和队列讲解

栈和队列

3.1 栈和队列的定义和特点

栈 (stack) 是限定仅在表尾进行插入或删除操作的线性表。 因此, 对栈来说, 表尾端有其 特殊含义, 称为栈顶 (top), 相应地, 表头端称为栈底 (bottom/base)。 不含元素的空表称为空栈。

栈是按后进先出的原则进行的, 如 图(a) 所示。 因此, 栈又称为后进先出 (Last In First Out, LIFO) 的线性表

插入元素叫入栈(PUSH),删除元素叫弹栈(POP)

image-20230801212051890

栈与一般线性表的区别

image-20230801213722984

和栈相反,队列(queue)是一种**先进先出(First In First Out, FIFO)**的线性表。它只允许在表 的一端进行插入,而在另一端删除元素。

在队列中,允许插入的一端称为队尾(rear), 允许 删除的一端则称为队头(front)。

image-20230801212349071

3.2 案例引用

**【案例3.1】**进制转换

将十进制整数 N 向其他进制数d(二、八、十六)的转换

转换法则: N除以d倒取余

例如:将十进制数159转换成八进制数

image-20230801214650193

利用栈的先进后出,将每次计算结果入栈,最终取出来的就是结果

image-20230801214724287

**【案例3.2】**括号匹配的校验

假设表达式中允许包含两种括号: 圆括号和方括号

image-20230801214922504

遇见左括号就入栈,遇见右括号就与栈顶元素相比较,符合就弹栈,不符合就是不匹配。

image-20230801215514238

**【案例3.3】**表达式求值

这里介绍的算法是由运算符优先级确定运算顺序的对表达式求值算法—— 算符优先算法

表达式组成

操作数(Qperand): 常数、变量

运算符(operator): 算术运算符、关系运算符和逻辑运算符

界限符(delimiter) :左右括弧和表达式结束符。

任何一个算术表达式都由操作数(常数、变量)、算术运算符(+、-、/)和界限符 (括号、表达式结束符“#'、虚设的表达式起始符# )组成。后两者统称为算符。

例如:# 3* (7-1)#

image-20230801220624360

3.3 栈的顺序表示和实现

栈的抽象数据类型的定义

image-20230801220951700

顺序栈的表示和实现

由于栈本身就是线性表,于是栈也有顺序存储和链式存储俩种实现方式。顺序存储——顺序栈 | 链式存储——链栈

存储方式: 同一般线性表的顺序存储结构完全相同

利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素。栈底一般在低地址端。

使用俩个指针base、top:base指向栈底元素,top指向栈顶元素。但是为了操作方便,一般 top 指向栈顶的下一个存储地址。另外使用一个 stacksize 表示栈的最大容量。

image-20230801222241444

空栈时,base 和 top 同时指向栈底,没有元素可以弹栈,只能入栈

image-20230801222614910

满栈时,top 与 base 的差值等于stacksize,不允许在入栈,只能弹栈

image-20230801222755002

使用数组作为顺序栈存储方式特点

简单、方便,但是容易产生溢出【数组大小固定】

上溢(overflow):栈已经满,又要压入元素

下溢(underflow):栈已经空,还要弹出元素


代码实现栈的初始化

public class Stack {

    // 栈的最大容量
    public int stackSize;
    // top指针,指向栈顶
    public int top;
    // base指针,指向栈底
    public int base;
    // 使用数组模拟栈
    public Object[] stack;

    // 初始化栈
    public Stack(int stackSize) {
        this.stackSize = stackSize;
        stack = new Object[stackSize];
        top = 0;
        base = 0;
    }
    
    /**
     * 是否空栈
     * */
    public boolean isEmpty(){
        return top == base;
    }
    /**
     * 满栈
     * */
    public boolean isFull(){
        return  (top - base) == stackSize;
    }
    /**
     * 栈的长度
     * */
    public int getLength(){
        return  top - base;
    }
    /**
     * 清空栈
     * */
    public void clear(){
        this.top = this.base;
    }
    
}

顺序栈的入栈

  • 判断顺序栈是否已经满了,若满了抛出异常
  • 将元素压入栈
  • top指针+1
    /**
     * 入栈
     * */
    public void push(Object e) {
        if (isFull()) {
            throw new RuntimeException("栈满");
        }
        stack[top++] = e;
    }

顺序栈的出栈

  • 判断是否栈空,若空抛出异常
  • top指针-1
  • 弹出元素
    /**
     * 出栈
     * */
    public Object pop(){
        if (isEmpty()) {
            throw new RuntimeException("栈空");
        }
        return stack[--top];
    }

3.4 栈的链式表示和实现

链栈是运算受限的单链表,只能在链表头部进行操作

image-20230806104625248

通过上面的图片,可以看出链表的方向是和单链表相反的,从 an ~ a1,这样的目的主要是为了操作方便,栈的弹栈、入栈都是从表头开始的,这样不会影响后续结点

链栈的初始化:

public class ChainedStack {

    // 头指针
    Node s;

    public ChainedStack() {
        this.s = null;
    }
}

入栈操作:

image-20230806110521898

入栈结点p,将p结点的next域指向s,s重新指向栈顶

image-20230806110551495

    /**
     * 入栈
     * */
    public void push(Node p) {
        // 将新结点指向栈顶元素
        p.next = s;
        // 更改指针
        s = p
    }

弹栈操作:

将弹出的结点使用一个指针p保存,将s指向p.next

image-20230806110824188


    /**
     * 弹栈
     * */
    public Object pop(){
        if (s == null) {
            throw new RuntimeException("空栈");
        }
        Node res = s;
        s = res.next;
        return res.data;
    }

取栈顶元素

    /**
     * 取栈顶元素
     * */
    public Object getTop(){
        if (s == null) {
            throw new RuntimeException("空栈");
        }
        return s.data;
    }

3.5 队列的顺序表示和实现

image-20230802220932178

与栈不同的是,无论是弹栈、压栈始终是移动一个栈顶指针,而队列中,出队移动front头指针,入队移动rear尾指针。

image-20230802222012253

代码实现

public class Queue {
    int maxSize; // 队列的最大容量
    Object[] queue;
    int front; // 头指针
    int rear; // 尾指针

    public Queue(int maxSize) {
        this.maxSize = maxSize;
        queue = new Object[maxSize];
        front = 0 ;
        rear = 0;
    }

    // 是否为空队列
    public boolean isEmpty(){
        return front == rear;
    }

    // 是否为满队列
    public boolean isFull(){
        return rear == maxSize-1;
    }

    // 获取队列元素的个数
    public int getLength(){
        return front - rear;
    }

    // 入队
    public void add(Object e){
        if (isFull()) {
            throw new RuntimeException("满队");
        }
        queue[rear++] = e;
    }
    // 出队
    public Object out(){
        if (isEmpty()){
            throw new RuntimeException("空队");
        }
        Object o = queue[front];
        // 将出队的元素存放位置置空
        queue[front++] = null;
        return o;
    }

}

问题

image-20230802223515182

假设当 rear 已经等于 maxSize【黄色部分为队列长度】之后,还能入队吗?

答案是不能的,数组下标是从0开始的,rear= maxSize时,在入队就已经越界了。

其实我们发现下标从 0~front 之间是没有存储元素的,长度虽然为 6,但实际上存储了俩个元素,并没有真正的存满,这种情况就称为 假溢出

相对应的有真溢出,就是front = 0,rear = maxsize, 之间已经存满了元素,此时在入队就已经没有位置了。这种情况就无需处理了,而假溢出才是我们真正要解决的。

image-20230802224505194

解决 假溢出 情况:

使用循环队列的方式解决假溢出情况, 当 rear = maxSize 时,让 rear 重新从 指向 0 的位置,当入队时,插入在 0 的位置上

image-20230802225315575

那么怎么让 rear 重新变为0 呢? 可以利用模运算(取余)

当 rear =1,maxSize=6时,1% 6 = 1, 此时rear指向 1

当 rear =2,maxSize=6时,2% 6 = 2, 此时rear指向 2

当 rear =6,maxSize=6时,6% 6 = 0, 此时rear指向 0

插入元素e: queue[rear] = e; (rear+1) % maxSize
删除元素: e =  queue[front]; (front+1) % maxSize

循环队列如何判断空队列和满队列呢?

在循环队列中,可以发现无论是空队还是满队,都是 front == rear

image-20230802230157146

解决方案

1.另外设一个标志以区别队空

2.另设一个变量录元素个数

3.少用一个元素空间

我用的是第二种方式,易于理解。

代码实现

public class CircularQueues {

    Object[] queue;
    int front;
    int rear;
    int maxSize;
    int count; // 用来记录元素的个数

    // 初始化队列
    public CircularQueues(int maxSize) {
        this.maxSize = maxSize;
        this.front = 0;
        this.rear = 0;
        this.queue = new Object[maxSize];
        this.count = 0;
    }

    /**
     * 判断是否为空
     * */
    public boolean isEmpty() {
        return count == 0;
    }

    /**
     * 判断是否为满
     * */
    public boolean isFull() {
        return count == maxSize;
    }

    /**
     * 入队
     * */
    public void add(Object e) {
        if (isFull()) {
            throw new RuntimeException("满队");
        }
        // 入队操作
        queue[rear] = e;
        rear = (rear + 1) % maxSize;
        // 记录数+1
        count++;
    }

    /**
     * 出队
     * */
    public Object out() {
        if (isEmpty()) {
            throw new RuntimeException("空队");
        }
        Object e = queue[front];
        front = (front + 1) % maxSize;
        // 记录数-1
        count--;
        return e;
    }
    
        /**
     * 获取对头元素
     * */
    public Object getHeadEle() {
        if (isEmpty()) {
            throw new RuntimeException("空队");
        }
        return queue[front];
    }
}

3.6 队列的链式表示和实现

顺序队列采用数组实现,大小固定,若无法估计队列的长度,直接使用链式队列。

头指针的指向头结点,尾指针指向尾结点。

image-20230806095447728

链队列指针变化情况

初始情况下,头指针、尾指针都指向头结点

image-20230806100310551

链队列的初始化

public class ChainedQueues {
    // 头指针
    Node front;
    // 尾指针
    Node rear;
    // 头指针
    Node dummyHead;

    public ChainedQueues() {
        // 初始化,头结点的数据随意,可以不设置
        this.dummyHead = new Node(-1);
        this.front = dummyHead;
        this.rear = dummyHead;
    }
}

入队操作

头删尾插,将rear指针的next域指向新结点,并将 rear指针后移即可。

    /**
     * 入队
     * */
    public void add(Node e) {
        rear.next = e;
        rear = e;

    }

出队操作:

出队操作时将首元结点删除掉并返回,很简单,头指针的next域就是要出队的元素。

    public Object out() {
        if (rear == front) {
            throw new RuntimeException("队空");
        }
        Node res = front.next;
        front.next = res.next;
        return res.data;
    }

但是这样写会有一些问题,如果出队的正好是队尾元素,此时 front.next = null, 但是 rear 指针仍然在指向 res,因此判断非空时,会返回 false

因此当出队的是队尾元素还需要将 rear 指针重新指向头结点

image-20230806103305729

    /**
     * 出队
     * 如果出队的是最后一个结点,还要修改尾指针
     * */
    public Object out() {
        if (isEmpty()) {
            throw new RuntimeException("队空");
        }
        Node res = front.next;
        front.next = res.next;
        if (res == rear) {
            // 最后一个结点
            rear = dummyHead;
        }
        return res.data;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

鲨瓜2号

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

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

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

打赏作者

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

抵扣说明:

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

余额充值