栈与队列及其常见面试题

本文深入介绍了栈和队列这两种基本数据结构,包括它们的定义、操作、抽象数据类型以及存储方式。栈是后进先出(LIFO)结构,常用操作包括压栈和弹栈,而队列则是先进先出(FIFO)结构,支持入队和出队。文中还探讨了栈和队列的多种实现,如顺序存储和链式存储,并展示了如何使用栈解决括号匹配问题。此外,文章提到了如何用栈和队列互换实现,以及设计特殊功能的栈和队列,如带有最小元素检索的栈和循环队列。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

认识栈与队列

认识栈

栈的定义

栈(stack)是限定仅在表尾进行插入和删除操作的线性表。

我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。

理解栈的定义需要注意:
首先它是一个线性表,也就是说,栈元素具有线性关系,即前驱后继关系。只不过它是一种特殊的线性表而已。定义中说是在线性表的表尾进行插入和删除操作,这里表尾是指栈顶,而不是栈底。

它的特殊之处就在于限制了这个线性表的插入和删除位置,它始终只在栈顶进行。这也就使得:栈底是固定的,最先进的只能在栈底。

栈的插入操作,叫作进栈,也称压栈、入栈。类似子弹入弹夹
栈的删除操作,叫作出栈,有的也叫作弹栈。如同弹夹中的子弹出夹

入栈示意图:

在这里插入图片描述

出栈示意图:

在这里插入图片描述

进出栈变化形式及抽象数据类型

现在我要问问大家,这个最先进栈的元素,是不是就只能是最后出栈呢?

答案是不一定,要看什么情况。栈对线性表的插入和删除的位置进行了限制,并没有对元素进出的时间进行限制,也就是说,在不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证是栈顶元素出栈就可以

举例来说,如果我们现在是有3个整型数字元素1、2、3依次进栈,会有哪些出栈次序呢?

  • 第一种:1、2、3进,再3、2、1出。这是最简单最好理解的一种,出栈次序为3、2、1。
  • 第二种:1进,1出,2进,2出,3进,3出。也就是进一个就出一个,出栈次序为1、2、3。
  • 第三种:1进,2进,2出,1出,3进,3出。出栈次序为2、1、3。
  • 第四种:1进,1出,2进,3进,3出,2出。出栈次序为1、3、2。
  • 第五种:1进,2进,2出,3进,3出,1出。出栈次序为2、3、1。

有没有可能是3、1、2这样的次序出栈呢?答案是肯定不会。因为3先出栈,就意味着,3曾经进栈,既然3都进栈了,那也就意味着,1和2已经进栈了,此时,2—定是在1的上面,就是更接近栈顶,那么出栈只可能是3、2、1,不然不满足1、2、3依次进栈的要求,所以此时不会发生1比2先出栈的情况。

从这个简单的例子就能看出,只是3个元素,就有5种可能的出栈次序,如果元素数量多,其实出栈的变化将会更多。这个知识点一定要弄明白。

栈的抽象数据类型

ADT栈(stack)
Data同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
OperationInitstack(*S):初始化操作,建立一个空栈SDestroyStack(*S):若栈存在,则销毁它。
ClearStack(*S):将栈清空。
StackEmpty(S):若栈为空,返回true,否则返回falseGetTop(S,*e):若栈存在且非空,用e返回S的栈顶元素。
Push(*s,e):若栈S存在,插入新元素e到栈S中并成为栈顶元素。
Pop(*S,*e):删除栈S中栈顶元素,并用e返回其值。
StackLength(S):返回栈S的元素个数。
endADT

栈的顺序存储与链式存储

  • 栈的顺序存储:利用数组来实现栈的结构

入栈操作:

在这里插入图片描述

出栈操作:
在这里插入图片描述

  • 两栈共享空间

两栈共享空间结构

栈满条件:top1+1==top2

  • 栈的链式存储

栈的链式存储结构实际上就是一个单链表,叫做链栈。插入和删除操作只能在链栈的栈顶进行。栈顶指针Top应该在链表的哪头?

在这里插入图片描述

假如按照正常逻辑,放在链表的尾部,插入操作要从头结点开始,挨着挨着遍历过去,到最后一个元素,也就是栈顶就可以实现插入。

删除操作呢?删除操作其实是没有办法进行的,因为链表的删除要知道被删除结点的前一个结点的信息,我们没法从链表的尾结点倒着回去找它的前一个结点,也没有办法确定它的前一个结点的信息,因此删除操作就无法实现。

