【数据结构】栈和队列


前言

一键助你了解并认识栈和队列,栈和队列的概念为何?如何使用栈和队列?栈和队列的应用场景?栈、虚拟机栈、栈帧有什么区别?什么是循环队列、双端队列?如何用栈和队列实现彼此?本篇文章统统会为你解答。
注:笔者代码水平与笔力有限,如有错误欢迎大家在评论区指出,我会及时回复改正的。

一、栈是什么?

栈(Stack)是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。
栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
生活中常见的例子有:羽毛球在羽毛球筒中的进出、子弹在弹夹中的进出等。

一言蔽之,栈是一种先进后出的数据结构。我们可以用顺序表实现栈也可以用链表实现栈,不过一般都使用顺序表,两者分别叫做顺序栈与链式栈。如果你是第一次接触栈这个数据结构,了解这些内容即可,对整体有个大概后在回过头来看这些内容便一目了然。
再具体些,顺序栈就是底层是一个数组的栈,对应Stack;链式栈则对应利用LinkedList或者Deque引用。

实现链式栈时,又分为单向链表(入栈出栈从链表头部开始)与双向链表,进行入栈出栈操作时的时间复杂度都是O(1)。

1. 栈的方法与功能

Java提供了Stack类,下面我们使用它来了解栈的使用方式。

		// 实例化一个栈
        Stack<Integer> stack = new Stack<>();

        // 入栈
        // push方法返回值的意思是:入栈的元素
        stack.push(12);
        stack.push(23);
        stack.push(34);
        stack.push(45);
        stack.push(56);

        // pop出栈 返回值的意思是:出栈的元素
        Integer x = stack.pop();
        System.out.println(x);

        // peek方法:获取栈顶元素 但是 不删除
        Integer y = stack.peek();
        System.out.println(y);

        Integer y2 = stack.peek();
        System.out.println(y2);

        boolean flg = stack.isEmpty();
        System.out.println(flg);

        flg = stack.empty();
        System.out.println(flg);

如上述代码块,我们可以了解到,Stack常用的方法与功能有:

方法功能
Stack()构造一个空的栈
E push(E e)将e入栈,并返回e
E pop()将栈顶元素出栈并返回
E peek()获取栈顶元素
int size()获取栈中有效元素个数
boolean empty()检测栈是否为空

2. 栈的模拟实现

使用整型数组模拟实现栈:

public class MyStack {
    // usedSize 可以表示当前存放数据的个数,也可以表示当前存放数据的下标
    public int[] elem;
    public int usedSize;

    public MyStack() {
        this.elem = new int[10];
    }

    public void push(int val) {
        if (isFull()) {
            // 扩容
            elem = Arrays.copyOf(elem,2*elem.length);
        }
        elem[usedSize] = val;
        usedSize++;
    }

    public boolean isFull() {
        return usedSize == elem.length;
    }

    public int pop() {
        if (empty()) {
        	throw new RuntimeException("栈为空,无法出栈。");
        }

        int oldVal = elem[usedSize - 1];
        // 当栈中的元素为引用数据类型时,出栈后需要置空
        //elem[usedSize] = null;
        usedSize--;
        return oldVal;
    }

    public int peek() {
        if (empty()) {
            throw new RuntimeException("栈为空,无法获取栈顶元素。");
        }

        return elem[usedSize - 1];
    }

    public boolean empty() {
        return usedSize == 0;
    }
}

使用对象数组模拟实现栈:

public class MyStack2<E> {
    public Object[] elem;
    public int usedSize;

    public MyStack2() {
        this.elem = new Object[10];
    }

    public void push(E val) {
        if (isFull()) {
            // 扩容
            elem = Arrays.copyOf(elem, 2 * elem.length);
        }
        elem[usedSize] = val;
        usedSize++;
    }

    public boolean isFull() {
        return usedSize == elem.length;
    }

    public E pop() {
        if (empty()) {
            return null;
        }

        E oldVal = (E) elem[usedSize - 1];
//        elem[usedSize] = null;
        usedSize--;
        return oldVal;
    }

    public E peek() {
        if (empty()) {
            return null;
        }

        return (E) elem[usedSize - 1];
    }

    public boolean empty() {
        return usedSize == 0;
    }
}

3. 栈的应用场景

(1) 改变元素的序列

若进栈序列为 1,2,3,4 ,进栈过程中可以出栈,则下列不可能的出栈序列是()
A: 1,4,3,2
B: 2,3,4,1
C: 3,1,4,2
D: 3,4,2,1

一个栈的初始状态为空。现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则元素出栈的顺序是( )
A: 12345ABCDE
B: EDCBA54321
C: ABCDE12345
D: 54321EDCBA

这类题目需要注意题干中是否说明入栈时是否可以出栈。
两题答案:C、B