在这里插入图片描述
因此经过前人的总结,将头指针所指的位置当做栈顶对于插入和删除操作都十分方便

认识队列

队列的定义及抽象数据类型

队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。

队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。假设队列是q=(a1,a2,…,an),那么a,就是队头元素,而a,是队尾元素。这样我们就可以删除时总是从a,开始,而插入时,列在最后。这也比较符合我们通常生活中的习惯,排在第一个的优先出列,最后来的当然排在队伍最后,如下图所示。

  • 队列的抽象数据类型

同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,删除数据只能在队头进行。

ADT队列(Queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitQueue(*Q):初始化操作,建立一个空队列QDestroyQueue(*Q):若队列Q存在,则销毁它。
ClearQueue(*Q):将队列Q清空。
QueueEmpty(Q):若队列Q为空,返回true,否则返回falseGetHead(Q,*e):若队列Q存在且非空,用e返回队列Q的队头元素。
EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素。
DeQueue(*Q,*e):删除队列Q中队头元素,并用e返回其值。
QueueLength(Q):返回队列Q的元素个数。
endADT

循环队列

在这里插入图片描述

队满的判断:(Q.tail + 1) % Maxsize == Q.head

入队的操作为:

Q.head[Q.tail]=e; 将元素e放入Q.tail指向的空间

Q.head=(Q.tail+1)%Maxsize tail指针向后移动一个单位

至于为什么要加对(Q.rear+1)取模,这是因为rear指针会一直增加到比队列容量大的数字,对其取模就是为了使其缩小,指向真正的位置例如 容量大小为5 队尾指向9 取模队尾指向的是4,说明队尾指正已经走过了一个圈

出队的操作:

e=Q.tail[Q.head];

Q.head=[Q.head+1]%Maxsize;

将头指针向着元素增长方向移动一个单元,原来的头指针指向的元素并没有真正的删除,只是逻辑上的删除,后序如果要用到这个空间,会用新的元素将其覆盖掉

栈与队列常见面试题

1.问题:给定一个只包括’(‘,’)‘,’{‘,’}‘,’[‘,’]'的字符串s,判断字符串是否有效

有效条件:
1.左括号必须用相同类型的右括号闭合
2.左括号必须以正确的顺序闭合

解:匹配时可能的情况:
1.( { } )匹配
2.(((()左括号多
3.())))右括号多
4.{(]}左右括号次序不一样
5.""空字符串
6.null
利用的数据结构:栈。遍历字符串s,遇到左括号入栈,遇到右括号与栈顶元素匹配。匹配成功,栈顶出栈,遍历坐标向后移

结果:
1.左右括号匹配成功的情况下栈和字符串都遍历完成
2.若左括号多,当字符串遍历完成的时候栈是不空的
3.若右括号多。当字符串遍历到右括号时,此时栈内左括号为空
4.遇到不匹配直接false
public boolean isValid(String s) {
        if(s == null) {
            return false;
        }
        if(s.length() == 0) {//""空字符串
            return true;
        }

        Stack<Character> stack = new Stack<>();
        for(int i = 0;i < s.length();i++) {//遍历字符串
            char ch = s.charAt(i);
            if(ch == '{' || ch == '(' || ch == '[') {//左括号入栈
                stack.push(ch);
            }else{
                //遇到右括号了
                if(stack.empty()) {
                    System.out.println("右括号多!");
                    return false;
                }
                //栈不为空:获取栈顶元素,查看是否匹配
                char tmp = stack.peek();//获取栈顶元素
                if(tmp == '{' && ch == '}' || tmp == '[' && ch == ']' || tmp == '(' && ch == ')' ) {//匹配
                    stack.pop();//匹配成功栈顶元素直接出栈
                }else{
                    System.out.println("左右括号顺序不匹配!");
                    return false;
                }
            }
        }
        if(!stack.empty()) {//遍历完字符串后栈不为空
            System.out.println("左括号多!");
            return false;
        }
        return true;
    }

2.用队列实现栈

解:
创建两个队列,入栈时入不为空的队列;出栈时出不为空的队列,然后弹出栈顶元素,两个整体类似于栈的先入后出

class MyStack {
    private Queue<Integer> qu1 = new LinkedList<>();
    private Queue<Integer> qu2 = new LinkedList<>();

    // 入栈
    public void push(int x) {
        if(!qu1.isEmpty()){
            qu1.offer(x);
        }else if(!qu2.isEmpty()){
            qu2.offer(x);
        }else{//都为空自己选一个就好
            qu1.offer(x);
        }
    }
     //出栈
    public int pop() {
        if(empty()) {
            return -1;
        }
        int e = -1;
        if(!qu1.isEmpty()) {
            int size = qu1.size();
            for(int i = 0;i < size-1;i++){//将不为空的队列中的元素输入到为空的队列中,直到只剩最后一个元素
                e = qu1.poll();
                qu2.offer(e);
            }
            e = qu1.poll();//将最后一个元素弹出,此时两个整体类似于栈的先入后出
        }else{
            int size = qu2.size();
            for(int i = 0;i < size-1;i++){
                e = qu2.poll();
                qu1.offer(e);
            }
            e = qu2.poll();
        }
        return e;
    }
     //得到栈顶元素,不删除
    public int top() {//思路和出栈一样,只是直接到栈顶,弹出栈顶元素
        if(empty()) {
            return -1;
        }
        int e = -1;
        if(!qu1.isEmpty()) {
            int size = qu1.size();
            for(int i = 0;i < size;i++){
                e = qu1.poll();
                qu2.offer(e);
            }
        }else{
            int size = qu2.size();
            for(int i = 0;i < size;i++){
                e = qu2.poll();
                qu1.offer(e);
            }
        }
        return e;
    }
     public boolean empty() {
        return qu1.isEmpty() && qu2.isEmpty();
    }
}

3.栈实现队列

解:
入队时存放到s1这个栈,出队时出s2这个栈,整体类似于队列

class MyQueue {

    private Stack<Integer> s1;
    private Stack<Integer> s2;

    public MyQueue() {
        s1 = new Stack<>();
        s2 = new Stack<>();
    }

    public void push(int x) {
        s1.push(x);//指定push到第一个栈里面
    }

    public int pop() {
        if(empty()) return -1;
        if(s2.empty()) {
            while(!s1.empty()) {//将栈s1中的所有元素放入s2栈中
                s2.push(s1.pop());
            }
        }
        return s2.pop();
    }

    public int peek() {
        if(empty()) return -1;
        if(s2.empty()) {
            while(!s1.empty()) {
                s2.push(s1.pop());
            }
        }
        return s2.peek();
    }

    public boolean empty() {

        return s1.empty() && s2.empty();
    }
}

4.设计一个支持push pop top操作,并能在常数时间内检索到最小元素的栈

存放时就已经在刻意保持元素在出栈之后可以随时找到最小元素
注意:最小值一直在发生改变

class MinStack {

    private Stack<Integer> stack = new Stack<>();
    private Stack<Integer> minStack = new Stack<>();


    public void push(int val) {
        stack.push(val);
        if(minStack.empty()) {//第一次入栈
            minStack.push(val);
        }else{
            int x = minStack.peek();
            if(val <= x) {//以后再次入栈需要先比较
                minStack.push(val);
            }
        }
    }

    public void pop() {
        int x = stack.pop();
        if(x == minStack.peek()){
            minStack.pop();
        }
    }

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

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

5.设计一个循环队列

public class MyCircularQueue {
    private int[] elem;
    private int usedSize;
    private int front;
    private int rear;

    public MyCircularQueue(int k) {
        this.elem = new int[k];
    }

    /**
     * 入队
     * @param value
     * @return
     */
    public boolean enQueue(int value) {
        if(isFull()) {
            return false;
        }
        this.elem[this.rear] = value;
        this.rear = (this.rear+1) % this.elem.length;
        return true;
    }

    public boolean isFull() {
        if( (this.rear+1) % this.elem.length == this.front) {
            return true;
        }
        return false;
    }


    /**
     * 出队
     * @return
     */
    public boolean deQueue() {
        if(isEmpty()) {
            return false;
        }
        this.front = (this.front+1) % this.elem.length;
        return true;
    }

    public boolean isEmpty() {
        if(this.front == this.rear){
            return true;
        }
        return false;
    }

    /**
     * 得到队头元素  相当于 peek()
     * @return
     */
    public int Front() {
        if(isEmpty()) {
            return -1;
        }
        int val = this.elem[this.front];
        //this.front = (this.front+1) % this.elem.length; 不能删除的
        return val;
    }

    /**
     * 得到队尾元素
     * @return
     */
    public int Rear() {
        if(isEmpty()) {
            return -1;
        }
        if(this.rear == 0) {
            return this.elem[this.elem.length-1];
        }
        return this.elem[this.rear-1];
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值