(2) 将递归转化为循环

逆序打印链表

// 递归方式
void printList (Node head) {
	if (null != head) {
		printList(head.next);
		System.out.print(head.val + " ");
	}
}
// 循环方式
void printList(Node head) {
	if (null == head) {
		return;
	}

	Stack<Node> s = new Stack<>();
	// 将链表中的结点保存在栈中
	Node cur = head;
	while (null != cur) {
		s.push(cur);
		cur = cur.next;
	}
	// 将栈中的元素出栈
	while (!s.empty()) {
		System.out.print(s.pop() + " ");
	}
}

(3) 括号匹配

原题链接:力扣-有效的括号
参考代码:

class Solution {
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>();
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            // 1.左括号入栈
            if (ch == '(' || ch == '[' || ch == '{') {
                stack.push(ch);
            } else {
            	// 2.遇到了右括号
                if (stack.empty()) {
                    return false;
                } else {
                	// 3.取栈顶元素的左括号,看和当前右括号是否匹配
                    char chL = stack.peek();
                    if (chL == '(' && ch == ')' || 
                    chL == '[' && ch == ']' || chL == '{' && ch == '}') {
                    	// 4.证明当前这一对括号是匹配的
                        stack.pop();
                    } else {
                    	// 5.当前括号不匹配
                        return false;
                    }
                }
            }
        }
        return stack.empty();
    }
}

(4) 逆波兰表达式求值

原题链接:力扣-逆波兰表达式求值

举例:5*(2+3)转换为逆波兰表示式则为:523+*。具体解释力扣里有百科链接。

参考代码:

class Solution {
    public int evalRPN(String[] tokens) {
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < tokens.length; i++) {
            String tmp = tokens[i];
            if (!isOperation(tmp)) {
                // 说明该字符代表数字
                Integer val = Integer.valueOf(tmp);
                stack.push(val);
            } else {
                // 说明该字符代表 + - * / 操作
                Integer val2 = stack.pop();
                Integer val1 = stack.pop();
                switch (tmp) {
                    case "+":
                        stack.push(val1 + val2);
                        break;
                    case "-":
                        stack.push(val1 - val2);
                        break;
                    case "*":
                        stack.push(val1 * val2);
                        break;
                    case "/":
                        stack.push(val1 / val2);
                        break;
                    default:
                }
            }
        }
        return stack.pop();
    }

    public boolean isOperation(String s) {
        if ("+".equals(s) || "-".equals(s) || "*".equals(s) || "/".equals(s)) {
            return true;
        }
        return false;
    }
}

代码解析:本质上就是遇到数字就入栈,遇到操作符就连出两栈分别作为操作符右边和左边的数字(顺序不能错),然后将计算结果继续入栈,最后将唯一的结果出栈。

(5) 出栈入栈次序匹配

原题链接:牛客-栈的压入、弹出序列
参考代码:

public boolean IsPopOrder(int[] pushV, int[] popV) {
	Stack<Integer> stack = new Stack<>();
    int j = 0;
    for (int i = 0; i < pushV.length; i++) {
    	stack.push(pushV[i]);
        for (; stack.peek() == popV[j] && j < popV.length && !stack.empty(); j++) {
        	stack.pop();
        }
    }
    return stack.empty();
}

代码解析:用 i 和 j 分别遍历入栈序列、出栈序列,入栈的同时通过条件限制判断出栈的时机,那么本题的难点就在于需要哪些条件。

  1. stack.peek() == popV[j]:这个最好理解,当栈顶元素与当前出栈序列对应的元素相等时才可出栈。
  2. j < popV.lengeh:当出栈序列遍历至序列尾后便要结束遍历。
  3. !stack.empty():不写这个条件运行这段代码时可能会报空栈异常:java.util.EmptyStackException
    具体来说就是当我们用一个空栈来进行出栈(pop)或者取顶(peek)操作时,由于此时栈是空的,无法完成这些操作,所以抛出这个异常。

我们来看看为什么会这样,比如说我们将前两个条件保留,去掉第三个栈非空的条件,然后给出 入栈序列(pushV) 和 出栈序列(popV) 分别为:[2,1,0],[1,2,0]
入栈序列的2入栈,此时栈中只有一个2,于是开始遍历出栈序列
1:栈中的2与出栈序列的1不相等,继续遍历;
2:栈中的2与出栈序列的2相等,出栈序列也没有遍历完,进行出栈操作(此时栈已经空了),继续遍历;
0:这里我们便遇到了栈空时进行取顶(peek)操作,异常的来源便是这里。
综上,一旦栈空就没必要继续遍历出栈序列了,直接跳出循环继续遍历入栈序列→入栈→遍历出栈序列→出栈即可。

(6) 最小栈

原题链接:力扣-最小栈
参考代码:

class MinStack {
    Stack<Integer> stack;
    Stack<Integer> minStack;

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

    public void push(int val) {
        stack.push(val);
        if (minStack.empty()) {
            minStack.push(val);
        } else {
            int peekVal = minStack.peek();
            if (val <= peekVal) {
                minStack.push(val);
            }
        }
    }

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

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

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

代码解析:用两个栈实现能在常数时间内检索到最小元素的一个栈。
一个栈(stack)正常存放栈中元素,另一个栈(minStack)只存最小的那个元素。

3. 栈、虚拟机栈、栈帧的区别

虚拟机栈:定义局部变量时,这些局部变量会存到虚拟机栈中。
栈帧:调用函数时,要给函数开辟一块内存,这块内存就可以理解为栈帧。

二、队列是什么?

队列(Queue)是只允许在一段进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)的原则。
入队列:进行插入操作的一端称为队尾(Tail/Rear)。
出队列:进行删除操作的一端称为队头(Head/Front)。
生活中的例子:排队打饭

注意读音:是Queue不是Queen,即就读作q
队列既可以用双向链表实现,也可以用数组实现,一般用链表实现

1. 队列的方法与功能

Java提供了Queue接口,下面我们使用它来了解队列的使用方式

		Queue<Integer> queue = new LinkedList<>();
        // 尾插法
        queue.offer(1);
        queue.offer(2);
        queue.offer(3);
        queue.offer(4);

        // 头删
        System.out.println(queue.poll());
        System.out.println(queue.peek());
        System.out.println(queue.peek());
        System.out.println(queue.isEmpty());

如上述代码块,我们可以了解到,Queue常用的方法与功能有:

方法功能
boolean offer(E e)入队列
E poll()出队列
peek()获取队头元素
int size()获取队列中有效元素个数
boolean isEmpty()检测队列是否为空

2. 队列的模拟实现

使用链表实现队列:

public class MyQueue {
    static class ListNode {
        public int val;
        public ListNode prev;
        public ListNode next;

        public ListNode(int val) {
            this.val = val;
        }
    }

    public ListNode head;
    public ListNode last;

    public void offer(int val) {
        ListNode node = new ListNode(val);
        if (head == null) {
            head = last = node;
        } else {
            last.next = node;
            node.prev = last;
            last = last.next;
        }
    }

    public int poll() {
        if (head == null) {
            return -1;
        }
        int ret = head.val;
        if (head.next == null) {
            head = null;
            last = null;
        } else {
            head = head.next;
            head.prev = null;
        }
        return ret;
    }

    public int peek() {
        if (head == null) {
            return -1;
        }
        return head.val;
    }

    public boolean isEmpty() {
        return head == null;
    }
}

3. 循环队列

在上文我们实现队列时,使用了链表。那么假如我们要使用数组实现队列时,又该怎么办?
队列包含队头(head)和队尾(last),用一般的思路来构建队列时,会出现空间浪费的现象,如:
队头为0,队尾为7。若此时出队三次,则队头为3,队尾为7,继续添加元素也只会增加队尾坐标,队头前面的三个空间就被浪费掉了。

循环队列就可以很好的解决这个问题,为了不出现空间浪费的情况,在队满且last需要增加时,我们让last从队尾移至队头(如何让last这样移动这里按下不表,后文有详解)。形象点说,就是让队列卷起来,头尾相接,last超出队尾不会消失,会继续在队头“重生”。
举个例子,仍然队头为0,队尾为7,假如数组大小就是8,此时队内放了7个元素,再次让元素入队时,last会直接跑到0位置,这样就算元素出队时head向“后”跑,元素入队时last也会再次利用起head空出的位置,这样就解决了空间浪费问题。

但是,这样的循环队列引出了一个新的问题:我们无法判断队列空或满!
还是上面的例子,队满后head、last都在0位置,而初始状态的空队也是这样的,这就导致我们无法判断队列空或满。

针对这个问题,有以下三种方式解决:(first视为head)

  1. 使用usedSize计数来表示队内元素数量,即 空:us == 0 满:us == 数组长度
  2. 只需要表示空满两个状态的话,不妨定义一个布尔类型flg来表示空满:boolean flg = false;
    即last移动过程中与first相遇就为true,first移动过程中与last相遇就为false。
  3. 浪费一个空间:当last的下一个是first就认为此时队列已满,这样first与last相遇时就是空。

好,这样我们解决完判断队列空或满的问题后,下面便解决如何让last从队尾移动到队头:

如果我们不做处理,正常入队后的操作是last++,但要让last重新回到队头,我们不妨让last = (last + 1) % len,len就是数组长度。
这样让last++后对数组长度取余,就可以让last重新回到队头。

下面我们尝试设计一个浪费一个空间形式的循环队列:
原题链接:力扣-设计循环队列
参考代码:

class MyCircularQueue {
    public int[] elem;
    public int first;
    public int last;

    public MyCircularQueue(int k) {
        elem = new int[k + 1];
    }
    
    public boolean enQueue(int value) {
        if (isFull()) {
            return false;
        }
        elem[last] = value;
        last = (last + 1) % elem.length;
        return true;
    }
    
    public boolean deQueue() {
        if (isEmpty()) {
            return false;
        }
        first = (first + 1) % elem.length;
        return true;
    }
    
    public int Front() {
        if (isEmpty()) {
            return -1;
        }
        return elem[first];
    }
    
    public int Rear() {
        if (isEmpty()) {
            return - 1;
        }
        int index = last == 0 ? elem.length - 1 : last - 1;
        return elem[index];
    }
    
    public boolean isEmpty() {
        return first == last;
    }
    
    public boolean isFull() {
        return first == (last + 1) % elem.length;
    }
}

4. 双端队列

双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。
双端队列的元素可以从队头出队和入队,也可以从队尾出队和入队。
同样的,双端队列也既有链式实现又有线性实现

// 双端队列
// 链式实现
Deque<Integer> queue = new LinkedList<>();
queue.offerFirst(1);
queue.pollFirst();
// 线性实现
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);

既然LinkedList和ArrayDeque这两个类可以实现双端队列,那么进一步,它们俩就可以既当做队列使用,也可以当做栈使用

三、栈和队列的相互实现

1. 用队列实现栈

原题链接:力扣-用队列实现栈
参考代码:

class MyStack {
    public Queue<Integer> queue1;
    public Queue<Integer> queue2;

    public MyStack() {
        queue1 = new LinkedList<>();
        queue2 = new LinkedList<>();
    }

    public void push(int x) {
        if (empty()) {
            queue1.offer(x);
            return;
        }

        if (queue1.isEmpty()) {
            queue1.offer(x);
        } else {
            queue2.offer(x);
        }
    }

    public int pop() {
        int size;
        if (!queue2.isEmpty()) {
            size = queue2.size();
            for (int i = 0; i < size - 1; i++) {
                queue1.offer(queue2.poll());
            }
            return queue2.poll();
        } else {
            size = queue1.size();
            for (int i = 0; i < size - 1; i++) {
                queue2.offer(queue1.poll());
            }
            return queue1.poll();
        }
    }

    public int top() {
        int size;
        int ret = -1;
        if (!queue2.isEmpty()) {
            size = queue2.size();
            for (int i = 0; i < size; i++) {
                ret = queue2.poll();
                queue1.offer(ret);
            }
        } else {
            size = queue1.size();
            for (int i = 0; i < size; i++) {
                ret = queue1.poll();
                queue2.offer(ret);
            }
        }
        return ret;
    }

    public boolean empty() {
        return queue1.isEmpty() && queue2.isEmpty();
    }
}

代码解析:我们看到题目:用队列实现栈。因此我们要知道一个前提:能否用一个队列实现栈?
显然是不行的,因为队列先进先出,栈先进后出,所以一个队列不能实现栈。
接下来我们考虑用两个队列实现栈,我们不妨将问题拆解为两个过程:出栈、入栈。

  1. 出栈:把其中一个非空队列的N-1个元素放到另一个队列当中,当前队列剩下的一个元素就是需要“出栈”的元素。
  2. 入栈:把数据放到非空队列当中。

第一次“入栈”时两个队列都是空,我们直接规定放到第一个队列中即可。

2. 用栈实现队列

原题链接:力扣-用栈实现队列
参考代码:

class MyQueue {
    public Stack<Integer> stack1;
    public Stack<Integer> stack2;

    public MyQueue() {
        stack1 = new Stack<>();
        stack2 = new Stack<>();
    }
    
    public void push(int x) {
        stack1.push(x);
    }
    
    public int pop() {
        if (stack2.empty()) {
            while (!stack1.empty()) {
                stack2.push(stack1.pop());
            }
        }
        return stack2.pop();
    }
    
    public int peek() {
        if (stack2.empty()) {
            while (!stack1.empty()) {
                stack2.push(stack1.pop());
            }
        }
        return stack2.peek();
    }
    
    public boolean empty() {
        return stack1.empty() && stack2.empty();
    }
}

代码解析:与上题类似,首先一个栈无法实现队列。
所以用两个栈,记作入队栈(stack1)和出队栈(stack2):

  1. 入队:把数据放到s1当中。
  2. 出队:s2为空时,需要将s1中的全部元素出栈,再入栈到s2中,这样s2出栈的元素就是要“出队”的元素;s2不为空时,此时s2的出栈元素就是要“出队”的元素。

码字不易,点个赞吧(〃‘▽’〃)。

  • 23
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值